apostrophe 3.53.0 → 3.55.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/CHANGELOG.md +58 -1
- package/defaults.js +1 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +5 -2
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +28 -19
- package/modules/@apostrophecms/any-doc-type/index.js +2 -2
- package/modules/@apostrophecms/any-page-type/index.js +2 -2
- package/modules/@apostrophecms/doc/index.js +55 -29
- package/modules/@apostrophecms/doc-type/index.js +11 -6
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +4 -440
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +445 -0
- package/modules/@apostrophecms/i18n/i18n/de.json +113 -105
- package/modules/@apostrophecms/i18n/i18n/es.json +10 -0
- package/modules/@apostrophecms/i18n/i18n/fr.json +8 -0
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +10 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +8 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +1 -0
- package/modules/@apostrophecms/log/index.js +429 -0
- package/modules/@apostrophecms/login/index.js +47 -4
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +14 -1
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +1 -1
- package/modules/@apostrophecms/module/index.js +32 -6
- package/modules/@apostrophecms/module/lib/log.js +68 -0
- package/modules/@apostrophecms/page/index.js +71 -19
- package/modules/@apostrophecms/page/lib/legacy-migrations.js +0 -57
- package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +8 -285
- package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +291 -0
- package/modules/@apostrophecms/page-type/index.js +39 -26
- package/modules/@apostrophecms/piece-type/index.js +19 -11
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +2 -357
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +2 -86
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +2 -254
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +2 -77
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputBoolean.vue +2 -44
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +2 -64
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +2 -94
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +3 -47
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -82
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +2 -37
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +2 -26
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -57
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +2 -259
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +2 -38
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +2 -275
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +2 -167
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +2 -115
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +3 -279
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +2 -83
- package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +10 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +361 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +89 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +257 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputAttachment.js +81 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputBoolean.js +48 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +68 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputColor.js +98 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js +49 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +86 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js +41 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +29 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRange.js +60 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +262 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSelect.js +41 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +278 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +170 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +118 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +281 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSearchList.js +85 -0
- package/modules/@apostrophecms/template/index.js +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -2
- package/modules/@apostrophecms/util/index.js +83 -13
- package/modules/@apostrophecms/util/lib/logger.js +19 -17
- package/package.json +1 -1
- package/test/docs.js +35 -2
- package/test/log.js +1765 -0
- package/test/pages.js +57 -0
- package/test-lib/util.js +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Per module log handlers.
|
|
2
|
+
// Usage (same arguments for all log handlers):
|
|
3
|
+
// ```js
|
|
4
|
+
// self.logError('event-type');
|
|
5
|
+
// self.logError('event-type', { key: 'value' });
|
|
6
|
+
// self.logError('event-type', 'some message');
|
|
7
|
+
// self.logError('event-type', 'some message', { key: 'value' });
|
|
8
|
+
// Prepend `req` followed by any of the above argument variations.
|
|
9
|
+
// self.logError(req, ...);
|
|
10
|
+
// ```
|
|
11
|
+
//
|
|
12
|
+
// Event type is required and can be any string.
|
|
13
|
+
// If `req` is provided, the `data` object argument will be enriched with
|
|
14
|
+
// additional information from the request.
|
|
15
|
+
// Example:
|
|
16
|
+
// self.logError('event-type', 'some message', { key: 'value' });
|
|
17
|
+
// will log:
|
|
18
|
+
// 'current-module-name: event-type: some message',
|
|
19
|
+
// {
|
|
20
|
+
// type: 'event-type',
|
|
21
|
+
// severity: 'error',
|
|
22
|
+
// module: 'current-module-name',
|
|
23
|
+
// key: 'value',
|
|
24
|
+
// }
|
|
25
|
+
// If the option `messageAs` of `@apostrophecms/log` is set to 'msg',
|
|
26
|
+
// the result of the above log entry will be:
|
|
27
|
+
// {
|
|
28
|
+
// type: 'event-type',
|
|
29
|
+
// severity: 'error',
|
|
30
|
+
// module: 'current-module-name',
|
|
31
|
+
// key: 'value',
|
|
32
|
+
// msg: 'current-module-name: event-type: some message',
|
|
33
|
+
// }
|
|
34
|
+
//
|
|
35
|
+
// If `filter` option is set, the log entry will be logged only if the
|
|
36
|
+
// `severity` or `eventType` match any filter. For more information about
|
|
37
|
+
// filters see `@apostrophecms/log` module.
|
|
38
|
+
module.exports = function (self) {
|
|
39
|
+
const exception = new Error(
|
|
40
|
+
`Structured logging is not available for module "${self.__meta.name}".`
|
|
41
|
+
);
|
|
42
|
+
return {
|
|
43
|
+
logDebug(...args) {
|
|
44
|
+
if (!self.__structuredLoggingEnabled) {
|
|
45
|
+
throw exception;
|
|
46
|
+
}
|
|
47
|
+
self.apos.structuredLog.logEntry(self, 'debug', ...args);
|
|
48
|
+
},
|
|
49
|
+
logInfo(...args) {
|
|
50
|
+
if (!self.__structuredLoggingEnabled) {
|
|
51
|
+
throw exception;
|
|
52
|
+
}
|
|
53
|
+
self.apos.structuredLog.logEntry(self, 'info', ...args);
|
|
54
|
+
},
|
|
55
|
+
logWarn(...args) {
|
|
56
|
+
if (!self.__structuredLoggingEnabled) {
|
|
57
|
+
throw exception;
|
|
58
|
+
}
|
|
59
|
+
self.apos.structuredLog.logEntry(self, 'warn', ...args);
|
|
60
|
+
},
|
|
61
|
+
logError(...args) {
|
|
62
|
+
if (!self.__structuredLoggingEnabled) {
|
|
63
|
+
throw exception;
|
|
64
|
+
}
|
|
65
|
+
self.apos.structuredLog.logEntry(self, 'error', ...args);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -5,7 +5,7 @@ const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
|
|
|
5
5
|
const expressCacheOnDemand = require('express-cache-on-demand')();
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
|
-
cascades: [ 'batchOperations' ],
|
|
8
|
+
cascades: [ 'batchOperations', 'utilityOperations' ],
|
|
9
9
|
options: {
|
|
10
10
|
alias: 'page',
|
|
11
11
|
types: [
|
|
@@ -127,7 +127,7 @@ module.exports = {
|
|
|
127
127
|
getAll: [
|
|
128
128
|
...self.enableCacheOnDemand ? [ expressCacheOnDemand ] : [],
|
|
129
129
|
async (req) => {
|
|
130
|
-
self.
|
|
130
|
+
await self.publicApiCheckAsync(req);
|
|
131
131
|
const all = self.apos.launder.boolean(req.query.all);
|
|
132
132
|
const archived = self.apos.launder.booleanOrNull(req.query.archived);
|
|
133
133
|
const flat = self.apos.launder.boolean(req.query.flat);
|
|
@@ -215,7 +215,7 @@ module.exports = {
|
|
|
215
215
|
async (req, _id) => {
|
|
216
216
|
_id = self.inferIdLocaleAndMode(req, _id);
|
|
217
217
|
// Edit access to draft is sufficient to fetch either
|
|
218
|
-
self.
|
|
218
|
+
await self.publicApiCheckAsync(req);
|
|
219
219
|
const criteria = self.getIdCriteria(_id);
|
|
220
220
|
const result = await self.getRestQuery(req).permission(false).and(criteria).toObject();
|
|
221
221
|
|
|
@@ -253,7 +253,7 @@ module.exports = {
|
|
|
253
253
|
//
|
|
254
254
|
// This call is atomic with respect to other REST write operations on pages.
|
|
255
255
|
async post(req) {
|
|
256
|
-
self.
|
|
256
|
+
await self.publicApiCheckAsync(req);
|
|
257
257
|
let targetId = self.apos.launder.string(req.body._targetId);
|
|
258
258
|
let position = self.apos.launder.string(req.body._position || 'lastChild');
|
|
259
259
|
// Here we have to normalize before calling insert because we
|
|
@@ -338,7 +338,7 @@ module.exports = {
|
|
|
338
338
|
|
|
339
339
|
async put(req, _id) {
|
|
340
340
|
_id = self.inferIdLocaleAndMode(req, _id);
|
|
341
|
-
self.
|
|
341
|
+
await self.publicApiCheckAsync(req);
|
|
342
342
|
|
|
343
343
|
return self.withLock(req, async () => {
|
|
344
344
|
const page = await self.findForEditing(req, { _id }).toObject();
|
|
@@ -385,7 +385,7 @@ module.exports = {
|
|
|
385
385
|
},
|
|
386
386
|
async delete(req, _id) {
|
|
387
387
|
_id = self.inferIdLocaleAndMode(req, _id);
|
|
388
|
-
self.
|
|
388
|
+
await self.publicApiCheckAsync(req);
|
|
389
389
|
const page = await self.findOneForEditing(req, {
|
|
390
390
|
_id
|
|
391
391
|
});
|
|
@@ -396,9 +396,9 @@ module.exports = {
|
|
|
396
396
|
// You may pass `_targetId` and `_position` to move the page within the tree. `_position`
|
|
397
397
|
// may be `before`, `after` or `inside`. To move a page into or out of the archive, set
|
|
398
398
|
// `archived` to `true` or `false`.
|
|
399
|
-
patch(req, _id) {
|
|
399
|
+
async patch(req, _id) {
|
|
400
400
|
_id = self.inferIdLocaleAndMode(req, _id);
|
|
401
|
-
self.
|
|
401
|
+
await self.publicApiCheckAsync(req);
|
|
402
402
|
return self.patch(req, _id);
|
|
403
403
|
}
|
|
404
404
|
};
|
|
@@ -659,6 +659,13 @@ database.`);
|
|
|
659
659
|
};
|
|
660
660
|
}
|
|
661
661
|
}
|
|
662
|
+
},
|
|
663
|
+
composeUtilityOperations() {
|
|
664
|
+
self.utilityOperations = Object.entries(self.utilityOperations || {})
|
|
665
|
+
.map(([ action, properties ]) => ({
|
|
666
|
+
action,
|
|
667
|
+
...properties
|
|
668
|
+
}));
|
|
662
669
|
}
|
|
663
670
|
},
|
|
664
671
|
'apostrophe:ready': {
|
|
@@ -830,6 +837,7 @@ database.`);
|
|
|
830
837
|
browserOptions.localized &&
|
|
831
838
|
Object.keys(self.apos.i18n.locales).length > 1 &&
|
|
832
839
|
Object.values(self.apos.i18n.locales).some(locale => locale._edit);
|
|
840
|
+
browserOptions.utilityOperations = self.utilityOperations;
|
|
833
841
|
return browserOptions;
|
|
834
842
|
},
|
|
835
843
|
// Returns a query that finds pages the current user can edit
|
|
@@ -864,8 +872,6 @@ database.`);
|
|
|
864
872
|
position = normalized.position;
|
|
865
873
|
return self.withLock(req, async () => {
|
|
866
874
|
let peers;
|
|
867
|
-
page.aposLastTargetId = targetId;
|
|
868
|
-
page.aposLastPosition = position;
|
|
869
875
|
const target = await self.getTarget(req, targetId, position);
|
|
870
876
|
if (!target) {
|
|
871
877
|
throw self.apos.error('notfound');
|
|
@@ -1203,8 +1209,6 @@ database.`);
|
|
|
1203
1209
|
}
|
|
1204
1210
|
moved.level = level;
|
|
1205
1211
|
moved.rank = rank;
|
|
1206
|
-
moved.aposLastTargetId = targetId;
|
|
1207
|
-
moved.aposLastPosition = position;
|
|
1208
1212
|
// Are we in the archive? Our new parent reveals that
|
|
1209
1213
|
if (parent.archived) {
|
|
1210
1214
|
moved.archived = true;
|
|
@@ -1222,8 +1226,11 @@ database.`);
|
|
|
1222
1226
|
// value. `position` is used to prevent attempts to move after the archive
|
|
1223
1227
|
// "page."
|
|
1224
1228
|
async getTarget(req, targetId, position) {
|
|
1225
|
-
const criteria = self.getIdCriteria(targetId);
|
|
1226
|
-
|
|
1229
|
+
const criteria = self.getIdCriteria(self.inferIdLocaleAndMode(req, targetId));
|
|
1230
|
+
// Use findForEditing to ensure we get improvements to that method from
|
|
1231
|
+
// npm modules that make the query more inclusive. Then explicitly shut off
|
|
1232
|
+
// things we know we don't want to be blocked by
|
|
1233
|
+
const target = await self.findForEditing(req, criteria).permission(false).archived(null).areas(false).ancestors({
|
|
1227
1234
|
depth: 1,
|
|
1228
1235
|
archived: null,
|
|
1229
1236
|
orphan: null,
|
|
@@ -2053,9 +2060,7 @@ database.`);
|
|
|
2053
2060
|
page.rank = rank;
|
|
2054
2061
|
const $set = {
|
|
2055
2062
|
path: page.path,
|
|
2056
|
-
rank: page.rank
|
|
2057
|
-
aposLastTargetId: home.aposDocId,
|
|
2058
|
-
aposLastPosition: 'lastChild'
|
|
2063
|
+
rank: page.rank
|
|
2059
2064
|
};
|
|
2060
2065
|
if (argv['new-slug']) {
|
|
2061
2066
|
$set.slug = argv['new-slug'];
|
|
@@ -2275,7 +2280,10 @@ database.`);
|
|
|
2275
2280
|
return self.apos.doc.getManager(page.type)
|
|
2276
2281
|
.allowedSchema(req, page, parentPage);
|
|
2277
2282
|
},
|
|
2278
|
-
|
|
2283
|
+
// Can be extended on a project level with `_super(req, true)` to disable
|
|
2284
|
+
// permission check and public API projection. You shouldn't do this
|
|
2285
|
+
// if you're not sure what you're doing.
|
|
2286
|
+
getRestQuery(req, omitPermissionCheck = false) {
|
|
2279
2287
|
const query = self.find(req)
|
|
2280
2288
|
.ancestors(true)
|
|
2281
2289
|
.children(true)
|
|
@@ -2283,7 +2291,7 @@ database.`);
|
|
|
2283
2291
|
.applyBuildersSafely(req.query);
|
|
2284
2292
|
// Minimum standard for a REST query without a public projection
|
|
2285
2293
|
// is being allowed to view drafts on the site
|
|
2286
|
-
if (!self.canAccessApi(req)) {
|
|
2294
|
+
if (!omitPermissionCheck && !self.canAccessApi(req)) {
|
|
2287
2295
|
if (!self.options.publicApiProjection) {
|
|
2288
2296
|
// Shouldn't be needed thanks to publicApiCheck, but be sure
|
|
2289
2297
|
query.and({
|
|
@@ -2329,6 +2337,11 @@ database.`);
|
|
|
2329
2337
|
}
|
|
2330
2338
|
}
|
|
2331
2339
|
},
|
|
2340
|
+
// An async version of the above. It can be overridden to implement
|
|
2341
|
+
// an asynchronous check of the public API permissions.
|
|
2342
|
+
async publicApiCheckAsync(req) {
|
|
2343
|
+
return self.publicApiCheck(req);
|
|
2344
|
+
},
|
|
2332
2345
|
getAllProjection() {
|
|
2333
2346
|
return {
|
|
2334
2347
|
_url: 1,
|
|
@@ -2489,6 +2502,45 @@ database.`);
|
|
|
2489
2502
|
await self.apos.attachment.recomputeAllDocReferences();
|
|
2490
2503
|
}
|
|
2491
2504
|
});
|
|
2505
|
+
},
|
|
2506
|
+
async inferLastTargetIdAndPosition(doc) {
|
|
2507
|
+
const parentPath = self.getParentPath(doc);
|
|
2508
|
+
const parentAposDocId = parentPath.split('/').pop();
|
|
2509
|
+
const parentId = doc.aposLocale
|
|
2510
|
+
? `${parentAposDocId}:${doc.aposLocale}`
|
|
2511
|
+
: parentAposDocId;
|
|
2512
|
+
const peerCriteria = {
|
|
2513
|
+
path: self.matchDescendants(parentPath),
|
|
2514
|
+
level: doc.level
|
|
2515
|
+
};
|
|
2516
|
+
if (doc.aposLocale) {
|
|
2517
|
+
peerCriteria.aposLocale = doc.aposLocale;
|
|
2518
|
+
}
|
|
2519
|
+
const peers = await self.apos.doc.db.find(peerCriteria).sort({
|
|
2520
|
+
rank: 1
|
|
2521
|
+
}).project({
|
|
2522
|
+
_id: 1
|
|
2523
|
+
}).toArray();
|
|
2524
|
+
let targetId;
|
|
2525
|
+
let position;
|
|
2526
|
+
const index = peers.findIndex(peer => peer._id === doc._id);
|
|
2527
|
+
if (index === -1) {
|
|
2528
|
+
throw new Error('Cannot find page among its peers');
|
|
2529
|
+
}
|
|
2530
|
+
if (index === 0) {
|
|
2531
|
+
targetId = parentId;
|
|
2532
|
+
position = 'firstChild';
|
|
2533
|
+
} else if (index === (peers.length - 1)) {
|
|
2534
|
+
targetId = parentId;
|
|
2535
|
+
position = 'lastChild';
|
|
2536
|
+
} else {
|
|
2537
|
+
targetId = peers[index - 1]._id;
|
|
2538
|
+
position = 'after';
|
|
2539
|
+
}
|
|
2540
|
+
return {
|
|
2541
|
+
lastTargetId: targetId,
|
|
2542
|
+
lastPosition: position
|
|
2543
|
+
};
|
|
2492
2544
|
}
|
|
2493
2545
|
};
|
|
2494
2546
|
},
|
|
@@ -6,7 +6,6 @@ module.exports = (self) => {
|
|
|
6
6
|
addLegacyMigrations() {
|
|
7
7
|
self.addDeduplicateRanksMigration();
|
|
8
8
|
self.addFixHomePagePathMigration();
|
|
9
|
-
self.addMissingLastTargetIdAndPositionMigration();
|
|
10
9
|
self.addArchivedMigration();
|
|
11
10
|
},
|
|
12
11
|
addArchivedMigration() {
|
|
@@ -100,62 +99,6 @@ module.exports = (self) => {
|
|
|
100
99
|
}
|
|
101
100
|
});
|
|
102
101
|
});
|
|
103
|
-
},
|
|
104
|
-
addMissingLastTargetIdAndPositionMigration() {
|
|
105
|
-
self.apos.migration.add('missing-last-target-id-and-position', async () => {
|
|
106
|
-
await self.apos.migration.eachDoc({
|
|
107
|
-
slug: /^\//,
|
|
108
|
-
// Home page should not have them, that's OK
|
|
109
|
-
level: {
|
|
110
|
-
$gte: 0
|
|
111
|
-
},
|
|
112
|
-
aposLastTargetId: {
|
|
113
|
-
$exists: 0
|
|
114
|
-
}
|
|
115
|
-
}, 5, async (doc) => {
|
|
116
|
-
const parentPath = self.getParentPath(doc);
|
|
117
|
-
const parentAposDocId = parentPath.split('/').pop();
|
|
118
|
-
let parentId;
|
|
119
|
-
if (doc.aposLocale) {
|
|
120
|
-
parentId = `${parentAposDocId}:${doc.aposLocale}`;
|
|
121
|
-
} else {
|
|
122
|
-
parentId = parentAposDocId;
|
|
123
|
-
}
|
|
124
|
-
const peerCriteria = {
|
|
125
|
-
path: self.matchDescendants(parentPath),
|
|
126
|
-
level: doc.level
|
|
127
|
-
};
|
|
128
|
-
if (doc.aposLocale) {
|
|
129
|
-
peerCriteria.aposLocale = doc.aposLocale;
|
|
130
|
-
}
|
|
131
|
-
const peers = await self.apos.doc.db.find(peerCriteria).sort({
|
|
132
|
-
rank: 1
|
|
133
|
-
}).project({
|
|
134
|
-
_id: 1
|
|
135
|
-
}).toArray();
|
|
136
|
-
let targetId;
|
|
137
|
-
let position;
|
|
138
|
-
const index = peers.findIndex(peer => peer._id === doc._id);
|
|
139
|
-
if (index === -1) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (index === 0) {
|
|
143
|
-
targetId = parentId;
|
|
144
|
-
position = 'firstChild';
|
|
145
|
-
} else {
|
|
146
|
-
targetId = peers[index - 1]._id;
|
|
147
|
-
position = 'after';
|
|
148
|
-
}
|
|
149
|
-
return self.apos.doc.db.updateOne({
|
|
150
|
-
_id: doc._id
|
|
151
|
-
}, {
|
|
152
|
-
$set: {
|
|
153
|
-
aposLastTargetId: targetId,
|
|
154
|
-
aposLastPosition: position
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
102
|
}
|
|
160
103
|
};
|
|
161
104
|
};
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
/>
|
|
21
21
|
</template>
|
|
22
22
|
<template #primaryControls>
|
|
23
|
+
<AposUtilityOperations
|
|
24
|
+
:module-options="moduleOptions"
|
|
25
|
+
:has-relationship-field="!!relationshipField"
|
|
26
|
+
/>
|
|
23
27
|
<AposContextMenu
|
|
24
28
|
v-if="relationshipField"
|
|
25
29
|
:menu="moreMenu"
|
|
@@ -86,294 +90,13 @@
|
|
|
86
90
|
</template>
|
|
87
91
|
|
|
88
92
|
<script>
|
|
89
|
-
import
|
|
90
|
-
import AposArchiveMixin from 'Modules/@apostrophecms/ui/mixins/AposArchiveMixin';
|
|
91
|
-
import AposPublishMixin from 'Modules/@apostrophecms/ui/mixins/AposPublishMixin';
|
|
92
|
-
import AposDocsManagerMixin from 'Modules/@apostrophecms/modal/mixins/AposDocsManagerMixin';
|
|
93
|
-
import { klona } from 'klona';
|
|
93
|
+
import AposPagesManagerLogic from 'Modules/@apostrophecms/page/logic/AposPagesManager';
|
|
94
94
|
|
|
95
95
|
export default {
|
|
96
96
|
name: 'AposPagesManager',
|
|
97
|
-
mixins: [
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
moduleName: '@apostrophecms/page',
|
|
103
|
-
modal: {
|
|
104
|
-
active: false,
|
|
105
|
-
triggerFocusRefresh: 0,
|
|
106
|
-
type: 'slide',
|
|
107
|
-
showModal: false,
|
|
108
|
-
width: 'two-thirds'
|
|
109
|
-
},
|
|
110
|
-
pages: [],
|
|
111
|
-
pagesFlat: [],
|
|
112
|
-
options: {
|
|
113
|
-
columns: [
|
|
114
|
-
{
|
|
115
|
-
columnHeader: 'apostrophe:pageTitle',
|
|
116
|
-
property: 'title',
|
|
117
|
-
cellValue: 'title'
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
name: 'labels',
|
|
121
|
-
component: 'AposCellLabels'
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
columnHeader: 'apostrophe:lastEdited',
|
|
125
|
-
property: 'updatedAt',
|
|
126
|
-
component: 'AposCellLastEdited',
|
|
127
|
-
cellValue: 'updatedAt'
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
property: 'contextMenu',
|
|
131
|
-
component: 'AposCellContextMenu'
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
treeOptions: {
|
|
136
|
-
bulkSelect: !!this.relationshipField,
|
|
137
|
-
draggable: true,
|
|
138
|
-
ghostUnpublished: true
|
|
139
|
-
},
|
|
140
|
-
moreMenu: [
|
|
141
|
-
{
|
|
142
|
-
label: 'New Page',
|
|
143
|
-
action: 'new'
|
|
144
|
-
}
|
|
145
|
-
],
|
|
146
|
-
moreMenuButton: {
|
|
147
|
-
tooltip: {
|
|
148
|
-
content: 'More Options',
|
|
149
|
-
placement: 'bottom'
|
|
150
|
-
},
|
|
151
|
-
label: 'More Options',
|
|
152
|
-
icon: 'dots-vertical-icon',
|
|
153
|
-
iconOnly: true,
|
|
154
|
-
type: 'subtle',
|
|
155
|
-
modifiers: [ 'small', 'no-motion' ]
|
|
156
|
-
},
|
|
157
|
-
pageSetMenuSelection: 'live'
|
|
158
|
-
};
|
|
159
|
-
},
|
|
160
|
-
computed: {
|
|
161
|
-
moduleOptions() {
|
|
162
|
-
return apos.page;
|
|
163
|
-
},
|
|
164
|
-
items() {
|
|
165
|
-
if (!this.pages || !this.headers.length) {
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
return klona(this.pages);
|
|
169
|
-
},
|
|
170
|
-
selectAllChoice() {
|
|
171
|
-
const checkLen = this.checked.length;
|
|
172
|
-
const rowLen = this.pagesFlat.length;
|
|
173
|
-
|
|
174
|
-
return checkLen > 0 && checkLen !== rowLen ? {
|
|
175
|
-
value: 'checked',
|
|
176
|
-
indeterminate: true
|
|
177
|
-
} : {
|
|
178
|
-
value: 'checked'
|
|
179
|
-
};
|
|
180
|
-
},
|
|
181
|
-
saveRelationshipLabel() {
|
|
182
|
-
if (this.relationshipField && (this.relationshipField.max === 1)) {
|
|
183
|
-
return 'Select Page';
|
|
184
|
-
} else {
|
|
185
|
-
return 'Select Pages';
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
headers() {
|
|
189
|
-
let headers = this.options.columns || [];
|
|
190
|
-
if (!this.pageSetMenuSelectionIsLive) {
|
|
191
|
-
headers = headers.filter(h => h.component !== 'AposCellLabels');
|
|
192
|
-
}
|
|
193
|
-
return headers;
|
|
194
|
-
},
|
|
195
|
-
pageSetMenu() {
|
|
196
|
-
return [ {
|
|
197
|
-
label: 'apostrophe:live',
|
|
198
|
-
action: 'live',
|
|
199
|
-
modifiers: this.pageSetMenuSelectionIsLive ? [ 'selected', 'disabled' ] : []
|
|
200
|
-
}, {
|
|
201
|
-
label: 'apostrophe:archived',
|
|
202
|
-
action: 'archive',
|
|
203
|
-
modifiers: !this.pageSetMenuSelectionIsLive ? [ 'selected', 'disabled' ] : []
|
|
204
|
-
} ];
|
|
205
|
-
},
|
|
206
|
-
pageSetMenuButton() {
|
|
207
|
-
const button = {
|
|
208
|
-
label: this.pageSetMenuSelectionIsLive ? 'apostrophe:live' : 'apostrophe:archived',
|
|
209
|
-
icon: 'chevron-down-icon',
|
|
210
|
-
modifiers: [ 'no-motion', 'outline', 'icon-right' ],
|
|
211
|
-
class: 'apos-pages-manager__page-set-menu-button'
|
|
212
|
-
};
|
|
213
|
-
return button;
|
|
214
|
-
},
|
|
215
|
-
pageSetMenuSelectionIsLive() {
|
|
216
|
-
return this.pageSetMenuSelection === 'live';
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
watch: {
|
|
220
|
-
async pageSetMenuSelection() {
|
|
221
|
-
await this.getPages();
|
|
222
|
-
}
|
|
223
|
-
},
|
|
224
|
-
async mounted() {
|
|
225
|
-
// Get the data. This will be more complex in actuality.
|
|
226
|
-
this.modal.active = true;
|
|
227
|
-
await this.getPages();
|
|
228
|
-
this.modal.triggerFocusRefresh++;
|
|
229
|
-
|
|
230
|
-
apos.bus.$on('content-changed', this.getPages);
|
|
231
|
-
apos.bus.$on('command-menu-manager-create-new', this.create);
|
|
232
|
-
apos.bus.$on('command-menu-manager-close', this.confirmAndCancel);
|
|
233
|
-
},
|
|
234
|
-
destroyed() {
|
|
235
|
-
apos.bus.$off('content-changed', this.getPages);
|
|
236
|
-
apos.bus.$off('command-menu-manager-create-new', this.create);
|
|
237
|
-
apos.bus.$off('command-menu-manager-close', this.confirmAndCancel);
|
|
238
|
-
},
|
|
239
|
-
methods: {
|
|
240
|
-
moreMenuHandler(action) {
|
|
241
|
-
if (action === 'new') {
|
|
242
|
-
this.create();
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
async getPages () {
|
|
246
|
-
const self = this;
|
|
247
|
-
if (this.gettingPages) {
|
|
248
|
-
// Avoid race conditions by trying again later if already in progress
|
|
249
|
-
setTimeout(this.getPages, 100);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
// Not reactive, so not in data()
|
|
253
|
-
this.gettingPages = true;
|
|
254
|
-
try {
|
|
255
|
-
this.pages = [];
|
|
256
|
-
this.pagesFlat = [];
|
|
257
|
-
|
|
258
|
-
let pageTree = (await apos.http.get(
|
|
259
|
-
'/api/v1/@apostrophecms/page', {
|
|
260
|
-
busy: true,
|
|
261
|
-
qs: {
|
|
262
|
-
all: '1',
|
|
263
|
-
archived: this.relationshipField || this.pageSetMenuSelectionIsLive ? '0' : 'any',
|
|
264
|
-
// Also fetch published docs as _publishedDoc subproperties
|
|
265
|
-
withPublished: 1
|
|
266
|
-
},
|
|
267
|
-
draft: true
|
|
268
|
-
}
|
|
269
|
-
));
|
|
270
|
-
|
|
271
|
-
// If editor is looking at the archive tree, trim the normal page tree response
|
|
272
|
-
if (this.pageSetMenuSelection === 'archive') {
|
|
273
|
-
pageTree = pageTree._children.find(page => page.slug === '/archive');
|
|
274
|
-
pageTree = pageTree._children;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
formatPage(pageTree);
|
|
278
|
-
|
|
279
|
-
if (!pageTree.length && pageTree.length !== 0) {
|
|
280
|
-
pageTree = [ pageTree ];
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
this.pages = [ ...pageTree ];
|
|
284
|
-
|
|
285
|
-
} finally {
|
|
286
|
-
this.gettingPages = false;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function formatPage(page) {
|
|
290
|
-
if (page.length) {
|
|
291
|
-
page.forEach(formatPage);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
self.pagesFlat.push(klona(page));
|
|
296
|
-
|
|
297
|
-
if (Array.isArray(page._children)) {
|
|
298
|
-
page._children.forEach(formatPage);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
async update(page) {
|
|
303
|
-
const body = {
|
|
304
|
-
_targetId: page.endContext,
|
|
305
|
-
_position: page.endIndex
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const route = `${this.moduleOptions.action}/${page.changedId}`;
|
|
309
|
-
try {
|
|
310
|
-
await apos.http.patch(route, {
|
|
311
|
-
busy: true,
|
|
312
|
-
body,
|
|
313
|
-
draft: true
|
|
314
|
-
});
|
|
315
|
-
} catch (error) {
|
|
316
|
-
await apos.notify(error.body.message || this.$t('apostrophe:treeError'), {
|
|
317
|
-
type: 'danger',
|
|
318
|
-
icon: 'alert-circle-icon',
|
|
319
|
-
dismiss: true,
|
|
320
|
-
localize: false
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
await this.getPages();
|
|
325
|
-
if (this.pagesFlat.find(page => {
|
|
326
|
-
return (page.aposDocId === (window.apos.page.page && window.apos.page.page.aposDocId)) && page.archived;
|
|
327
|
-
})) {
|
|
328
|
-
// With the current page gone, we need to move to safe ground
|
|
329
|
-
location.assign(`${window.apos.prefix}/`);
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
toggleRowCheck(id) {
|
|
333
|
-
if (this.checked.includes(id)) {
|
|
334
|
-
this.checked = this.checked.filter(item => item !== id);
|
|
335
|
-
} else {
|
|
336
|
-
this.checked.push(id);
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
selectAll(event) {
|
|
340
|
-
if (!this.checked.length) {
|
|
341
|
-
this.pagesFlat.forEach((row) => {
|
|
342
|
-
this.toggleRowCheck(row._id);
|
|
343
|
-
});
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (this.checked.length <= this.pagesFlat.length) {
|
|
348
|
-
this.checked.forEach((id) => {
|
|
349
|
-
this.toggleRowCheck(id);
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
},
|
|
353
|
-
async create() {
|
|
354
|
-
const doc = await apos.modal.execute(this.moduleOptions.components.editorModal, {
|
|
355
|
-
moduleName: this.moduleName
|
|
356
|
-
});
|
|
357
|
-
if (!doc) {
|
|
358
|
-
// Cancel clicked
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
await this.getPages();
|
|
362
|
-
if (this.relationshipField) {
|
|
363
|
-
doc._fields = doc._fields || {};
|
|
364
|
-
// Must push to checked docs or it will try to do it for us
|
|
365
|
-
// and not include _fields
|
|
366
|
-
this.checkedDocs.push(doc);
|
|
367
|
-
this.checked.push(doc._id);
|
|
368
|
-
}
|
|
369
|
-
},
|
|
370
|
-
setCheckedDocs(checkedDocs) {
|
|
371
|
-
this.checked = checkedDocs.map(doc => doc._id);
|
|
372
|
-
},
|
|
373
|
-
updateCheckedDocs() {
|
|
374
|
-
this.checkedDocs = this.checked.map(_id => this.pagesFlat.find(page => page._id === _id));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
97
|
+
mixins: [ AposPagesManagerLogic ],
|
|
98
|
+
// Keep it for linting
|
|
99
|
+
emits: [ 'archive', 'search', 'safe-close', 'modal-result' ]
|
|
377
100
|
};
|
|
378
101
|
</script>
|
|
379
102
|
|