apostrophe 4.27.1 → 4.28.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/index.js +3 -0
  3. package/lib/stream-proxy.js +49 -0
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
  5. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
  6. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
  8. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
  9. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
  10. package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
  11. package/modules/@apostrophecms/asset/index.js +3 -2
  12. package/modules/@apostrophecms/attachment/index.js +270 -0
  13. package/modules/@apostrophecms/doc/index.js +8 -2
  14. package/modules/@apostrophecms/doc-type/index.js +81 -1
  15. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
  16. package/modules/@apostrophecms/express/index.js +30 -1
  17. package/modules/@apostrophecms/file/index.js +71 -6
  18. package/modules/@apostrophecms/i18n/index.js +20 -1
  19. package/modules/@apostrophecms/image/index.js +11 -0
  20. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
  21. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
  22. package/modules/@apostrophecms/login/index.js +43 -11
  23. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
  24. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
  25. package/modules/@apostrophecms/page/index.js +9 -11
  26. package/modules/@apostrophecms/page-type/index.js +6 -1
  27. package/modules/@apostrophecms/piece-page-type/index.js +100 -13
  28. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  32. package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
  33. package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
  34. package/modules/@apostrophecms/styles/lib/methods.js +35 -12
  35. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
  36. package/modules/@apostrophecms/task/index.js +9 -1
  37. package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
  38. package/modules/@apostrophecms/ui/index.js +2 -0
  39. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
  40. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
  42. package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
  43. package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
  44. package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
  45. package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
  46. package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
  47. package/modules/@apostrophecms/uploadfs/index.js +15 -1
  48. package/modules/@apostrophecms/url/index.js +419 -1
  49. package/package.json +6 -6
  50. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
  51. package/test/external-front.js +1 -0
  52. package/test/files.js +135 -0
  53. package/test/login-requirements.js +145 -3
  54. package/test/static-build.js +2701 -0
  55. package/test/universal-graph.js +1135 -0
@@ -0,0 +1,461 @@
1
+ import { defineStore } from 'pinia';
2
+ import { ref } from 'vue';
3
+ import WidgetGraph from '../universal/widgetGraph.js';
4
+
5
+ /**
6
+ * @typedef {import('../universal/widgetGraph.js').NodeMeta} NodeMeta
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} Widget
11
+ * @property {string} _id - Unique widget identifier.
12
+ * @property {string} type - Widget type name (e.g. '@apostrophecms/rich-text').
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} GraphEntry
17
+ * @property {WidgetGraph} graph - The directed graph instance.
18
+ * @property {import('vue').Ref<number>} revision - Reactive revision counter.
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} DiscoveredChild
23
+ * @property {string} widgetId - The child widget's _id.
24
+ * @property {string} type - The child widget's type.
25
+ * @property {string} areaId - The _id of the area containing the child.
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} ConsoleLine
30
+ * @property {string} text - The formatted text including %c placeholders.
31
+ * @property {string[]} styles - CSS style strings for each %c substitution.
32
+ */
33
+
34
+ export const useWidgetGraphStore = defineStore('widgetGraph', () => {
35
+ /** @type {Map<string, GraphEntry>} graphKey → { graph, revision } */
36
+ const graphs = new Map();
37
+
38
+ // ── public API ──
39
+
40
+ /**
41
+ * Register a widget and discover its children from area fields
42
+ * in the schema. Handles node creation, edge creation, and
43
+ * revision bumping internally.
44
+ *
45
+ * @param {string} graphKey
46
+ * @param {Widget} widget
47
+ * @param {Object} [options]
48
+ * @param {string} [options.areaId] - The _id of the area containing this widget.
49
+ * @returns {void}
50
+ */
51
+ function registerWidget(graphKey, widget, { areaId } = {}) {
52
+ try {
53
+ const { graph } = _ensure(graphKey);
54
+ const type = widget.type;
55
+ const schema = apos.modules[apos.area.widgetManagers[type]]?.schema || [];
56
+
57
+ graph.addNode(widget._id, {
58
+ type,
59
+ areaId
60
+ });
61
+
62
+ // Discover direct children from the widget's schema and register
63
+ // them as nodes with edges. Each child will recursively register
64
+ // its own children when its AposAreaWidget mounts.
65
+ const children = _discoverChildren(schema, widget);
66
+ for (const child of children) {
67
+ graph.addNode(child.widgetId, {
68
+ type: child.type,
69
+ areaId: child.areaId
70
+ });
71
+ graph.addEdge(widget._id, child.widgetId);
72
+ }
73
+
74
+ _bump(graphKey);
75
+ } catch (e) {
76
+ // eslint-disable-next-line no-console
77
+ console.warn(`[widgetGraph] registerWidget: ${e.message}`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Remove a widget and its entire subtree.
83
+ *
84
+ * @param {string} graphKey
85
+ * @param {string} widgetId
86
+ * @returns {void}
87
+ */
88
+ function unregisterWidget(graphKey, widgetId) {
89
+ const entry = graphs.get(graphKey);
90
+ if (entry && entry.graph.hasNode(widgetId)) {
91
+ entry.graph.removeNode(widgetId);
92
+ _bump(graphKey);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Re-instantiate the graph for a graphKey (e.g. page refresh).
98
+ * Faster than clear() — allocates fresh internals.
99
+ *
100
+ * @param {string} graphKey
101
+ * @returns {void}
102
+ */
103
+ function resetGraph(graphKey) {
104
+ graphs.set(graphKey, {
105
+ graph: new WidgetGraph(),
106
+ revision: ref(0)
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Delete the graph entry entirely (e.g. modal close).
112
+ *
113
+ * @param {string} graphKey
114
+ * @returns {void}
115
+ */
116
+ function destroyGraph(graphKey) {
117
+ graphs.delete(graphKey);
118
+ }
119
+
120
+ /**
121
+ * Get the WidgetGraph instance for a graphKey, or null.
122
+ *
123
+ * @param {string} graphKey
124
+ * @returns {WidgetGraph | null}
125
+ */
126
+ function getGraph(graphKey) {
127
+ return graphs.get(graphKey)?.graph ?? null;
128
+ }
129
+
130
+ /**
131
+ * Get the reactive revision ref for a graphKey.
132
+ * Returns a ref(0) stub if no graph exists yet, so that
133
+ * computed properties don't break.
134
+ *
135
+ * @param {string} graphKey
136
+ * @returns {import('vue').Ref<number>}
137
+ */
138
+ function getRevision(graphKey) {
139
+ return graphs.get(graphKey)?.revision ?? ref(0);
140
+ }
141
+
142
+ /**
143
+ * Get the direct parent of a widget.
144
+ * Reactive — triggers recomputation when the graph changes.
145
+ *
146
+ * @param {string} graphKey
147
+ * @param {string} nodeId
148
+ * @returns {string | null}
149
+ */
150
+ function getParent(graphKey, nodeId) {
151
+ const { graph } = _read(graphKey);
152
+ return graph?.getParent(nodeId) ?? null;
153
+ }
154
+
155
+ /**
156
+ * Check if two widgets share the same direct parent.
157
+ * Two root widgets are NOT considered to share a parent.
158
+ * Reactive — triggers recomputation when the graph changes.
159
+ *
160
+ * @param {string} graphKey
161
+ * @param {string} idA
162
+ * @param {string} idB
163
+ * @returns {boolean}
164
+ */
165
+ function hasCommonParent(graphKey, idA, idB) {
166
+ const { graph } = _read(graphKey);
167
+ return graph?.hasCommonParent(idA, idB) ?? false;
168
+ }
169
+
170
+ /**
171
+ * Get all ancestor ids from the direct parent up to the root.
172
+ * Ordered nearest-first: [directParent, grandparent, …, root].
173
+ * Reactive — triggers recomputation when the graph changes.
174
+ *
175
+ * @param {string} graphKey
176
+ * @param {string} nodeId
177
+ * @returns {string[]}
178
+ */
179
+ function getAncestors(graphKey, nodeId) {
180
+ const { graph } = _read(graphKey);
181
+ return graph?.getAncestors(nodeId) ?? [];
182
+ }
183
+
184
+ /**
185
+ * Check if two widgets share any common ancestor.
186
+ * Two root widgets are NOT considered to share a common ancestor.
187
+ * Reactive — triggers recomputation when the graph changes.
188
+ *
189
+ * @param {string} graphKey
190
+ * @param {string} idA
191
+ * @param {string} idB
192
+ * @returns {boolean}
193
+ */
194
+ function hasCommonAncestor(graphKey, idA, idB) {
195
+ const { graph } = _read(graphKey);
196
+ return graph?.hasCommonAncestor(idA, idB) ?? false;
197
+ }
198
+
199
+ /**
200
+ * Check if `candidateId` is an ancestor of `nodeId`.
201
+ * Reactive — triggers recomputation when the graph changes.
202
+ *
203
+ * @param {string} graphKey
204
+ * @param {string} nodeId
205
+ * @param {string} candidateId
206
+ * @returns {boolean}
207
+ */
208
+ function hasAncestor(graphKey, nodeId, candidateId) {
209
+ const { graph } = _read(graphKey);
210
+ return graph?.hasAncestor(nodeId, candidateId) ?? false;
211
+ }
212
+
213
+ /**
214
+ * Get the direct children of a widget.
215
+ * Reactive — triggers recomputation when the graph changes.
216
+ *
217
+ * @param {string} graphKey
218
+ * @param {string} nodeId
219
+ * @returns {string[]}
220
+ */
221
+ function getChildren(graphKey, nodeId) {
222
+ const { graph } = _read(graphKey);
223
+ return graph?.getChildren(nodeId) ?? [];
224
+ }
225
+
226
+ /**
227
+ * Check if `childId` is a direct child of `nodeId`.
228
+ * Reactive — triggers recomputation when the graph changes.
229
+ *
230
+ * @param {string} graphKey
231
+ * @param {string} nodeId
232
+ * @param {string} childId
233
+ * @returns {boolean}
234
+ */
235
+ function hasChild(graphKey, nodeId, childId) {
236
+ const { graph } = _read(graphKey);
237
+ return graph?.hasChild(nodeId, childId) ?? false;
238
+ }
239
+
240
+ /**
241
+ * Get all descendant ids (transitive children) of a widget.
242
+ * Reactive — triggers recomputation when the graph changes.
243
+ *
244
+ * @param {string} graphKey
245
+ * @param {string} nodeId
246
+ * @returns {string[]}
247
+ */
248
+ function getDescendants(graphKey, nodeId) {
249
+ const { graph } = _read(graphKey);
250
+ return graph?.getDescendants(nodeId) ?? [];
251
+ }
252
+
253
+ /**
254
+ * Check if `candidateId` is a descendant of `nodeId`.
255
+ * Reactive — triggers recomputation when the graph changes.
256
+ *
257
+ * @param {string} graphKey
258
+ * @param {string} nodeId
259
+ * @param {string} candidateId
260
+ * @returns {boolean}
261
+ */
262
+ function hasDescendant(graphKey, nodeId, candidateId) {
263
+ const { graph } = _read(graphKey);
264
+ return graph?.hasDescendant(nodeId, candidateId) ?? false;
265
+ }
266
+
267
+ // ── internal ──
268
+
269
+ /**
270
+ * Read a graph entry and touch its reactive revision counter.
271
+ * When called from within a Vue computed or watcher, this
272
+ * establishes a dependency so the consumer re-evaluates
273
+ * whenever the graph is mutated.
274
+ *
275
+ * @param {string} graphKey
276
+ * @returns {{ graph: WidgetGraph | null, revision: number }}
277
+ */
278
+ function _read(graphKey) {
279
+ const entry = graphs.get(graphKey);
280
+ if (!entry) {
281
+ return {
282
+ graph: null,
283
+ revision: 0
284
+ };
285
+ }
286
+ // Access .value to let Vue's reactivity system track this ref
287
+ const revision = entry.revision.value;
288
+ return {
289
+ graph: entry.graph,
290
+ revision
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Ensure a graph entry exists for the given key.
296
+ * Creates a new WidgetGraph and revision ref(0) if absent.
297
+ *
298
+ * @param {string} graphKey
299
+ * @returns {GraphEntry}
300
+ */
301
+ function _ensure(graphKey) {
302
+ if (!graphs.has(graphKey)) {
303
+ graphs.set(graphKey, {
304
+ graph: new WidgetGraph(),
305
+ revision: ref(0)
306
+ });
307
+ }
308
+ return graphs.get(graphKey);
309
+ }
310
+
311
+ /**
312
+ * Increment the reactive revision counter for a graph key,
313
+ * signalling dependents that the graph has changed.
314
+ *
315
+ * @param {string} graphKey
316
+ * @returns {void}
317
+ */
318
+ function _bump(graphKey) {
319
+ const entry = graphs.get(graphKey);
320
+ if (entry) {
321
+ entry.revision.value++;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Recursively scan schema fields for nested area children.
327
+ * Traverses area, array, and object fields to find all
328
+ * widget items contained within. Only current schema widgets
329
+ * will be discovered. Returns flat list of direct children only.
330
+ *
331
+ * @param {Object[]} schema - Apostrophe field schema array.
332
+ * @param {Object} dataObj - The data object matching the schema.
333
+ * @returns {DiscoveredChild[]}
334
+ */
335
+ function _discoverChildren(schema, dataObj) {
336
+ const children = [];
337
+ for (const field of schema || []) {
338
+ if (field.type === 'area') {
339
+ const area = dataObj[field.name];
340
+ if (area?.items?.length) {
341
+ for (const child of area.items) {
342
+ children.push({
343
+ widgetId: child._id,
344
+ type: child.type,
345
+ areaId: area._id
346
+ });
347
+ }
348
+ }
349
+ } else if (field.type === 'array' && field.schema) {
350
+ for (const item of (dataObj[field.name] || [])) {
351
+ children.push(..._discoverChildren(field.schema, item));
352
+ }
353
+ } else if (field.schema) {
354
+ // object, or any future compound type with a nested schema
355
+ if (dataObj[field.name]) {
356
+ children.push(..._discoverChildren(field.schema, dataObj[field.name]));
357
+ }
358
+ }
359
+ }
360
+ return children;
361
+ }
362
+
363
+ // ── debug ──
364
+
365
+ // Register window.aposWidgetGraph() when debug mode is enabled.
366
+ // Usage from browser console: aposWidgetGraph()
367
+ // Optionally pass a graphKey: aposWidgetGraph('some-doc-id')
368
+ if (typeof window !== 'undefined' && apos.ui.debug) {
369
+ const {
370
+ log, groupCollapsed, groupEnd, table
371
+ } = console;
372
+ window.aposWidgetGraph = function (graphKey) {
373
+ const entries = graphKey
374
+ ? (graphs.has(graphKey) ? [ [ graphKey, graphs.get(graphKey) ] ] : [])
375
+ : [ ...graphs ];
376
+
377
+ if (graphKey && entries.length === 0) {
378
+ log('%c⚠ No graph for key "%s"', 'color:#f44', graphKey);
379
+ return;
380
+ }
381
+ if (entries.length === 0) {
382
+ log('%c⚠ No widget graphs registered.', 'color:#f44');
383
+ return;
384
+ }
385
+
386
+ for (const [ key, { graph } ] of entries) {
387
+ const roots = graph.getRoots();
388
+ groupCollapsed(
389
+ `%cTree ${key} %c(${graph.size} nodes)`,
390
+ 'color:#dcdcaa;font-weight:bold;font-size:13px',
391
+ 'color:#999;font-weight:normal;font-size:12px'
392
+ );
393
+ roots.forEach((root, i) => {
394
+ const lines = _printTree(graph, root, '', i === roots.length - 1, true);
395
+ for (const line of lines) {
396
+ log(line.text, ...line.styles);
397
+ }
398
+ });
399
+ groupEnd();
400
+
401
+ // Raw data table in its own collapsible group
402
+ groupCollapsed(
403
+ `%cData ${key} %c(${graph.size} rows)`,
404
+ 'color:#9cdcfe;font-weight:bold;font-size:13px',
405
+ 'color:#999;font-weight:normal;font-size:12px'
406
+ );
407
+ const rows = {};
408
+ for (const nodeId of graph.getNodes()) {
409
+ const meta = graph.getMeta(nodeId) || {};
410
+ rows[nodeId] = {
411
+ type: meta.type || '',
412
+ areaId: meta.areaId || '',
413
+ parent: graph.getParent(nodeId) || '(root)',
414
+ children: graph.getChildren(nodeId).length,
415
+ depth: graph.getDepth(nodeId)
416
+ };
417
+ }
418
+ table(rows);
419
+ groupEnd();
420
+ }
421
+ };
422
+
423
+ function _printTree(graph, nodeId, prefix, isLast, isRoot = false) {
424
+ const meta = graph.getMeta(nodeId);
425
+ const label = meta?.type || 'unknown';
426
+ const connector = isRoot ? '' : (isLast ? '└─ ' : '├─ ');
427
+ const childPrefix = isRoot ? '' : (prefix + (isLast ? ' ' : '│ '));
428
+ const result = [
429
+ {
430
+ text: `${prefix}${connector}%c${label}%c {${nodeId}}`,
431
+ styles: [ 'color:#4EC9B0;font-weight:bold', 'color:#888' ]
432
+ }
433
+ ];
434
+ const children = graph.getChildren(nodeId);
435
+ children.forEach((childId, i) => {
436
+ result.push(
437
+ ..._printTree(graph, childId, childPrefix, i === children.length - 1)
438
+ );
439
+ });
440
+ return result;
441
+ }
442
+ }
443
+
444
+ return {
445
+ registerWidget,
446
+ unregisterWidget,
447
+ resetGraph,
448
+ destroyGraph,
449
+ getGraph,
450
+ getRevision,
451
+ getParent,
452
+ hasCommonParent,
453
+ getAncestors,
454
+ hasCommonAncestor,
455
+ hasAncestor,
456
+ getChildren,
457
+ hasChild,
458
+ getDescendants,
459
+ hasDescendant
460
+ };
461
+ });