Okay, here's an update that addresses some of the comments and demonstrates some more sophisticated behavior.
This has only been lightly tested. Also, I did no work on styling the UI. I don't know what was causing those resubmit errors before--I haven't seen anything like that with either of these versions.
(*These are just descriptions of the representations I'll use.
Deleted ToDo items will be removed from the repository and thus will
have no representation (you could choose to add something with a
deleted at property).*)
Protect[ActiveToDo, CompleteToDo];
ActiveToDo[id, description, createdAt, dueAt, recurrenceFn];
CompleteToDo[id, description, createdAt, dueAt, completedAt];
(Accessors)
Id[todo : (_ActiveToDo | _CompleteToDo)] := todo[[1]];
Description[todo : (_ActiveToDo | _CompleteToDo)] := todo[[2]];
CreatedAt[todo : (_ActiveToDo | _CompleteToDo)] := todo[[3]];
DueAt[todo : (_ActiveToDo | _CompleteToDo)] := todo[[4]];
RecurrenceFn[todo_ActiveToDo] := todo[[5]];
CompletedAt[todo_CompleteToDo] := todo[[5]];
(Display functions)
DisplayToDo[todo_ActiveToDo] :=
Description[todo] <> " (" <> DateString[DueAt[todo], {"DateTimeShort"}] <> ")";
DisplayToDo[todo_CompleteToDo] :=
Style[Description[todo] <> " (" <> DateString[DueAt[todo], {"DateTimeShort"}] <> ")", FontVariations -> {"StrikeThrough" -> True}];
(Transformations)
(I've added the idea of recurrence, so completing a recurring task
results in a new active task along with the original task transformed
to completed. I've provided some sample recurrence functions.)
Complete[todo_CompleteToDo] := {todo};
Complete[todo_ActiveToDo] :=
{ReplacePart[CompleteToDo @@ todo, 5 -> Now],
RecurrenceFn[todo][todo]};
RecurDaily[todo_ActiveToDo] :=
NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, 1], "Recurrence" -> RecurDaily|>];
RecurWeekly[todo_ActiveToDo] :=
NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, 7], "Recurrence" -> RecurWeekly|>];
RecurMonthly[todo_ActiveToDo] :=
NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, Quantity[1, "Months"]], "Recurrence" -> RecurMonthly|>];
RecurNever[_] := Nothing;
(Constructors)
(I didn't bother with constructors that don't use an association
argument, since this is intended to be used with a form.)
NewToDo[] := NewToDo[<||>];
NewToDo[data_Association] :=
With[
{id = CreateUUID[]},
ActiveToDo[
id,
Lookup[data, "Description", id],
Now,
Lookup[data, "DueAt", DatePlus[Now, Lookup[data, "DueDays", 1]]],
Lookup[data, "Recurrence", RecurNever]]];
(Initialize a cloud object and give it some data.)
ToDoList = CloudObject["ToDoList"];
CloudPut[{}, ToDoList];(use this for true initialization, the following was for testing)
initData =
{NewToDo[<|"Description" -> "take out garbage", "Recurrence" -> RecurWeekly|>],
NewToDo[<|"Description" -> "clean filter", "Recurrence" -> RecurMonthly|>],
NewToDo[<|"Description" -> "one time task", "Recurrence" -> RecurNever|>]};
CloudPut[initData, ToDoList];
(The following map allows us to provide a friendly UI for our
recurrence functions.)
RecurrenceMap = {"None" -> RecurNever, "Daily" -> RecurDaily, "Weekly" -> RecurWeekly, "Monthly" -> RecurMonthly};
(Simple creator widget.)
ToDoCreator =
CompoundElement[
<|"Description" -> "String",
{"DueDays", "Days until due"} -> "Number",
"Recurrence" -> Keys[RecurrenceMap]|>];
(The widget for existing tasks is a bit more complicated. Basically,
we'll associate the task id with a pair of booleans that allow you to
either complete or delete the task.)
ToDoCompleterSpec[todo_ActiveToDo] :=
{{Id[todo], DisplayToDo[todo]} -> CompoundElement[{"Complete?" -> "Boolean", "Delete?" -> "Boolean"}]};
(This function is kind of messy, but suffices for a demo. The form
will give us a bunch of old keys with the complete/delete choices,
and it'll also have one item that contains a list of all of the new
tasks. We gather all of these items into groups and process them
against the existing tasks. The HTTPRedirect at the end allows us to
return to the same page, making it sort of like a self-contained
app.)
HandleThenReturn[data_] :=
With[
{oldData = CloudGet[CloudObject["ToDoList"]],
newItems = NewToDo /@ Lookup[data, "NewToDoList", {}] /. RecurrenceMap,
completeIds = Keys[Select[data, #["Complete?"] &]],
deleteIds = Keys[Select[data, #["Delete?"] &]]},
CloudPut[
Flatten[
{newItems,
Map[
If[MemberQ[deleteIds, Id[#]], Nothing, If[MemberQ[completeIds, Id[#]], Complete[#], #]] &,
oldData]}],
ToDoList];
HTTPRedirect[CloudObject["ToDoListWorker"]]];
(And here is the form.)
ToDoListWorker :=
FormFunction[
Flatten[
{ToDoCompleterSpec /@ Cases[CloudGet[CloudObject["ToDoList"]], _ActiveToDo],
Delimiter, {"NewToDoList", "New Tasks"} -> RepeatingElement[ToDoCreator, {0, {0, Infinity}}]}],
HandleThenReturn];
(Deploy the form.)
CloudDeploy[Delayed[ToDoListWorker], "ToDoListWorker"]