domma-cms 0.86.0 → 0.87.0
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/package.json
CHANGED
|
@@ -87,6 +87,14 @@ export async function todoRoutes(fastify, opts = {}) {
|
|
|
87
87
|
return request.user?.id ?? request.user?.sub ?? null;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** A to-do was completed, removed or re-dated: this person's notices follow at once (0.87). Never throws. */
|
|
91
|
+
async function tell(uid) {
|
|
92
|
+
try {
|
|
93
|
+
const {todoChanged} = await import('../../services/notification-sources.js');
|
|
94
|
+
await todoChanged(uid, await loadAll(uid), localDay(new Date()));
|
|
95
|
+
} catch { /* a notice never breaks a to-do */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
async function loadAll(uid) {
|
|
91
99
|
const {entries} = await listEntries(SLUG, {limit: 10000, sort: 'createdAt', order: 'asc'});
|
|
92
100
|
let items = entries.map(toTodo);
|
|
@@ -185,6 +193,7 @@ export async function todoRoutes(fastify, opts = {}) {
|
|
|
185
193
|
}
|
|
186
194
|
|
|
187
195
|
const updated = await updateEntry(SLUG, id, merged);
|
|
196
|
+
if ('status' in patch || 'dueAt' in patch) tell(entry.data.userId);
|
|
188
197
|
return reply.send(toTodo(updated));
|
|
189
198
|
});
|
|
190
199
|
|
|
@@ -194,6 +203,7 @@ export async function todoRoutes(fastify, opts = {}) {
|
|
|
194
203
|
const all = await loadAll(uid);
|
|
195
204
|
const completed = all.filter(t => t.status === 'completed');
|
|
196
205
|
await Promise.all(completed.map(t => deleteEntry(SLUG, t.id)));
|
|
206
|
+
if (completed.length) tell(uid);
|
|
197
207
|
return reply.send({success: true, removed: completed.length});
|
|
198
208
|
});
|
|
199
209
|
|
|
@@ -207,6 +217,7 @@ export async function todoRoutes(fastify, opts = {}) {
|
|
|
207
217
|
if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Todo not found'});
|
|
208
218
|
|
|
209
219
|
await deleteEntry(SLUG, id);
|
|
220
|
+
tell(entry.data.userId);
|
|
210
221
|
return reply.send({ success: true });
|
|
211
222
|
});
|
|
212
223
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import fs from 'fs';
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import {config, getConfig} from '../config.js';
|
|
21
|
-
import {notify, registerNotificationSource} from './notify.js';
|
|
21
|
+
import {notify, registerNotificationSource, resolveNotification} from './notify.js';
|
|
22
22
|
|
|
23
23
|
const day = (d = new Date()) => d.toISOString().slice(0, 10);
|
|
24
24
|
|
|
@@ -133,6 +133,53 @@ export function recordCrash(err) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/** Register every core source. Called once, before plugins load. */
|
|
136
|
+
/**
|
|
137
|
+
* To-dos (0.87): each person's overdue and due-today to-dos, as at most TWO
|
|
138
|
+
* notices for them (one listing the overdue, one the due today) - never one
|
|
139
|
+
* per task, so twenty overdue tasks are one notice. Completed ones and those
|
|
140
|
+
* with no due day are left out. Pure.
|
|
141
|
+
*
|
|
142
|
+
* @param {object[]} todos - {id, userId, text, status, dueAt}
|
|
143
|
+
* @param {string} today - YYYY-MM-DD (the server's day)
|
|
144
|
+
* @returns {{raise: object[], clear: string[]}} notices, and the keys to resolve (users with nothing in that state)
|
|
145
|
+
*/
|
|
146
|
+
export function todoNotices(todos, today) {
|
|
147
|
+
const endOfDay = new Date(Date.parse(`${today}T00:00:00Z`) + 86400000).toISOString();
|
|
148
|
+
const byUser = new Map();
|
|
149
|
+
for (const t of todos || []) {
|
|
150
|
+
if (!t?.userId) continue;
|
|
151
|
+
if (!byUser.has(t.userId)) byUser.set(t.userId, {overdue: [], today: []});
|
|
152
|
+
if (t.status === 'completed') continue;
|
|
153
|
+
const day = /^(\d{4}-\d{2}-\d{2})/.exec(String(t.dueAt ?? ''))?.[1];
|
|
154
|
+
if (!day) continue;
|
|
155
|
+
if (day < today) byUser.get(t.userId).overdue.push(t);
|
|
156
|
+
else if (day === today) byUser.get(t.userId).today.push(t);
|
|
157
|
+
}
|
|
158
|
+
const name = (t) => String(t.text || 'To-do').replace(/\s+/g, ' ').trim().slice(0, 80) || 'To-do';
|
|
159
|
+
const list = (ts) => ts.slice(0, 5).map(t => `- ${name(t)}`).join('\n') + (ts.length > 5 ? `\n- and ${ts.length - 5} more` : '');
|
|
160
|
+
const raise = [];
|
|
161
|
+
const clear = [];
|
|
162
|
+
for (const [uid, {overdue, today: due}] of byUser) {
|
|
163
|
+
const who = {userIds: [String(uid)]};
|
|
164
|
+
if (overdue.length) {
|
|
165
|
+
raise.push({title: overdue.length === 1 ? `Overdue: ${name(overdue[0])}` : `${overdue.length} to-dos are overdue`,
|
|
166
|
+
body: list(overdue), severity: 'warning', link: '#/todo', dedupeKey: `todo-overdue:${uid}`, audience: who});
|
|
167
|
+
} else clear.push(`todo-overdue:${uid}`);
|
|
168
|
+
if (due.length) {
|
|
169
|
+
raise.push({title: due.length === 1 ? `Due today: ${name(due[0])}` : `${due.length} to-dos are due today`,
|
|
170
|
+
body: list(due), severity: 'info', link: '#/todo', dedupeKey: `todo-today:${uid}:${today}`, audience: who, expiresAt: endOfDay});
|
|
171
|
+
} else clear.push(`todo-today:${uid}:${today}`);
|
|
172
|
+
}
|
|
173
|
+
return {raise, clear};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A person's to-dos changed (completed, deleted, re-dated): if nothing is overdue any more, their notice goes now. */
|
|
177
|
+
export async function todoChanged(userId, todos, today) {
|
|
178
|
+
if (!userId) return;
|
|
179
|
+
const {clear} = todoNotices((todos || []).filter(t => t.userId === userId).concat([{userId}]), today);
|
|
180
|
+
for (const key of clear) await resolveNotification(key, 'core:todo').catch(() => 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
136
183
|
export async function registerCoreSources() {
|
|
137
184
|
const plugins = await import('./plugins.js');
|
|
138
185
|
const {classifyEntitlement} = await import('./pluginEntitlement.js');
|
|
@@ -154,6 +201,20 @@ export async function registerCoreSources() {
|
|
|
154
201
|
}
|
|
155
202
|
});
|
|
156
203
|
|
|
204
|
+
registerNotificationSource({
|
|
205
|
+
id: 'core:todo', label: 'To-dos overdue or due today', severity: 'warning', every: 'daily', atBoot: true,
|
|
206
|
+
description: "Each morning: a person's overdue to-dos, and those due today - one notice each, to that person.",
|
|
207
|
+
async check({notify: raise, resolve, now}) {
|
|
208
|
+
const {listEntries} = await import('./collections.js');
|
|
209
|
+
const {entries} = await listEntries('todo-items', {limit: 100000}).catch(() => ({entries: []}));
|
|
210
|
+
const d = now || new Date();
|
|
211
|
+
const today = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
212
|
+
const {raise: notices, clear} = todoNotices(entries.map(e => ({id: e.id, ...e.data})), today);
|
|
213
|
+
for (const key of clear) await resolve(key);
|
|
214
|
+
for (const n of notices) await raise(n);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
157
218
|
registerNotificationSource({
|
|
158
219
|
id: 'core:licence', label: 'Plugin licences', severity: 'warning', every: 'daily', atBoot: true,
|
|
159
220
|
description: 'A licensed plugin that has stopped, is in its grace period, or ends within 14 days.',
|