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.
- package/.claude/settings.local.json +38 -1
- package/README.md +92 -2
- package/docs/README.md +7 -6
- package/docs/_sidebar.md +36 -21
- package/docs/_topbar.md +2 -2
- package/docs/architecture/comprehensions.md +282 -0
- package/docs/architecture/fluid-models.md +355 -0
- package/docs/architecture/module-architecture.md +234 -0
- package/docs/{modules.md → architecture/modules.md} +25 -22
- package/docs/cover.md +2 -2
- package/docs/css/docuserve.css +6 -6
- package/docs/examples/examples.md +71 -0
- package/docs/examples/todolist/todo-list-cli-client.md +178 -0
- package/docs/examples/todolist/todo-list-console-client.md +152 -0
- package/docs/examples/todolist/todo-list-model.md +114 -0
- package/docs/examples/todolist/todo-list-server.md +128 -0
- package/docs/examples/todolist/todo-list-web-client.md +177 -0
- package/docs/examples/todolist/todo-list.md +162 -0
- package/docs/getting-started.md +8 -7
- package/docs/index.html +4 -4
- package/docs/{meadow.md → modules/meadow.md} +4 -6
- package/docs/{orator.md → modules/orator.md} +1 -0
- package/docs/{pict.md → modules/pict.md} +30 -8
- package/docs/{utility.md → modules/utility.md} +0 -9
- package/docs/retold-catalog.json +1792 -231
- package/docs/retold-keyword-index.json +136439 -64616
- package/examples/todo-list/Dockerfile +45 -0
- package/examples/todo-list/README.md +394 -0
- package/examples/todo-list/cli-client/package-lock.json +418 -0
- package/examples/todo-list/cli-client/package.json +19 -0
- package/examples/todo-list/cli-client/source/TodoCLI-CLIProgram.js +30 -0
- package/examples/todo-list/cli-client/source/TodoCLI-Run.js +3 -0
- package/examples/todo-list/cli-client/source/commands/add/TodoCLI-Command-Add.js +74 -0
- package/examples/todo-list/cli-client/source/commands/complete/TodoCLI-Command-Complete.js +84 -0
- package/examples/todo-list/cli-client/source/commands/list/TodoCLI-Command-List.js +110 -0
- package/examples/todo-list/cli-client/source/commands/remove/TodoCLI-Command-Remove.js +49 -0
- package/examples/todo-list/cli-client/source/services/TodoCLI-Service-API.js +92 -0
- package/examples/todo-list/console-client/console-client.cjs +913 -0
- package/examples/todo-list/console-client/package-lock.json +426 -0
- package/examples/todo-list/console-client/package.json +19 -0
- package/examples/todo-list/console-client/views/PictView-TUI-Header.cjs +43 -0
- package/examples/todo-list/console-client/views/PictView-TUI-Layout.cjs +58 -0
- package/examples/todo-list/console-client/views/PictView-TUI-StatusBar.cjs +41 -0
- package/examples/todo-list/console-client/views/PictView-TUI-TaskList.cjs +104 -0
- package/examples/todo-list/docker-motd.sh +36 -0
- package/examples/todo-list/docker-run.sh +2 -0
- package/examples/todo-list/docker-shell.sh +2 -0
- package/examples/todo-list/model/MeadowSchema-Task.json +152 -0
- package/examples/todo-list/model/Task-Compiled.json +25 -0
- package/examples/todo-list/model/Task.mddl +15 -0
- package/examples/todo-list/model/data/seeded_todo_events.csv +1001 -0
- package/examples/todo-list/server/database-initialization-service.cjs +273 -0
- package/examples/todo-list/server/package-lock.json +6113 -0
- package/examples/todo-list/server/package.json +19 -0
- package/examples/todo-list/server/server.cjs +138 -0
- package/examples/todo-list/web-client/css/todolist-theme.css +235 -0
- package/examples/todo-list/web-client/generate-build-config.cjs +18 -0
- package/examples/todo-list/web-client/html/index.html +18 -0
- package/examples/todo-list/web-client/package-lock.json +12030 -0
- package/examples/todo-list/web-client/package.json +43 -0
- package/examples/todo-list/web-client/source/TodoList-Application-Config.json +12 -0
- package/examples/todo-list/web-client/source/TodoList-Application.cjs +383 -0
- package/examples/todo-list/web-client/source/providers/Provider-TaskData.cjs +243 -0
- package/examples/todo-list/web-client/source/providers/Router-Config.json +32 -0
- package/examples/todo-list/web-client/source/views/View-Layout.cjs +75 -0
- package/examples/todo-list/web-client/source/views/View-TaskForm.cjs +87 -0
- package/examples/todo-list/web-client/source/views/View-TaskList.cjs +127 -0
- package/examples/todo-list/web-client/source/views/calendar/View-MonthView.cjs +293 -0
- package/examples/todo-list/web-client/source/views/calendar/View-WeekView.cjs +149 -0
- package/examples/todo-list/web-client/source/views/calendar/View-YearView.cjs +226 -0
- package/modules/Include-Retold-Module-List.sh +2 -2
- package/package.json +5 -5
- package/docs/js/pict.min.js +0 -12
- package/docs/js/pict.min.js.map +0 -1
- package/docs/pict-docuserve.min.js +0 -58
- package/docs/pict-docuserve.min.js.map +0 -1
- /package/docs/{architecture.md → architecture/architecture.md} +0 -0
- /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
|
+
}
|