retold 4.0.1 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.claude/settings.local.json +38 -1
  2. package/README.md +92 -2
  3. package/docs/README.md +7 -6
  4. package/docs/_sidebar.md +36 -21
  5. package/docs/_topbar.md +2 -2
  6. package/docs/architecture/comprehensions.md +282 -0
  7. package/docs/architecture/fluid-models.md +355 -0
  8. package/docs/architecture/module-architecture.md +234 -0
  9. package/docs/{modules.md → architecture/modules.md} +25 -22
  10. package/docs/cover.md +2 -2
  11. package/docs/css/docuserve.css +6 -6
  12. package/docs/examples/examples.md +71 -0
  13. package/docs/examples/todolist/todo-list-cli-client.md +178 -0
  14. package/docs/examples/todolist/todo-list-console-client.md +152 -0
  15. package/docs/examples/todolist/todo-list-model.md +114 -0
  16. package/docs/examples/todolist/todo-list-server.md +128 -0
  17. package/docs/examples/todolist/todo-list-web-client.md +177 -0
  18. package/docs/examples/todolist/todo-list.md +162 -0
  19. package/docs/getting-started.md +8 -7
  20. package/docs/index.html +4 -4
  21. package/docs/{meadow.md → modules/meadow.md} +4 -6
  22. package/docs/{orator.md → modules/orator.md} +1 -0
  23. package/docs/{pict.md → modules/pict.md} +30 -8
  24. package/docs/{utility.md → modules/utility.md} +0 -9
  25. package/docs/retold-catalog.json +1792 -231
  26. package/docs/retold-keyword-index.json +136439 -64616
  27. package/examples/todo-list/Dockerfile +45 -0
  28. package/examples/todo-list/README.md +394 -0
  29. package/examples/todo-list/cli-client/package-lock.json +418 -0
  30. package/examples/todo-list/cli-client/package.json +19 -0
  31. package/examples/todo-list/cli-client/source/TodoCLI-CLIProgram.js +30 -0
  32. package/examples/todo-list/cli-client/source/TodoCLI-Run.js +3 -0
  33. package/examples/todo-list/cli-client/source/commands/add/TodoCLI-Command-Add.js +74 -0
  34. package/examples/todo-list/cli-client/source/commands/complete/TodoCLI-Command-Complete.js +84 -0
  35. package/examples/todo-list/cli-client/source/commands/list/TodoCLI-Command-List.js +110 -0
  36. package/examples/todo-list/cli-client/source/commands/remove/TodoCLI-Command-Remove.js +49 -0
  37. package/examples/todo-list/cli-client/source/services/TodoCLI-Service-API.js +92 -0
  38. package/examples/todo-list/console-client/console-client.cjs +913 -0
  39. package/examples/todo-list/console-client/package-lock.json +426 -0
  40. package/examples/todo-list/console-client/package.json +19 -0
  41. package/examples/todo-list/console-client/views/PictView-TUI-Header.cjs +43 -0
  42. package/examples/todo-list/console-client/views/PictView-TUI-Layout.cjs +58 -0
  43. package/examples/todo-list/console-client/views/PictView-TUI-StatusBar.cjs +41 -0
  44. package/examples/todo-list/console-client/views/PictView-TUI-TaskList.cjs +104 -0
  45. package/examples/todo-list/docker-motd.sh +36 -0
  46. package/examples/todo-list/docker-run.sh +2 -0
  47. package/examples/todo-list/docker-shell.sh +2 -0
  48. package/examples/todo-list/model/MeadowSchema-Task.json +152 -0
  49. package/examples/todo-list/model/Task-Compiled.json +25 -0
  50. package/examples/todo-list/model/Task.mddl +15 -0
  51. package/examples/todo-list/model/data/seeded_todo_events.csv +1001 -0
  52. package/examples/todo-list/server/database-initialization-service.cjs +273 -0
  53. package/examples/todo-list/server/package-lock.json +6113 -0
  54. package/examples/todo-list/server/package.json +19 -0
  55. package/examples/todo-list/server/server.cjs +138 -0
  56. package/examples/todo-list/web-client/css/todolist-theme.css +235 -0
  57. package/examples/todo-list/web-client/generate-build-config.cjs +18 -0
  58. package/examples/todo-list/web-client/html/index.html +18 -0
  59. package/examples/todo-list/web-client/package-lock.json +12030 -0
  60. package/examples/todo-list/web-client/package.json +43 -0
  61. package/examples/todo-list/web-client/source/TodoList-Application-Config.json +12 -0
  62. package/examples/todo-list/web-client/source/TodoList-Application.cjs +383 -0
  63. package/examples/todo-list/web-client/source/providers/Provider-TaskData.cjs +243 -0
  64. package/examples/todo-list/web-client/source/providers/Router-Config.json +32 -0
  65. package/examples/todo-list/web-client/source/views/View-Layout.cjs +75 -0
  66. package/examples/todo-list/web-client/source/views/View-TaskForm.cjs +87 -0
  67. package/examples/todo-list/web-client/source/views/View-TaskList.cjs +127 -0
  68. package/examples/todo-list/web-client/source/views/calendar/View-MonthView.cjs +293 -0
  69. package/examples/todo-list/web-client/source/views/calendar/View-WeekView.cjs +149 -0
  70. package/examples/todo-list/web-client/source/views/calendar/View-YearView.cjs +226 -0
  71. package/modules/Include-Retold-Module-List.sh +2 -2
  72. package/package.json +5 -5
  73. package/docs/js/pict.min.js +0 -12
  74. package/docs/js/pict.min.js.map +0 -1
  75. package/docs/pict-docuserve.min.js +0 -58
  76. package/docs/pict-docuserve.min.js.map +0 -1
  77. /package/docs/{architecture.md → architecture/architecture.md} +0 -0
  78. /package/docs/{fable.md → modules/fable.md} +0 -0
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "retold-example-todo-web-client",
3
+ "version": "1.0.0",
4
+ "description": "Retold Example: Todo List Web Client (Pict Application)",
5
+ "main": "source/TodoList-Application.cjs",
6
+ "scripts":
7
+ {
8
+ "postinstall": "node generate-build-config.cjs",
9
+ "build": "npx quack build && npx quack copy",
10
+ "start": "echo 'Build with: npm run build Then start the server in ../server/'"
11
+ },
12
+ "dependencies":
13
+ {
14
+ "pict": "^1.0.343",
15
+ "pict-application": "^1.0.28",
16
+ "pict-router": "^1.0.4",
17
+ "pict-view": "^1.0.64",
18
+ "pict-provider": "^1.0.3"
19
+ },
20
+ "devDependencies":
21
+ {
22
+ "quackage": "^1.0.41"
23
+ },
24
+ "copyFilesSettings":
25
+ {
26
+ "whenFileExists": "overwrite"
27
+ },
28
+ "copyFiles":
29
+ [
30
+ {
31
+ "from": "./html/*",
32
+ "to": "./dist/"
33
+ },
34
+ {
35
+ "from": "./css/*",
36
+ "to": "./dist/css/"
37
+ },
38
+ {
39
+ "from": "./node_modules/pict/dist/*",
40
+ "to": "./dist/js/"
41
+ }
42
+ ]
43
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "Name": "TodoList Pict Application",
3
+ "Hash": "TodoList",
4
+ "MainViewportViewIdentifier": "TodoList-Layout",
5
+ "AutoSolveAfterInitialize": true,
6
+ "AutoRenderMainViewportViewAfterInitialize": false,
7
+ "AutoRenderViewsAfterInitialize": false,
8
+ "pict_configuration":
9
+ {
10
+ "Product": "TodoList-Pict"
11
+ }
12
+ }
@@ -0,0 +1,383 @@
1
+ const libPictApplication = require('pict-application');
2
+ const libPictRouter = require('pict-router');
3
+
4
+ const libProviderTaskData = require('./providers/Provider-TaskData.cjs');
5
+
6
+ const libViewLayout = require('./views/View-Layout.cjs');
7
+ const libViewTaskList = require('./views/View-TaskList.cjs');
8
+ const libViewTaskForm = require('./views/View-TaskForm.cjs');
9
+ const libViewWeekView = require('./views/calendar/View-WeekView.cjs');
10
+ const libViewMonthView = require('./views/calendar/View-MonthView.cjs');
11
+ const libViewYearView = require('./views/calendar/View-YearView.cjs');
12
+
13
+ class TodoListApplication extends libPictApplication
14
+ {
15
+ constructor(pFable, pOptions, pServiceHash)
16
+ {
17
+ super(pFable, pOptions, pServiceHash);
18
+
19
+ // Register the router provider
20
+ this.pict.addProvider('PictRouter', require('./providers/Router-Config.json'), libPictRouter);
21
+
22
+ // Register the data provider
23
+ this.pict.addProvider('TodoList-TaskData', libProviderTaskData.default_configuration, libProviderTaskData);
24
+
25
+ // Register views
26
+ this.pict.addView('TodoList-Layout', libViewLayout.default_configuration, libViewLayout);
27
+ this.pict.addView('TodoList-TaskList', libViewTaskList.default_configuration, libViewTaskList);
28
+ this.pict.addView('TodoList-TaskForm', libViewTaskForm.default_configuration, libViewTaskForm);
29
+ this.pict.addView('TodoList-WeekView', libViewWeekView.default_configuration, libViewWeekView);
30
+ this.pict.addView('TodoList-MonthView', libViewMonthView.default_configuration, libViewMonthView);
31
+ this.pict.addView('TodoList-YearView', libViewYearView.default_configuration, libViewYearView);
32
+ }
33
+
34
+ onAfterInitializeAsync(fCallback)
35
+ {
36
+ // Initialize shared application state
37
+ this.pict.AppData.TodoList =
38
+ {
39
+ Tasks: [],
40
+ AllTasks: [],
41
+ TotalCount: 0,
42
+ FilteredCount: 0,
43
+ SelectedTask: null,
44
+ EditMode: false,
45
+ FormTitle: 'New Task',
46
+
47
+ // List state drives server-side sort and pagination
48
+ ListState:
49
+ {
50
+ SortColumn: 'DueDate',
51
+ SortDirection: 'DESC',
52
+ SearchText: '',
53
+ Begin: 0,
54
+ Cap: 250
55
+ },
56
+
57
+ // Calendar view state for week/month/year views
58
+ CalendarState:
59
+ {
60
+ // The anchor date for the current calendar view (ISO string YYYY-MM-DD)
61
+ AnchorDate: new Date().toISOString().substring(0, 10),
62
+ // Computed summary rows populated by the calendar views
63
+ WeekRows: [],
64
+ MonthRows: [],
65
+ YearRows: [],
66
+ // Labels for the navigation header
67
+ WeekLabel: '',
68
+ MonthLabel: '',
69
+ YearLabel: ''
70
+ }
71
+ };
72
+
73
+ // Load tasks from the API, then render the layout
74
+ this.pict.providers['TodoList-TaskData'].loadTasks(
75
+ (pError) =>
76
+ {
77
+ if (pError)
78
+ {
79
+ this.log.error('Failed to load tasks: ' + pError.message);
80
+ }
81
+
82
+ // Render the layout (which triggers child view renders)
83
+ this.pict.views['TodoList-Layout'].render();
84
+
85
+ return super.onAfterInitializeAsync(fCallback);
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Navigate to a hash route.
91
+ *
92
+ * @param {string} pRoute - The route path (e.g., '/TaskList').
93
+ */
94
+ navigateTo(pRoute)
95
+ {
96
+ this.pict.providers.PictRouter.navigate(pRoute);
97
+ }
98
+
99
+ /**
100
+ * Render a named view into the content area.
101
+ *
102
+ * @param {string} pViewIdentifier - The view to render.
103
+ */
104
+ showView(pViewIdentifier)
105
+ {
106
+ if (pViewIdentifier in this.pict.views)
107
+ {
108
+ this.pict.views[pViewIdentifier].render();
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Change the sort order from the toolbar dropdown and reload.
114
+ *
115
+ * Called from the sort <select> element in the TaskList filter bar.
116
+ * The value is a "Column~Direction" string (e.g. "DueDate~DESC").
117
+ *
118
+ * @param {string} pSortValue - "Column~Direction" from the dropdown.
119
+ */
120
+ changeSortOrder(pSortValue)
121
+ {
122
+ let tmpParts = pSortValue.split('~');
123
+ if (tmpParts.length === 2)
124
+ {
125
+ this.pict.AppData.TodoList.ListState.SortColumn = tmpParts[0];
126
+ this.pict.AppData.TodoList.ListState.SortDirection = tmpParts[1];
127
+ }
128
+
129
+ let tmpProvider = this.pict.providers['TodoList-TaskData'];
130
+ tmpProvider.loadTasks(
131
+ () =>
132
+ {
133
+ this.pict.views['TodoList-TaskList'].render();
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Search tasks by name or description.
139
+ *
140
+ * Reads the search input value, stores it in ListState, and reloads.
141
+ * Called from the search input in the TaskList toolbar on Enter or
142
+ * from the search button click.
143
+ */
144
+ searchTasks()
145
+ {
146
+ let tmpInput = document.getElementById('tl-search-input');
147
+ this.pict.AppData.TodoList.ListState.SearchText = tmpInput ? tmpInput.value : '';
148
+
149
+ let tmpProvider = this.pict.providers['TodoList-TaskData'];
150
+ tmpProvider.loadTasks(
151
+ () =>
152
+ {
153
+ this.pict.views['TodoList-TaskList'].render();
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Clear the search filter and reload the full list.
159
+ */
160
+ clearSearch()
161
+ {
162
+ this.pict.AppData.TodoList.ListState.SearchText = '';
163
+
164
+ let tmpProvider = this.pict.providers['TodoList-TaskData'];
165
+ tmpProvider.loadTasks(
166
+ () =>
167
+ {
168
+ this.pict.views['TodoList-TaskList'].render();
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Show the task form in "add" mode.
174
+ */
175
+ addTask()
176
+ {
177
+ this.pict.AppData.TodoList.SelectedTask =
178
+ {
179
+ IDTask: 0,
180
+ Name: '',
181
+ Description: '',
182
+ DueDate: '',
183
+ LengthInHours: 0,
184
+ Status: 'Pending'
185
+ };
186
+ this.pict.AppData.TodoList.EditMode = false;
187
+ this.pict.AppData.TodoList.FormTitle = 'New Task';
188
+ this.pict.views['TodoList-TaskForm'].render();
189
+ }
190
+
191
+ /**
192
+ * Show the task form in "edit" mode for a given task ID.
193
+ *
194
+ * @param {number|string} pIDTask - The task ID to edit.
195
+ */
196
+ editTask(pIDTask)
197
+ {
198
+ let tmpIDTask = parseInt(pIDTask, 10);
199
+ let tmpTasks = this.pict.AppData.TodoList.Tasks;
200
+ let tmpTask = null;
201
+
202
+ for (let i = 0; i < tmpTasks.length; i++)
203
+ {
204
+ if (tmpTasks[i].IDTask === tmpIDTask)
205
+ {
206
+ tmpTask = tmpTasks[i];
207
+ break;
208
+ }
209
+ }
210
+
211
+ if (!tmpTask)
212
+ {
213
+ this.log.warn('Task not found: ' + pIDTask);
214
+ return;
215
+ }
216
+
217
+ // Clone the task so edits don't modify the list data directly
218
+ this.pict.AppData.TodoList.SelectedTask = JSON.parse(JSON.stringify(tmpTask));
219
+ this.pict.AppData.TodoList.EditMode = true;
220
+ this.pict.AppData.TodoList.FormTitle = 'Edit Task';
221
+ this.pict.views['TodoList-TaskForm'].render();
222
+ }
223
+
224
+ /**
225
+ * Save the task from the form (create or update based on EditMode).
226
+ */
227
+ saveTask()
228
+ {
229
+ let tmpTaskData =
230
+ {
231
+ Name: document.getElementById('taskName').value,
232
+ Description: document.getElementById('taskDescription').value,
233
+ DueDate: document.getElementById('taskDueDate').value,
234
+ LengthInHours: parseFloat(document.getElementById('taskHours').value) || 0,
235
+ Status: document.getElementById('taskStatus').value
236
+ };
237
+
238
+ let tmpProvider = this.pict.providers['TodoList-TaskData'];
239
+ let tmpSelf = this;
240
+
241
+ let tmpAfterSave = (pError) =>
242
+ {
243
+ if (pError)
244
+ {
245
+ tmpSelf.log.error('Save failed: ' + pError.message);
246
+ return;
247
+ }
248
+ // Reload tasks and show the list
249
+ tmpProvider.loadTasks(
250
+ () =>
251
+ {
252
+ tmpSelf.pict.views['TodoList-TaskList'].render();
253
+ });
254
+ };
255
+
256
+ if (this.pict.AppData.TodoList.EditMode)
257
+ {
258
+ tmpTaskData.IDTask = this.pict.AppData.TodoList.SelectedTask.IDTask;
259
+ tmpProvider.updateTask(tmpTaskData, tmpAfterSave);
260
+ }
261
+ else
262
+ {
263
+ tmpProvider.createTask(tmpTaskData, tmpAfterSave);
264
+ }
265
+ }
266
+
267
+ // ──────────────────────────────────────────────────────────────
268
+ // Calendar view helpers
269
+ // ──────────────────────────────────────────────────────────────
270
+
271
+ /**
272
+ * Show a calendar view. Loads all tasks if they haven't been
273
+ * fetched yet, then renders the requested view.
274
+ *
275
+ * @param {string} pViewIdentifier - 'TodoList-WeekView', etc.
276
+ */
277
+ showCalendarView(pViewIdentifier)
278
+ {
279
+ let tmpProvider = this.pict.providers['TodoList-TaskData'];
280
+ let tmpSelf = this;
281
+
282
+ // Only fetch once per session; subsequent navigations reuse the cache
283
+ if (this.pict.AppData.TodoList.AllTasks.length > 0)
284
+ {
285
+ tmpSelf.pict.views[pViewIdentifier].render();
286
+ return;
287
+ }
288
+
289
+ tmpProvider.loadAllTasks(
290
+ (pError) =>
291
+ {
292
+ if (pError)
293
+ {
294
+ tmpSelf.log.error('Failed to load all tasks: ' + pError.message);
295
+ }
296
+ tmpSelf.pict.views[pViewIdentifier].render();
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Navigate the calendar anchor date by a delta and re-render the view.
302
+ *
303
+ * @param {string} pUnit - 'week', 'month', or 'year'
304
+ * @param {number} pDelta - Number of units to shift (negative = back, positive = forward)
305
+ */
306
+ calendarNavigate(pUnit, pDelta)
307
+ {
308
+ let tmpCal = this.pict.AppData.TodoList.CalendarState;
309
+ let tmpDate = new Date(tmpCal.AnchorDate + 'T00:00:00');
310
+
311
+ if (pUnit === 'week')
312
+ {
313
+ tmpDate.setDate(tmpDate.getDate() + (pDelta * 7));
314
+ }
315
+ else if (pUnit === 'month')
316
+ {
317
+ tmpDate.setMonth(tmpDate.getMonth() + pDelta);
318
+ }
319
+ else if (pUnit === 'year')
320
+ {
321
+ tmpDate.setFullYear(tmpDate.getFullYear() + pDelta);
322
+ }
323
+
324
+ tmpCal.AnchorDate = tmpDate.toISOString().substring(0, 10);
325
+
326
+ let tmpViewMap =
327
+ {
328
+ week: 'TodoList-WeekView',
329
+ month: 'TodoList-MonthView',
330
+ year: 'TodoList-YearView'
331
+ };
332
+
333
+ this.pict.views[tmpViewMap[pUnit]].render();
334
+ }
335
+
336
+ /**
337
+ * Jump the calendar anchor to today and re-render the current calendar view.
338
+ *
339
+ * @param {string} pUnit - 'week', 'month', or 'year'
340
+ */
341
+ calendarToday(pUnit)
342
+ {
343
+ this.pict.AppData.TodoList.CalendarState.AnchorDate = new Date().toISOString().substring(0, 10);
344
+
345
+ let tmpViewMap =
346
+ {
347
+ week: 'TodoList-WeekView',
348
+ month: 'TodoList-MonthView',
349
+ year: 'TodoList-YearView'
350
+ };
351
+
352
+ this.pict.views[tmpViewMap[pUnit]].render();
353
+ }
354
+
355
+ /**
356
+ * Delete a task and refresh the list.
357
+ *
358
+ * @param {number|string} pIDTask - The task ID to delete.
359
+ */
360
+ deleteTask(pIDTask)
361
+ {
362
+ let tmpProvider = this.pict.providers['TodoList-TaskData'];
363
+ let tmpSelf = this;
364
+
365
+ tmpProvider.deleteTask(pIDTask,
366
+ (pError) =>
367
+ {
368
+ if (pError)
369
+ {
370
+ tmpSelf.log.error('Delete failed: ' + pError.message);
371
+ return;
372
+ }
373
+ tmpProvider.loadTasks(
374
+ () =>
375
+ {
376
+ tmpSelf.pict.views['TodoList-TaskList'].render();
377
+ });
378
+ });
379
+ }
380
+ }
381
+
382
+ module.exports = TodoListApplication;
383
+ module.exports.default_configuration = require('./TodoList-Application-Config.json');
@@ -0,0 +1,243 @@
1
+ const libPictProvider = require('pict-provider');
2
+
3
+ const _ProviderConfiguration =
4
+ {
5
+ ProviderIdentifier: 'TodoList-TaskData',
6
+ AutoInitialize: true,
7
+ AutoInitializeOrdinal: 0
8
+ };
9
+
10
+ class TaskDataProvider extends libPictProvider
11
+ {
12
+ constructor(pFable, pOptions, pServiceHash)
13
+ {
14
+ super(pFable, pOptions, pServiceHash);
15
+ }
16
+
17
+ /**
18
+ * Build the Meadow Reads URL from the current ListState.
19
+ *
20
+ * Uses the FSF (Filter Sort Field) instruction in the FilteredTo path
21
+ * segment so the sort happens server-side in SQL.
22
+ *
23
+ * @returns {string} The API URL for the Reads endpoint.
24
+ */
25
+ buildReadsURL()
26
+ {
27
+ let tmpListState = this.pict.AppData.TodoList.ListState;
28
+
29
+ let tmpSortColumn = tmpListState.SortColumn || 'IDTask';
30
+ let tmpSortDirection = tmpListState.SortDirection || 'DESC';
31
+ let tmpBegin = tmpListState.Begin || 0;
32
+ let tmpCap = tmpListState.Cap || 250;
33
+
34
+ // Build the filter stanzas -- search first, then sort
35
+ let tmpFilterParts = [];
36
+
37
+ // If a search term is present, add LIKE filters on Name and Description (OR-connected)
38
+ let tmpSearchText = (tmpListState.SearchText || '').trim();
39
+ if (tmpSearchText.length > 0)
40
+ {
41
+ // Encode % for the LIKE wildcards around the search term
42
+ let tmpEncodedTerm = encodeURIComponent(tmpSearchText);
43
+ tmpFilterParts.push('FBVOR~Name~LK~%25' + tmpEncodedTerm + '%25');
44
+ tmpFilterParts.push('FBVOR~Description~LK~%25' + tmpEncodedTerm + '%25');
45
+ }
46
+
47
+ // Always add the sort stanza last
48
+ tmpFilterParts.push('FSF~' + tmpSortColumn + '~' + tmpSortDirection + '~0');
49
+
50
+ let tmpFilter = tmpFilterParts.join('~');
51
+
52
+ return '/1.0/Tasks/FilteredTo/' + tmpFilter + '/' + tmpBegin + '/' + tmpCap;
53
+ }
54
+
55
+ /**
56
+ * Build the filter-only portion (no sort, no pagination) for the Count endpoint.
57
+ *
58
+ * @returns {string|null} The filter stanza string, or null if no search is active.
59
+ */
60
+ buildSearchFilter()
61
+ {
62
+ let tmpListState = this.pict.AppData.TodoList.ListState;
63
+ let tmpSearchText = (tmpListState.SearchText || '').trim();
64
+
65
+ if (tmpSearchText.length === 0)
66
+ {
67
+ return null;
68
+ }
69
+
70
+ let tmpEncodedTerm = encodeURIComponent(tmpSearchText);
71
+ let tmpFilterParts = [];
72
+ tmpFilterParts.push('FBVOR~Name~LK~%25' + tmpEncodedTerm + '%25');
73
+ tmpFilterParts.push('FBVOR~Description~LK~%25' + tmpEncodedTerm + '%25');
74
+
75
+ return tmpFilterParts.join('~');
76
+ }
77
+
78
+ /**
79
+ * Load tasks from the API using current sort and pagination state.
80
+ *
81
+ * Also fetches the total record count (unfiltered) and, when a search
82
+ * is active, the filtered count so the toolbar can display
83
+ * "showing X of Y records".
84
+ *
85
+ * @param {Function} fCallback - Callback(pError, pTasks)
86
+ */
87
+ loadTasks(fCallback)
88
+ {
89
+ let tmpURL = this.buildReadsURL();
90
+ let tmpSearchFilter = this.buildSearchFilter();
91
+
92
+ // Always fetch the overall total count
93
+ let tmpCountURL = '/1.0/Tasks/Count';
94
+
95
+ // When a search filter is active, also fetch the filtered count
96
+ let tmpFilteredCountURL = tmpSearchFilter
97
+ ? '/1.0/Tasks/Count/FilteredTo/' + tmpSearchFilter
98
+ : null;
99
+
100
+ let tmpFetches = [fetch(tmpURL), fetch(tmpCountURL)];
101
+ if (tmpFilteredCountURL)
102
+ {
103
+ tmpFetches.push(fetch(tmpFilteredCountURL));
104
+ }
105
+
106
+ Promise.all(tmpFetches)
107
+ .then(
108
+ (pResponses) =>
109
+ {
110
+ return Promise.all(pResponses.map((pR) => { return pR.json(); }));
111
+ })
112
+ .then(
113
+ (pResults) =>
114
+ {
115
+ let tmpTasks = pResults[0];
116
+ let tmpTotalCount = pResults[1];
117
+ let tmpFilteredCount = pResults[2];
118
+
119
+ if (Array.isArray(tmpTasks))
120
+ {
121
+ this.pict.AppData.TodoList.Tasks = tmpTasks;
122
+ }
123
+
124
+ // Store the total record count
125
+ if (tmpTotalCount && typeof tmpTotalCount.Count === 'number')
126
+ {
127
+ this.pict.AppData.TodoList.TotalCount = tmpTotalCount.Count;
128
+ }
129
+
130
+ // Store the filtered count (or fall back to total when not searching)
131
+ if (tmpFilteredCount && typeof tmpFilteredCount.Count === 'number')
132
+ {
133
+ this.pict.AppData.TodoList.FilteredCount = tmpFilteredCount.Count;
134
+ }
135
+ else
136
+ {
137
+ this.pict.AppData.TodoList.FilteredCount = this.pict.AppData.TodoList.TotalCount;
138
+ }
139
+
140
+ return fCallback(null, tmpTasks);
141
+ })
142
+ .catch(
143
+ (pError) =>
144
+ {
145
+ this.log.error('Error loading tasks: ' + pError.message);
146
+ return fCallback(pError);
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Load ALL tasks sorted by DueDate ascending.
152
+ *
153
+ * Used by the calendar views (week, month, year) which need the full
154
+ * data set to compute per-period summaries. Stores the result in
155
+ * AppData.TodoList.AllTasks.
156
+ *
157
+ * @param {Function} fCallback - Callback(pError, pTasks)
158
+ */
159
+ loadAllTasks(fCallback)
160
+ {
161
+ let tmpURL = '/1.0/Tasks/FilteredTo/FSF~DueDate~ASC~0/0/10000';
162
+
163
+ fetch(tmpURL)
164
+ .then(
165
+ (pResponse) =>
166
+ {
167
+ return pResponse.json();
168
+ })
169
+ .then(
170
+ (pTasks) =>
171
+ {
172
+ if (Array.isArray(pTasks))
173
+ {
174
+ this.pict.AppData.TodoList.AllTasks = pTasks;
175
+ }
176
+ return fCallback(null, pTasks);
177
+ })
178
+ .catch(
179
+ (pError) =>
180
+ {
181
+ this.log.error('Error loading all tasks: ' + pError.message);
182
+ return fCallback(pError);
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Create a new task.
188
+ *
189
+ * @param {Object} pTaskData - The task record to create.
190
+ * @param {Function} fCallback - Callback(pError, pRecord)
191
+ */
192
+ createTask(pTaskData, fCallback)
193
+ {
194
+ fetch('/1.0/Task',
195
+ {
196
+ method: 'POST',
197
+ headers: { 'Content-Type': 'application/json' },
198
+ body: JSON.stringify(pTaskData)
199
+ })
200
+ .then((pResponse) => { return pResponse.json(); })
201
+ .then((pRecord) => { return fCallback(null, pRecord); })
202
+ .catch((pError) => { return fCallback(pError); });
203
+ }
204
+
205
+ /**
206
+ * Update an existing task.
207
+ *
208
+ * @param {Object} pTaskData - The task record to update (must include IDTask).
209
+ * @param {Function} fCallback - Callback(pError, pRecord)
210
+ */
211
+ updateTask(pTaskData, fCallback)
212
+ {
213
+ fetch('/1.0/Task',
214
+ {
215
+ method: 'PUT',
216
+ headers: { 'Content-Type': 'application/json' },
217
+ body: JSON.stringify(pTaskData)
218
+ })
219
+ .then((pResponse) => { return pResponse.json(); })
220
+ .then((pRecord) => { return fCallback(null, pRecord); })
221
+ .catch((pError) => { return fCallback(pError); });
222
+ }
223
+
224
+ /**
225
+ * Delete a task by ID.
226
+ *
227
+ * @param {number} pIDTask - The ID of the task to delete.
228
+ * @param {Function} fCallback - Callback(pError, pResult)
229
+ */
230
+ deleteTask(pIDTask, fCallback)
231
+ {
232
+ fetch('/1.0/Task/' + pIDTask,
233
+ {
234
+ method: 'DELETE'
235
+ })
236
+ .then((pResponse) => { return pResponse.json(); })
237
+ .then((pResult) => { return fCallback(null, pResult); })
238
+ .catch((pError) => { return fCallback(pError); });
239
+ }
240
+ }
241
+
242
+ module.exports = TaskDataProvider;
243
+ module.exports.default_configuration = _ProviderConfiguration;
@@ -0,0 +1,32 @@
1
+ {
2
+ "ProviderIdentifier": "Pict-Router",
3
+ "AutoInitialize": true,
4
+ "AutoInitializeOrdinal": 0,
5
+ "Routes":
6
+ [
7
+ {
8
+ "path": "/TaskList",
9
+ "template": "{~LV:Pict.PictApplication.showView(`TodoList-TaskList`)~}"
10
+ },
11
+ {
12
+ "path": "/TaskForm",
13
+ "template": "{~LV:Pict.PictApplication.addTask()~}"
14
+ },
15
+ {
16
+ "path": "/TaskForm/:id",
17
+ "template": "{~LV:Pict.PictApplication.editTask(`{~D:RouteData.id~}`)~}"
18
+ },
19
+ {
20
+ "path": "/WeekView",
21
+ "template": "{~LV:Pict.PictApplication.showCalendarView(`TodoList-WeekView`)~}"
22
+ },
23
+ {
24
+ "path": "/MonthView",
25
+ "template": "{~LV:Pict.PictApplication.showCalendarView(`TodoList-MonthView`)~}"
26
+ },
27
+ {
28
+ "path": "/YearView",
29
+ "template": "{~LV:Pict.PictApplication.showCalendarView(`TodoList-YearView`)~}"
30
+ }
31
+ ]
32
+ }