apostrophe 4.27.1 → 4.28.1
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 +15 -0
- package/CHANGELOG.md +40 -0
- package/README.md +142 -0
- package/index.js +3 -0
- package/lib/stream-proxy.js +49 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
- package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
- package/modules/@apostrophecms/asset/index.js +3 -2
- package/modules/@apostrophecms/attachment/index.js +270 -0
- package/modules/@apostrophecms/doc/index.js +8 -2
- package/modules/@apostrophecms/doc-type/index.js +81 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
- package/modules/@apostrophecms/express/index.js +30 -1
- package/modules/@apostrophecms/file/index.js +70 -6
- package/modules/@apostrophecms/i18n/index.js +20 -1
- package/modules/@apostrophecms/image/index.js +11 -0
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
- package/modules/@apostrophecms/login/index.js +43 -11
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
- package/modules/@apostrophecms/page/index.js +9 -11
- package/modules/@apostrophecms/page-type/index.js +6 -1
- package/modules/@apostrophecms/piece-page-type/index.js +100 -13
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
- package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
- package/modules/@apostrophecms/styles/lib/methods.js +35 -12
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
- package/modules/@apostrophecms/task/index.js +9 -1
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
- package/modules/@apostrophecms/ui/index.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
- package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
- package/modules/@apostrophecms/uploadfs/index.js +15 -1
- package/modules/@apostrophecms/url/index.js +419 -1
- package/package.json +5 -5
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/external-front.js +1 -0
- package/test/files.js +264 -0
- package/test/login-requirements.js +145 -3
- package/test/static-build.js +2701 -0
- package/test/universal-graph.js +1135 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rooted forest (collection of trees) with bidirectional
|
|
3
|
+
* adjacency and per-node metadata.
|
|
4
|
+
*
|
|
5
|
+
* Constraints:
|
|
6
|
+
* - Each node has at most one parent (tree structure).
|
|
7
|
+
* - No cycles (enforced).
|
|
8
|
+
* - Each node carries an arbitrary metadata object.
|
|
9
|
+
*
|
|
10
|
+
* Optimized for three query patterns:
|
|
11
|
+
* - Get the parent of a node: O(1)
|
|
12
|
+
* - Get the children of a node: O(1)
|
|
13
|
+
* - Check if two nodes share the same parent: O(1)
|
|
14
|
+
*
|
|
15
|
+
* Every edge is stored twice (once in each direction map),
|
|
16
|
+
* which costs 2x edge storage but gives O(1) lookups in either direction.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {Object} NodeMeta - arbitrary metadata object attached to each node
|
|
19
|
+
*/
|
|
20
|
+
export default class DirectedGraph {
|
|
21
|
+
/** @type {Map<string, Set<string>>} nodeId → Set of child ids */
|
|
22
|
+
#children;
|
|
23
|
+
|
|
24
|
+
/** @type {Map<string, string|null>} nodeId → parent id or null */
|
|
25
|
+
#parents;
|
|
26
|
+
|
|
27
|
+
/** @type {Map<string, NodeMeta>} nodeId → metadata object */
|
|
28
|
+
#meta;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.#children = new Map();
|
|
32
|
+
this.#parents = new Map();
|
|
33
|
+
this.#meta = new Map();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ensure a node exists in the graph.
|
|
38
|
+
* If the node already exists, its metadata is shallow-merged
|
|
39
|
+
* with any new values provided.
|
|
40
|
+
* @param {string} id
|
|
41
|
+
* @param {NodeMeta} [meta={}] - metadata to attach to the node
|
|
42
|
+
* @returns {this}
|
|
43
|
+
*/
|
|
44
|
+
addNode(id, meta = {}) {
|
|
45
|
+
if (!this.#children.has(id)) {
|
|
46
|
+
this.#children.set(id, new Set());
|
|
47
|
+
this.#parents.set(id, null);
|
|
48
|
+
this.#meta.set(id, { ...meta });
|
|
49
|
+
} else if (Object.keys(meta).length > 0) {
|
|
50
|
+
Object.assign(this.#meta.get(id), meta);
|
|
51
|
+
}
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add a directed edge from `parentId` to `childId`.
|
|
57
|
+
* Both nodes are created if they don't exist.
|
|
58
|
+
* Throws if:
|
|
59
|
+
* - the child already has a parent (single-parent constraint)
|
|
60
|
+
* - the edge would create a cycle
|
|
61
|
+
* @param {string} parentId
|
|
62
|
+
* @param {string} childId
|
|
63
|
+
* @returns {this}
|
|
64
|
+
*/
|
|
65
|
+
addEdge(parentId, childId) {
|
|
66
|
+
if (parentId === childId) {
|
|
67
|
+
throw new Error(`Self-loop not allowed: "${parentId}"`);
|
|
68
|
+
}
|
|
69
|
+
this.addNode(parentId);
|
|
70
|
+
this.addNode(childId);
|
|
71
|
+
|
|
72
|
+
const existingParent = this.#parents.get(childId);
|
|
73
|
+
if (existingParent !== null && existingParent !== parentId) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Node "${childId}" already has parent "${existingParent}". ` +
|
|
76
|
+
'Each node can have at most one parent.'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
// Already linked — nothing to do
|
|
80
|
+
if (existingParent === parentId) {
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cycle check: walk up from parentId; if we reach childId it's a cycle
|
|
85
|
+
if (this.hasAncestor(parentId, childId)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Adding edge "${parentId}" → "${childId}" would create a cycle`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.#children.get(parentId).add(childId);
|
|
92
|
+
this.#parents.set(childId, parentId);
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Remove the directed edge from `parentId` to `childId`.
|
|
98
|
+
* No-op if the edge doesn't exist.
|
|
99
|
+
* @param {string} parentId
|
|
100
|
+
* @param {string} childId
|
|
101
|
+
* @returns {this}
|
|
102
|
+
*/
|
|
103
|
+
removeEdge(parentId, childId) {
|
|
104
|
+
if (this.#parents.get(childId) === parentId) {
|
|
105
|
+
this.#parents.set(childId, null);
|
|
106
|
+
this.#children.get(parentId)?.delete(childId);
|
|
107
|
+
}
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove a node and its entire subtree (all descendants).
|
|
113
|
+
* The node is detached from its parent; every descendant
|
|
114
|
+
* is also removed from the graph.
|
|
115
|
+
* @param {string} id
|
|
116
|
+
* @returns {this}
|
|
117
|
+
*/
|
|
118
|
+
removeNode(id) {
|
|
119
|
+
if (!this.#children.has(id)) {
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
// Detach from parent
|
|
123
|
+
const parentId = this.#parents.get(id);
|
|
124
|
+
if (parentId !== null && parentId !== undefined) {
|
|
125
|
+
this.#children.get(parentId)?.delete(id);
|
|
126
|
+
}
|
|
127
|
+
// BFS to collect the entire subtree (including `id` itself)
|
|
128
|
+
const queue = [ id ];
|
|
129
|
+
const toRemove = [];
|
|
130
|
+
while (queue.length > 0) {
|
|
131
|
+
const current = queue.shift();
|
|
132
|
+
toRemove.push(current);
|
|
133
|
+
const children = this.#children.get(current);
|
|
134
|
+
if (children) {
|
|
135
|
+
for (const childId of children) {
|
|
136
|
+
queue.push(childId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Remove all collected nodes
|
|
141
|
+
for (const nodeId of toRemove) {
|
|
142
|
+
this.#children.delete(nodeId);
|
|
143
|
+
this.#parents.delete(nodeId);
|
|
144
|
+
this.#meta.delete(nodeId);
|
|
145
|
+
}
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a node exists in the graph.
|
|
151
|
+
* @param {string} id
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
hasNode(id) {
|
|
155
|
+
return this.#children.has(id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if a directed edge exists from `parentId` to `childId`.
|
|
160
|
+
* @param {string} parentId
|
|
161
|
+
* @param {string} childId
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
hasEdge(parentId, childId) {
|
|
165
|
+
return this.#parents.get(childId) === parentId;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if `childId` is a direct child of `nodeId`.
|
|
170
|
+
* Semantic convenience — equivalent to `hasEdge(nodeId, childId)`.
|
|
171
|
+
* @param {string} nodeId
|
|
172
|
+
* @param {string} childId
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
hasChild(nodeId, childId) {
|
|
176
|
+
return this.#parents.get(childId) === nodeId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the direct parent of a node.
|
|
181
|
+
* @param {string} id
|
|
182
|
+
* @returns {string|null} The parent id, or null if root / not found
|
|
183
|
+
*/
|
|
184
|
+
getParent(id) {
|
|
185
|
+
return this.#parents.get(id) ?? null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the direct children of a node.
|
|
190
|
+
* @param {string} id
|
|
191
|
+
* @returns {string[]} Array of child ids (empty if none or node doesn't exist)
|
|
192
|
+
*/
|
|
193
|
+
getChildren(id) {
|
|
194
|
+
const children = this.#children.get(id);
|
|
195
|
+
return children ? [ ...children ] : [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if two nodes share the same direct parent.
|
|
200
|
+
* Two root nodes (parent === null) are NOT considered to share a parent.
|
|
201
|
+
* @param {string} idA
|
|
202
|
+
* @param {string} idB
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
hasCommonParent(idA, idB) {
|
|
206
|
+
const parentA = this.#parents.get(idA);
|
|
207
|
+
const parentB = this.#parents.get(idB);
|
|
208
|
+
return parentA != null && parentA === parentB;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if two nodes share any common ancestor
|
|
213
|
+
* (not just the direct parent — any node in both ancestor chains).
|
|
214
|
+
* Collects ancestors of one node into a Set, then walks the other's
|
|
215
|
+
* chain checking for membership. O(d1 + d2) time.
|
|
216
|
+
* Two root nodes are NOT considered to share a common ancestor.
|
|
217
|
+
* @param {string} idA
|
|
218
|
+
* @param {string} idB
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
hasCommonAncestor(idA, idB) {
|
|
222
|
+
const ancestorsA = new Set();
|
|
223
|
+
let current = this.#parents.get(idA) ?? null;
|
|
224
|
+
while (current !== null) {
|
|
225
|
+
ancestorsA.add(current);
|
|
226
|
+
current = this.#parents.get(current) ?? null;
|
|
227
|
+
}
|
|
228
|
+
if (ancestorsA.size === 0) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
current = this.#parents.get(idB) ?? null;
|
|
232
|
+
while (current !== null) {
|
|
233
|
+
if (ancestorsA.has(current)) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
current = this.#parents.get(current) ?? null;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get the metadata object for a node.
|
|
243
|
+
* @param {string} id
|
|
244
|
+
* @returns {NodeMeta|null} The metadata, or null if the node doesn't exist
|
|
245
|
+
*/
|
|
246
|
+
getMeta(id) {
|
|
247
|
+
return this.#meta.get(id) ?? null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Update (shallow-merge) metadata for an existing node.
|
|
252
|
+
* @param {string} id
|
|
253
|
+
* @param {Partial<NodeMeta>} meta
|
|
254
|
+
* @returns {this}
|
|
255
|
+
*/
|
|
256
|
+
setMeta(id, meta) {
|
|
257
|
+
const existing = this.#meta.get(id);
|
|
258
|
+
if (!existing) {
|
|
259
|
+
throw new Error(`Node "${id}" does not exist`);
|
|
260
|
+
}
|
|
261
|
+
Object.assign(existing, meta);
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if `candidateId` is an ancestor of `nodeId`
|
|
267
|
+
* (i.e. reachable by walking up the single-parent chain).
|
|
268
|
+
* O(depth) — walks the parent chain.
|
|
269
|
+
* @param {string} nodeId
|
|
270
|
+
* @param {string} candidateId
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
hasAncestor(nodeId, candidateId) {
|
|
274
|
+
let current = this.#parents.get(nodeId) ?? null;
|
|
275
|
+
while (current !== null) {
|
|
276
|
+
if (current === candidateId) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
current = this.#parents.get(current) ?? null;
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if `candidateId` is a descendant of `nodeId`
|
|
286
|
+
* (i.e. reachable by walking down through children).
|
|
287
|
+
* Uses BFS.
|
|
288
|
+
* @param {string} nodeId
|
|
289
|
+
* @param {string} candidateId
|
|
290
|
+
* @returns {boolean}
|
|
291
|
+
*/
|
|
292
|
+
hasDescendant(nodeId, candidateId) {
|
|
293
|
+
const queue = [ nodeId ];
|
|
294
|
+
const visited = new Set();
|
|
295
|
+
while (queue.length > 0) {
|
|
296
|
+
const current = queue.shift();
|
|
297
|
+
if (current === candidateId && current !== nodeId) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
if (visited.has(current)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
visited.add(current);
|
|
304
|
+
const children = this.#children.get(current);
|
|
305
|
+
if (children) {
|
|
306
|
+
for (const childId of children) {
|
|
307
|
+
if (!visited.has(childId)) {
|
|
308
|
+
queue.push(childId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get all ancestor ids from the direct parent up to the root.
|
|
318
|
+
* Ordered nearest-first: [directParent, grandparent, …, root].
|
|
319
|
+
* @param {string} id
|
|
320
|
+
* @returns {string[]}
|
|
321
|
+
*/
|
|
322
|
+
getAncestors(id) {
|
|
323
|
+
const ancestors = [];
|
|
324
|
+
let current = this.#parents.get(id) ?? null;
|
|
325
|
+
while (current !== null) {
|
|
326
|
+
ancestors.push(current);
|
|
327
|
+
current = this.#parents.get(current) ?? null;
|
|
328
|
+
}
|
|
329
|
+
return ancestors;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get all descendant ids (transitive children) of a node.
|
|
334
|
+
* @param {string} id
|
|
335
|
+
* @returns {string[]}
|
|
336
|
+
*/
|
|
337
|
+
getDescendants(id) {
|
|
338
|
+
const descendants = [];
|
|
339
|
+
const queue = [ ...this.getChildren(id) ];
|
|
340
|
+
const visited = new Set();
|
|
341
|
+
while (queue.length > 0) {
|
|
342
|
+
const current = queue.shift();
|
|
343
|
+
if (visited.has(current)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
visited.add(current);
|
|
347
|
+
descendants.push(current);
|
|
348
|
+
const children = this.#children.get(current);
|
|
349
|
+
if (children) {
|
|
350
|
+
for (const childId of children) {
|
|
351
|
+
if (!visited.has(childId)) {
|
|
352
|
+
queue.push(childId);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return descendants;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get the root of the tree containing the given node
|
|
362
|
+
* (the furthest ancestor with no parent).
|
|
363
|
+
* @param {string} id
|
|
364
|
+
* @returns {string|null} The root id, or null if the node doesn't exist
|
|
365
|
+
*/
|
|
366
|
+
getRoot(id) {
|
|
367
|
+
if (!this.hasNode(id)) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
let current = id;
|
|
371
|
+
let parent = this.#parents.get(current);
|
|
372
|
+
while (parent !== null && parent !== undefined) {
|
|
373
|
+
current = parent;
|
|
374
|
+
parent = this.#parents.get(current);
|
|
375
|
+
}
|
|
376
|
+
return current;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get all root nodes (nodes with no parent).
|
|
381
|
+
* @returns {string[]}
|
|
382
|
+
*/
|
|
383
|
+
getRoots() {
|
|
384
|
+
const roots = [];
|
|
385
|
+
for (const [ id, parent ] of this.#parents) {
|
|
386
|
+
if (parent === null) {
|
|
387
|
+
roots.push(id);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return roots;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get all leaf nodes (nodes with no children).
|
|
395
|
+
* @returns {string[]}
|
|
396
|
+
*/
|
|
397
|
+
getLeaves() {
|
|
398
|
+
const leaves = [];
|
|
399
|
+
for (const [ id, children ] of this.#children) {
|
|
400
|
+
if (children.size === 0) {
|
|
401
|
+
leaves.push(id);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return leaves;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get the depth of a node (distance from its root).
|
|
409
|
+
* Root nodes have depth 0.
|
|
410
|
+
* @param {string} id
|
|
411
|
+
* @returns {number} The depth, or -1 if the node doesn't exist
|
|
412
|
+
*/
|
|
413
|
+
getDepth(id) {
|
|
414
|
+
if (!this.hasNode(id)) {
|
|
415
|
+
return -1;
|
|
416
|
+
}
|
|
417
|
+
let depth = 0;
|
|
418
|
+
let current = this.#parents.get(id);
|
|
419
|
+
while (current !== null && current !== undefined) {
|
|
420
|
+
depth++;
|
|
421
|
+
current = this.#parents.get(current);
|
|
422
|
+
}
|
|
423
|
+
return depth;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get all node ids in the graph.
|
|
428
|
+
* @returns {string[]}
|
|
429
|
+
*/
|
|
430
|
+
getNodes() {
|
|
431
|
+
return [ ...this.#children.keys() ];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get the total number of nodes.
|
|
436
|
+
* @returns {number}
|
|
437
|
+
*/
|
|
438
|
+
get size() {
|
|
439
|
+
return this.#children.size;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Remove all nodes and edges.
|
|
444
|
+
* @returns {this}
|
|
445
|
+
*/
|
|
446
|
+
clear() {
|
|
447
|
+
this.#children.clear();
|
|
448
|
+
this.#parents.clear();
|
|
449
|
+
this.#meta.clear();
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import DirectedGraph from './graph.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} NodeMeta
|
|
5
|
+
* @property {string} type – widget type name (e.g. '@apostrophecms/rich-text')
|
|
6
|
+
* @property {string} areaId – the _id of the area that contains this widget
|
|
7
|
+
* @property {boolean} [foreign] – true when the widget belongs to a different
|
|
8
|
+
* document than the graph owner (optional, low priority)
|
|
9
|
+
*/
|
|
10
|
+
export default class WidgetGraph extends DirectedGraph { }
|
|
@@ -4,6 +4,13 @@ const mkdirp = require('mkdirp');
|
|
|
4
4
|
const Promise = require('bluebird');
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
|
+
options: {
|
|
8
|
+
// When true, the default `uploadsUrl` omits `baseUrl`, producing a
|
|
9
|
+
// relative pathname for local storage drivers (e.g. `/uploads`).
|
|
10
|
+
// Useful when an external frontend like Astro proxies uploads and
|
|
11
|
+
// needs path-only attachment URLs.
|
|
12
|
+
relativeUrls: false
|
|
13
|
+
},
|
|
7
14
|
async init(self) {
|
|
8
15
|
self.uploadfs = await self.getInstance(self.options.uploadfs || {});
|
|
9
16
|
// Like @apostrophecms/express or @apostrophecms/db, this module has no
|
|
@@ -18,10 +25,17 @@ module.exports = {
|
|
|
18
25
|
// appropriate defaults and environment variables where not overridden by
|
|
19
26
|
// the given options object
|
|
20
27
|
async getInstance(options = {}) {
|
|
28
|
+
// Strip the URL if static build or explicitly requested.
|
|
29
|
+
const useRelativeUrls = self.apos.staticBaseUrl ||
|
|
30
|
+
self.options.relativeUrls === true;
|
|
31
|
+
const urlPrefix = useRelativeUrls
|
|
32
|
+
? ''
|
|
33
|
+
: (self.apos.baseUrl || '');
|
|
34
|
+
|
|
21
35
|
const uploadfsDefaultSettings = {
|
|
22
36
|
backend: 'local',
|
|
23
37
|
uploadsPath: self.apos.rootDir + '/public/uploads',
|
|
24
|
-
uploadsUrl:
|
|
38
|
+
uploadsUrl: urlPrefix + self.apos.prefix + '/uploads',
|
|
25
39
|
tempPath: self.apos.rootDir + '/data/temp/uploadfs'
|
|
26
40
|
};
|
|
27
41
|
const uploadfsSettings = {};
|