apostrophe 4.28.0 → 4.29.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 +33 -3
- package/README.md +142 -0
- package/defaults.js +1 -0
- package/lib/safe-json-script.js +27 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +1 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +3 -5
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +13 -1
- package/modules/@apostrophecms/asset/lib/globalIcons.js +3 -0
- package/modules/@apostrophecms/attachment/index.js +43 -1
- package/modules/@apostrophecms/color-field/index.js +7 -1
- package/modules/@apostrophecms/doc/index.js +11 -1
- package/modules/@apostrophecms/doc-type/index.js +165 -32
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +1 -1
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +104 -59
- package/modules/@apostrophecms/file/index.js +109 -9
- package/modules/@apostrophecms/i18n/i18n/de.json +0 -2
- package/modules/@apostrophecms/i18n/i18n/en.json +40 -1
- package/modules/@apostrophecms/i18n/i18n/es.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/fr.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/it.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
- package/modules/@apostrophecms/i18n/i18n/sk.json +0 -1
- package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nBatchReporting.js +18 -1
- package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nLocalizeActions.js +50 -0
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +56 -13
- package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +8 -2
- package/modules/@apostrophecms/layout-column-widget/index.js +156 -163
- package/modules/@apostrophecms/layout-widget/index.js +7 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +6 -11
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +3 -5
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +4 -4
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -16
- package/modules/@apostrophecms/layout-widget/ui/apos/lib/grid-state.mjs +7 -27
- package/modules/@apostrophecms/layout-widget/views/column.html +7 -9
- package/modules/@apostrophecms/login/index.js +39 -40
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +17 -2
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +3 -2
- package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +1 -0
- package/modules/@apostrophecms/page/index.js +2 -0
- package/modules/@apostrophecms/piece-type/index.js +3 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +5 -0
- package/modules/@apostrophecms/recently-edited/index.js +831 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +54 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedCombo.vue +454 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilterTag.vue +75 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilters.vue +287 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +16 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedManager.vue +346 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedBatch.js +193 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedData.js +276 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFetch.js +199 -0
- package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFilters.js +100 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +8 -4
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +1 -1
- package/modules/@apostrophecms/styles/index.js +10 -0
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +6 -0
- package/modules/@apostrophecms/styles/lib/handlers.js +5 -0
- package/modules/@apostrophecms/styles/lib/methods.js +9 -3
- package/modules/@apostrophecms/styles/lib/presets.js +119 -0
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +3 -8
- package/modules/@apostrophecms/styles/ui/apos/composables/AposStyles.js +1 -3
- package/modules/@apostrophecms/styles/ui/apos/render-factory.js +29 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/backgroundHelpers.mjs +140 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/customRules.mjs +105 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +195 -15
- package/modules/@apostrophecms/template/index.js +22 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +18 -4
- package/modules/@apostrophecms/ui/ui/apos/composables/useInfiniteScroll.js +91 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +5 -2
- package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
- package/modules/@apostrophecms/url/index.js +38 -4
- package/modules/@apostrophecms/widget-type/index.js +22 -6
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +8 -4
- package/package.json +19 -19
- package/test/files.js +129 -0
- package/test/layout-widget-migration.js +719 -0
- package/test/login-requirements.js +1 -1
- package/test/pieces-public-api.js +80 -0
- package/test/pieces.js +25 -0
- package/test/recently-edited.js +2311 -0
- package/test/schemas.js +39 -3
- package/test/static-build.js +642 -0
- package/test/styles.js +2569 -0
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposLayoutColControlDialog.vue +0 -171
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
// A virtual "piece type" that provides a cross-type manager
|
|
2
|
+
// for recently edited documents. It doesn't own documents but
|
|
3
|
+
// queries across all managed doc types. Write operations are
|
|
4
|
+
// delegated to each document's actual type manager.
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
extend: '@apostrophecms/piece-type',
|
|
8
|
+
options: {
|
|
9
|
+
alias: 'recentlyEdited',
|
|
10
|
+
label: 'apostrophe:recentlyEdited',
|
|
11
|
+
pluralLabel: 'apostrophe:recentlyEditedDocuments',
|
|
12
|
+
quickCreate: false,
|
|
13
|
+
showCreate: false,
|
|
14
|
+
showPermissions: false,
|
|
15
|
+
// 30-day window
|
|
16
|
+
recentDays: 30,
|
|
17
|
+
// Developer-configurable type exclusion
|
|
18
|
+
excludeTypes: [],
|
|
19
|
+
perPage: 50,
|
|
20
|
+
managerApiProjection: {
|
|
21
|
+
updatedAt: 1,
|
|
22
|
+
updatedBy: 1,
|
|
23
|
+
archived: 1,
|
|
24
|
+
modified: 1,
|
|
25
|
+
parked: 1,
|
|
26
|
+
lastPublishedAt: 1,
|
|
27
|
+
submitted: 1,
|
|
28
|
+
aposPermissions: 1
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
commands(self) {
|
|
32
|
+
return {
|
|
33
|
+
add: {
|
|
34
|
+
[`${self.__meta.name}:manager`]: {
|
|
35
|
+
type: 'item',
|
|
36
|
+
label: self.options.label,
|
|
37
|
+
action: {
|
|
38
|
+
type: 'admin-menu-click',
|
|
39
|
+
payload: {
|
|
40
|
+
itemName: `${self.__meta.name}:manager`
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
shortcut: 'T,R'
|
|
44
|
+
},
|
|
45
|
+
[`${self.__meta.name}:search`]: {
|
|
46
|
+
type: 'item',
|
|
47
|
+
label: 'apostrophe:commandMenuSearch',
|
|
48
|
+
action: {
|
|
49
|
+
type: 'command-menu-manager-focus-search'
|
|
50
|
+
},
|
|
51
|
+
shortcut: 'Ctrl+F Meta+F'
|
|
52
|
+
},
|
|
53
|
+
[`${self.__meta.name}:select-all`]: {
|
|
54
|
+
type: 'item',
|
|
55
|
+
label: 'apostrophe:commandMenuSelectAll',
|
|
56
|
+
action: {
|
|
57
|
+
type: 'command-menu-manager-select-all'
|
|
58
|
+
},
|
|
59
|
+
shortcut: 'Ctrl+Shift+A Meta+Shift+A'
|
|
60
|
+
},
|
|
61
|
+
[`${self.__meta.name}:exit-manager`]: {
|
|
62
|
+
type: 'item',
|
|
63
|
+
label: 'apostrophe:commandMenuExitManager',
|
|
64
|
+
action: {
|
|
65
|
+
type: 'command-menu-manager-close'
|
|
66
|
+
},
|
|
67
|
+
shortcut: 'Q'
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
remove: [
|
|
71
|
+
`${self.__meta.name}:create-new`,
|
|
72
|
+
`${self.__meta.name}:archive-selected`
|
|
73
|
+
],
|
|
74
|
+
modal: {
|
|
75
|
+
default: {
|
|
76
|
+
'@apostrophecms/command-menu:taskbar': {
|
|
77
|
+
label: 'apostrophe:commandMenuTaskbar',
|
|
78
|
+
commands: [
|
|
79
|
+
`${self.__meta.name}:manager`
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[`${self.__meta.name}:manager`]: {
|
|
84
|
+
'@apostrophecms/command-menu:manager': {
|
|
85
|
+
label: 'apostrophe:commandMenuManager',
|
|
86
|
+
commands: [
|
|
87
|
+
`${self.__meta.name}:search`,
|
|
88
|
+
`${self.__meta.name}:select-all`,
|
|
89
|
+
`${self.__meta.name}:exit-manager`
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
'@apostrophecms/command-menu:general': {
|
|
93
|
+
label: 'apostrophe:commandMenuGeneral',
|
|
94
|
+
commands: [
|
|
95
|
+
'@apostrophecms/command-menu:show-shortcut-list'
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
columns: {
|
|
103
|
+
add: {
|
|
104
|
+
title: {
|
|
105
|
+
label: 'apostrophe:title',
|
|
106
|
+
component: 'AposCellTitle'
|
|
107
|
+
},
|
|
108
|
+
type: {
|
|
109
|
+
label: 'apostrophe:type',
|
|
110
|
+
component: 'AposCellType'
|
|
111
|
+
},
|
|
112
|
+
_localeLabel: {
|
|
113
|
+
label: 'apostrophe:locale',
|
|
114
|
+
component: 'AposCellBasic'
|
|
115
|
+
},
|
|
116
|
+
_lastEditor: {
|
|
117
|
+
label: 'apostrophe:lastEditor',
|
|
118
|
+
component: 'AposCellBasic'
|
|
119
|
+
},
|
|
120
|
+
updatedAt: {
|
|
121
|
+
label: 'apostrophe:lastEdited',
|
|
122
|
+
component: 'AposCellLastEdited'
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
remove: [ 'labels' ],
|
|
126
|
+
order: [ 'title', 'type', '_localeLabel', '_lastEditor', 'updatedAt' ]
|
|
127
|
+
},
|
|
128
|
+
filters: {
|
|
129
|
+
add: {
|
|
130
|
+
_editedBy: {
|
|
131
|
+
label: 'apostrophe:recentlyEditedEditedBy'
|
|
132
|
+
},
|
|
133
|
+
_docType: {
|
|
134
|
+
label: 'apostrophe:type',
|
|
135
|
+
inputType: 'checkbox'
|
|
136
|
+
},
|
|
137
|
+
_action: {
|
|
138
|
+
label: 'apostrophe:recentlyEditedAction'
|
|
139
|
+
},
|
|
140
|
+
_locale: {
|
|
141
|
+
label: 'apostrophe:locale',
|
|
142
|
+
inputType: 'checkbox'
|
|
143
|
+
},
|
|
144
|
+
_status: {
|
|
145
|
+
label: 'apostrophe:recentlyEditedStatus'
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
remove: [ 'visibility', 'archived' ]
|
|
149
|
+
},
|
|
150
|
+
async init(self) {
|
|
151
|
+
self.filterChoiceRegistry = {
|
|
152
|
+
action: {},
|
|
153
|
+
status: {}
|
|
154
|
+
};
|
|
155
|
+
self.managedTypes = [];
|
|
156
|
+
self.managedTypeNames = [];
|
|
157
|
+
self.managedPageTypeNames = [];
|
|
158
|
+
self.managedPieceTypeNames = [];
|
|
159
|
+
|
|
160
|
+
self.addOwnFilterChoices();
|
|
161
|
+
await self.createIndexes();
|
|
162
|
+
},
|
|
163
|
+
handlers(self) {
|
|
164
|
+
return {
|
|
165
|
+
'apostrophe:modulesRegistered': {
|
|
166
|
+
detectManagedTypes() {
|
|
167
|
+
const internalExcludeTypes = [
|
|
168
|
+
self.__meta.name,
|
|
169
|
+
'@apostrophecms/submitted-draft',
|
|
170
|
+
'@apostrophecms/archive-page'
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const userExcludeTypes = self.options.excludeTypes || [];
|
|
174
|
+
const excludeSet = new Set([
|
|
175
|
+
...internalExcludeTypes,
|
|
176
|
+
...userExcludeTypes
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const managers = Object.values(self.apos.doc.managers);
|
|
180
|
+
self.managedTypes = managers
|
|
181
|
+
.filter(manager => {
|
|
182
|
+
if (!manager.__meta) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const name = manager.__meta.name;
|
|
186
|
+
if (excludeSet.has(name)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (!manager.isLocalized?.()) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
// Only concrete types: piece types and page types.
|
|
193
|
+
// Excludes abstract bases like any-doc-type,
|
|
194
|
+
// any-page-type, polymorphic-type.
|
|
195
|
+
const isPiece = self.apos.instanceOf(
|
|
196
|
+
manager, '@apostrophecms/piece-type'
|
|
197
|
+
);
|
|
198
|
+
const isPage = self.apos.instanceOf(
|
|
199
|
+
manager, '@apostrophecms/page-type'
|
|
200
|
+
);
|
|
201
|
+
if (!isPiece && !isPage) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
})
|
|
206
|
+
.map(manager => ({
|
|
207
|
+
name: manager.__meta.name,
|
|
208
|
+
label: manager.options.label || manager.__meta.name,
|
|
209
|
+
pluralLabel: manager.options.pluralLabel ||
|
|
210
|
+
manager.options.label ||
|
|
211
|
+
manager.__meta.name
|
|
212
|
+
}));
|
|
213
|
+
self.managedTypeNames = self.managedTypes.map(t => t.name);
|
|
214
|
+
|
|
215
|
+
// Cache page and piece type names for virtual group filters.
|
|
216
|
+
const managedManagersByName = Object.fromEntries(
|
|
217
|
+
managers
|
|
218
|
+
.filter(m => m.__meta && self.managedTypeNames.includes(m.__meta.name))
|
|
219
|
+
.map(m => [ m.__meta.name, m ])
|
|
220
|
+
);
|
|
221
|
+
self.managedPageTypeNames = self.managedTypeNames.filter(
|
|
222
|
+
name => self.apos.instanceOf(
|
|
223
|
+
managedManagersByName[name], '@apostrophecms/page-type'
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
self.managedPieceTypeNames = self.managedTypeNames.filter(
|
|
227
|
+
name => self.apos.instanceOf(
|
|
228
|
+
managedManagersByName[name], '@apostrophecms/piece-type'
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
methods(self) {
|
|
236
|
+
return {
|
|
237
|
+
// Register a new choice for a filter dropdown (Action or Status).
|
|
238
|
+
// External modules can call this in their `modulesRegistered` handler.
|
|
239
|
+
//
|
|
240
|
+
// `type` - 'action' or 'status'
|
|
241
|
+
// `name` - unique choice identifier (e.g. 'imported', 'translated')
|
|
242
|
+
// `label` - i18n key for the dropdown choice label
|
|
243
|
+
// `criteria` - a standard MongoDB criteria object (e.g.
|
|
244
|
+
// `{ lastPublishedAt: { $ne: null } }`), or a function
|
|
245
|
+
// receiving `{ cutoffDate }` and returning one when the
|
|
246
|
+
// criteria must be computed at query time (e.g. rolling date
|
|
247
|
+
// windows). `cutoffDate` is the `Date` marking the start of
|
|
248
|
+
// the configured rolling window (`options.recentDays`).
|
|
249
|
+
// Multi-field objects work as implicit `$and`, and any valid
|
|
250
|
+
// MongoDB operator is allowed.
|
|
251
|
+
// `archived` - optional boolean. When `true` the choice matches
|
|
252
|
+
// archived documents (overrides the default exclusion of
|
|
253
|
+
// archived docs). Otherwise it is ignored.
|
|
254
|
+
// `project` - optional projection object (e.g. `{ translatedAt: 1 }`)
|
|
255
|
+
// merged into `managerApiProjection` so the field is available
|
|
256
|
+
// to context operations in the recently-edited manager
|
|
257
|
+
//
|
|
258
|
+
// Examples:
|
|
259
|
+
//
|
|
260
|
+
// // Static criteria (object) — no date dependency
|
|
261
|
+
// addFilterChoice({
|
|
262
|
+
// type: 'status',
|
|
263
|
+
// name: 'translated',
|
|
264
|
+
// label: 'myModule:translated',
|
|
265
|
+
// criteria: { 'translationMeta.state': 'translated' }
|
|
266
|
+
// });
|
|
267
|
+
//
|
|
268
|
+
// // Dynamic criteria (function) — uses the rolling window
|
|
269
|
+
// addFilterChoice({
|
|
270
|
+
// type: 'action',
|
|
271
|
+
// name: 'imported',
|
|
272
|
+
// label: 'myModule:imported',
|
|
273
|
+
// criteria({ cutoffDate }) {
|
|
274
|
+
// return { importedAt: { $gte: cutoffDate } };
|
|
275
|
+
// }
|
|
276
|
+
// });
|
|
277
|
+
addFilterChoice({
|
|
278
|
+
type, name, label, criteria, archived, project
|
|
279
|
+
}) {
|
|
280
|
+
if (type !== 'action' && type !== 'status') {
|
|
281
|
+
throw new Error(`addFilterChoice: type must be "action" or "status", got "${type}"`);
|
|
282
|
+
}
|
|
283
|
+
self.filterChoiceRegistry[type][name] = {
|
|
284
|
+
label,
|
|
285
|
+
criteria,
|
|
286
|
+
archived: archived || false
|
|
287
|
+
};
|
|
288
|
+
if (project) {
|
|
289
|
+
Object.assign(self.options.managerApiProjection, project);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
// Calculate the cutoff date for recently edited documents based on the
|
|
293
|
+
// rolling window setting. Can be used by external modules to provide
|
|
294
|
+
// their own "recently X" filters that align with the same window.
|
|
295
|
+
getCutoffDate() {
|
|
296
|
+
const cutoff = new Date();
|
|
297
|
+
cutoff.setDate(
|
|
298
|
+
cutoff.getDate() - (self.options.recentDays || 30)
|
|
299
|
+
);
|
|
300
|
+
return cutoff;
|
|
301
|
+
},
|
|
302
|
+
addToAdminBar() {
|
|
303
|
+
self.apos.adminBar.add(
|
|
304
|
+
`${self.__meta.name}:manager`,
|
|
305
|
+
self.pluralLabel,
|
|
306
|
+
false,
|
|
307
|
+
{
|
|
308
|
+
component: 'AposRecentlyEditedIcon',
|
|
309
|
+
contextUtility: true,
|
|
310
|
+
tooltip: 'apostrophe:recentlyEditedDocuments'
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
addManagerModal() {
|
|
315
|
+
self.apos.modal.add(
|
|
316
|
+
`${self.__meta.name}:manager`,
|
|
317
|
+
'AposRecentlyEditedManager',
|
|
318
|
+
{ moduleName: self.__meta.name }
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
insert(req, piece, options) {
|
|
322
|
+
throw new Error('Virtual piece type, should never be inserted');
|
|
323
|
+
},
|
|
324
|
+
update(req, piece, options) {
|
|
325
|
+
const manager = self.apos.doc.getManager(piece.type);
|
|
326
|
+
return manager.update(req, piece, options);
|
|
327
|
+
},
|
|
328
|
+
publish(req, piece, options) {
|
|
329
|
+
const manager = self.apos.doc.getManager(piece.type);
|
|
330
|
+
return manager.publish(req, piece, options);
|
|
331
|
+
},
|
|
332
|
+
delete(req, piece, options) {
|
|
333
|
+
const manager = self.apos.doc.getManager(piece.type);
|
|
334
|
+
return manager.delete(req, piece, options);
|
|
335
|
+
},
|
|
336
|
+
revertDraftToPublished(req, piece, options) {
|
|
337
|
+
const manager = self.apos.doc.getManager(piece.type);
|
|
338
|
+
return manager.revertDraftToPublished(req, piece, options);
|
|
339
|
+
},
|
|
340
|
+
async distinctFromQuery(query, property, options = {}) {
|
|
341
|
+
const subquery = query.clone();
|
|
342
|
+
subquery.skip(undefined);
|
|
343
|
+
subquery.limit(undefined);
|
|
344
|
+
subquery.page(undefined);
|
|
345
|
+
subquery.perPage(undefined);
|
|
346
|
+
if (subquery.choices) {
|
|
347
|
+
subquery.choices(false);
|
|
348
|
+
}
|
|
349
|
+
if (subquery.counts) {
|
|
350
|
+
subquery.counts(false);
|
|
351
|
+
}
|
|
352
|
+
if (options.permission) {
|
|
353
|
+
subquery.and(
|
|
354
|
+
self.apos.permission.criteria(query.req, options.permission)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return subquery.toDistinct(property);
|
|
358
|
+
},
|
|
359
|
+
// Resolve the MongoDB criteria for a registered filter choice.
|
|
360
|
+
// Returns the criteria object — calls it if it's a function,
|
|
361
|
+
// passing `{ cutoffDate }` for dynamic date-based choices.
|
|
362
|
+
// The `cutoffDate` argument should come from `query.get('cutoffDate')`
|
|
363
|
+
// to ensure a single consistent date across all builders per query.
|
|
364
|
+
getFilterCriteria(entry, cutoffDate) {
|
|
365
|
+
return typeof entry.criteria === 'function'
|
|
366
|
+
? entry.criteria({ cutoffDate })
|
|
367
|
+
: entry.criteria;
|
|
368
|
+
},
|
|
369
|
+
async createIndexes() {
|
|
370
|
+
await self.apos.doc.db.createIndex(
|
|
371
|
+
{
|
|
372
|
+
updatedAt: -1,
|
|
373
|
+
_id: 1,
|
|
374
|
+
type: 1,
|
|
375
|
+
aposLocale: 1
|
|
376
|
+
},
|
|
377
|
+
{ name: 'recentlyEditedLookup' }
|
|
378
|
+
);
|
|
379
|
+
},
|
|
380
|
+
addOwnFilterChoices() {
|
|
381
|
+
self.addFilterChoice({
|
|
382
|
+
type: 'action',
|
|
383
|
+
name: 'created',
|
|
384
|
+
label: 'apostrophe:recentlyEditedActionCreated',
|
|
385
|
+
criteria({ cutoffDate }) {
|
|
386
|
+
return { createdAt: { $gte: cutoffDate } };
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
self.addFilterChoice({
|
|
390
|
+
type: 'action',
|
|
391
|
+
name: 'published',
|
|
392
|
+
label: 'apostrophe:recentlyEditedActionPublished',
|
|
393
|
+
criteria({ cutoffDate }) {
|
|
394
|
+
return { lastPublishedAt: { $gte: cutoffDate } };
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
self.addFilterChoice({
|
|
398
|
+
type: 'action',
|
|
399
|
+
name: 'submitted',
|
|
400
|
+
label: 'apostrophe:recentlyEditedActionSubmitted',
|
|
401
|
+
criteria({ cutoffDate }) {
|
|
402
|
+
return { 'submitted.at': { $gte: cutoffDate } };
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
self.addFilterChoice({
|
|
406
|
+
type: 'action',
|
|
407
|
+
name: 'localized',
|
|
408
|
+
label: 'apostrophe:recentlyEditedActionLocalized',
|
|
409
|
+
criteria({ cutoffDate }) {
|
|
410
|
+
return { localizedAt: { $gte: cutoffDate } };
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
self.addFilterChoice({
|
|
414
|
+
type: 'status',
|
|
415
|
+
name: 'live',
|
|
416
|
+
label: 'apostrophe:live',
|
|
417
|
+
criteria: { lastPublishedAt: { $ne: null } }
|
|
418
|
+
});
|
|
419
|
+
self.addFilterChoice({
|
|
420
|
+
type: 'status',
|
|
421
|
+
name: 'draft',
|
|
422
|
+
label: 'apostrophe:draft',
|
|
423
|
+
criteria: { lastPublishedAt: null }
|
|
424
|
+
});
|
|
425
|
+
self.addFilterChoice({
|
|
426
|
+
type: 'status',
|
|
427
|
+
name: 'modified',
|
|
428
|
+
label: 'apostrophe:pendingUpdates',
|
|
429
|
+
criteria: {
|
|
430
|
+
lastPublishedAt: { $ne: null },
|
|
431
|
+
$expr: {
|
|
432
|
+
$gt: [ '$updatedAt', '$lastPublishedAt' ]
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
self.addFilterChoice({
|
|
437
|
+
type: 'status',
|
|
438
|
+
name: 'submitted',
|
|
439
|
+
label: 'apostrophe:recentlyEditedStatusSubmitted',
|
|
440
|
+
criteria: { 'submitted.at': { $exists: true } }
|
|
441
|
+
});
|
|
442
|
+
self.addFilterChoice({
|
|
443
|
+
type: 'status',
|
|
444
|
+
name: 'archived',
|
|
445
|
+
label: 'apostrophe:archived',
|
|
446
|
+
archived: true
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
},
|
|
451
|
+
extendMethods(self) {
|
|
452
|
+
return {
|
|
453
|
+
find(_super, req, criteria, options) {
|
|
454
|
+
const cutoffDate = self.getCutoffDate();
|
|
455
|
+
return _super(req, criteria, options)
|
|
456
|
+
.type(null)
|
|
457
|
+
.locale(null)
|
|
458
|
+
.attachments(false)
|
|
459
|
+
.areas(false)
|
|
460
|
+
.relationships(false)
|
|
461
|
+
.cutoffDate(cutoffDate)
|
|
462
|
+
.and({
|
|
463
|
+
type: { $in: self.managedTypeNames },
|
|
464
|
+
aposMode: 'draft',
|
|
465
|
+
updatedAt: { $gte: cutoffDate }
|
|
466
|
+
})
|
|
467
|
+
.sort({
|
|
468
|
+
updatedAt: -1,
|
|
469
|
+
_id: 1
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
// Remove UI-computed column fields (_localeLabel, _lastEditor)
|
|
473
|
+
// from the projection to mute warnings - they don't exist in the DB and are
|
|
474
|
+
// explicitly supported by the custom manager component.
|
|
475
|
+
getManagerApiProjection(_super, req) {
|
|
476
|
+
const projection = _super(req);
|
|
477
|
+
for (const key of Object.keys(projection)) {
|
|
478
|
+
if ([ '_localeLabel', '_lastEditor' ].includes(key)) {
|
|
479
|
+
delete projection[key];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return projection;
|
|
483
|
+
},
|
|
484
|
+
// Inject the manager API projection into req.query before the
|
|
485
|
+
// parent processes it via applyBuildersSafely. If the client
|
|
486
|
+
// already sends a projection (e.g. { _id: 1 } for select-all),
|
|
487
|
+
// it takes precedence. This avoids unnecessarily data round-tripping.
|
|
488
|
+
//
|
|
489
|
+
// When `lean` is set, also disable URL resolution — the caller
|
|
490
|
+
// wants lightweight results close to raw DB data.
|
|
491
|
+
getRestQuery(_super, req) {
|
|
492
|
+
if (!req.query.project) {
|
|
493
|
+
const projection = self.getManagerApiProjection(req);
|
|
494
|
+
if (projection) {
|
|
495
|
+
req.query.project = projection;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const query = _super(req);
|
|
499
|
+
if (self.apos.launder.boolean(req.query.lean)) {
|
|
500
|
+
query.addUrls(false);
|
|
501
|
+
}
|
|
502
|
+
return query;
|
|
503
|
+
},
|
|
504
|
+
getBrowserData(_super, req) {
|
|
505
|
+
const data = _super(req);
|
|
506
|
+
return {
|
|
507
|
+
...data,
|
|
508
|
+
managedTypes: self.managedTypes,
|
|
509
|
+
batchOperations: [],
|
|
510
|
+
perPage: self.options.perPage,
|
|
511
|
+
rollingWindowDays: self.options.recentDays,
|
|
512
|
+
components: {
|
|
513
|
+
managerModal: 'AposRecentlyEditedManager'
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
queries(self, query) {
|
|
520
|
+
return {
|
|
521
|
+
builders: {
|
|
522
|
+
cutoffDate: {},
|
|
523
|
+
_docType: {
|
|
524
|
+
def: null,
|
|
525
|
+
launder(value) {
|
|
526
|
+
const allowed = new Set([
|
|
527
|
+
...self.managedTypeNames,
|
|
528
|
+
'@apostrophecms/any-page-type',
|
|
529
|
+
'@apostrophecms/piece-type'
|
|
530
|
+
]);
|
|
531
|
+
const raw = Array.isArray(value)
|
|
532
|
+
? self.apos.launder.strings(value)
|
|
533
|
+
: [ self.apos.launder.string(value) ].filter(Boolean);
|
|
534
|
+
return raw.filter(v => allowed.has(v));
|
|
535
|
+
},
|
|
536
|
+
finalize() {
|
|
537
|
+
const value = query.get('_docType');
|
|
538
|
+
if (!value || !value.length) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const resolved = [];
|
|
542
|
+
for (const v of value) {
|
|
543
|
+
if (v === '@apostrophecms/any-page-type') {
|
|
544
|
+
resolved.push(...self.managedPageTypeNames);
|
|
545
|
+
} else if (v === '@apostrophecms/piece-type') {
|
|
546
|
+
resolved.push(...self.managedPieceTypeNames);
|
|
547
|
+
} else {
|
|
548
|
+
resolved.push(v);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (resolved.length) {
|
|
552
|
+
query.and({ type: { $in: [ ...new Set(resolved) ] } });
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
async choices() {
|
|
556
|
+
const distinctTypes = await self.distinctFromQuery(query, 'type');
|
|
557
|
+
const managedByName = Object.fromEntries(
|
|
558
|
+
self.managedTypes.map(type => [ type.name, type ])
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const typeChoices = distinctTypes
|
|
562
|
+
.filter(type => managedByName[type])
|
|
563
|
+
.map(type => ({
|
|
564
|
+
value: type,
|
|
565
|
+
label: managedByName[type].label
|
|
566
|
+
}));
|
|
567
|
+
|
|
568
|
+
// Add virtual group entries when more than one type
|
|
569
|
+
// of that category has results — the group provides useful
|
|
570
|
+
// narrowing. A single-type category needs no group shortcut.
|
|
571
|
+
const pageCount = typeChoices.filter(
|
|
572
|
+
c => self.managedPageTypeNames.includes(c.value)
|
|
573
|
+
).length;
|
|
574
|
+
const pieceCount = typeChoices.filter(
|
|
575
|
+
c => self.managedPieceTypeNames.includes(c.value)
|
|
576
|
+
).length;
|
|
577
|
+
if (pageCount > 1) {
|
|
578
|
+
typeChoices.push({
|
|
579
|
+
value: '@apostrophecms/any-page-type',
|
|
580
|
+
label: 'apostrophe:pages'
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (pieceCount > 1) {
|
|
584
|
+
typeChoices.push({
|
|
585
|
+
value: '@apostrophecms/piece-type',
|
|
586
|
+
label: 'apostrophe:pieces'
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return typeChoices;
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
_editedBy: {
|
|
594
|
+
def: null,
|
|
595
|
+
launder(value) {
|
|
596
|
+
if (Array.isArray(value)) {
|
|
597
|
+
return self.apos.launder.strings(value).filter(Boolean);
|
|
598
|
+
}
|
|
599
|
+
return self.apos.launder.string(value) || null;
|
|
600
|
+
},
|
|
601
|
+
finalize() {
|
|
602
|
+
const value = query.get('_editedBy');
|
|
603
|
+
if (!value) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const values = Array.isArray(value) ? value : [ value ];
|
|
607
|
+
if (!values.length) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const hasSystem = values.includes('__system');
|
|
611
|
+
const userIds = values.filter(v => v !== '__system');
|
|
612
|
+
const conditions = [];
|
|
613
|
+
if (userIds.length) {
|
|
614
|
+
conditions.push({ 'updatedBy._id': { $in: userIds } });
|
|
615
|
+
}
|
|
616
|
+
if (hasSystem) {
|
|
617
|
+
conditions.push({ 'updatedBy._id': null });
|
|
618
|
+
}
|
|
619
|
+
if (conditions.length === 1) {
|
|
620
|
+
query.and(conditions[0]);
|
|
621
|
+
} else {
|
|
622
|
+
query.and({ $or: conditions });
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
async choices() {
|
|
626
|
+
const users = await self.distinctFromQuery(query, 'updatedBy');
|
|
627
|
+
const choices = [];
|
|
628
|
+
for (const user of users) {
|
|
629
|
+
if (!user) {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (user._id) {
|
|
633
|
+
choices.push({
|
|
634
|
+
value: user._id,
|
|
635
|
+
label: user.title || user.username || user._id
|
|
636
|
+
});
|
|
637
|
+
} else if (user.title) {
|
|
638
|
+
choices.push({
|
|
639
|
+
value: '__system',
|
|
640
|
+
label: user.title
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return choices;
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
_locale: {
|
|
648
|
+
def: null,
|
|
649
|
+
launder(value) {
|
|
650
|
+
if (Array.isArray(value)) {
|
|
651
|
+
return self.apos.launder.strings(value)
|
|
652
|
+
.filter(v => v && self.apos.i18n.locales[v]);
|
|
653
|
+
}
|
|
654
|
+
const laundered = self.apos.launder.string(value);
|
|
655
|
+
if (laundered && self.apos.i18n.locales[laundered]) {
|
|
656
|
+
return laundered;
|
|
657
|
+
}
|
|
658
|
+
return null;
|
|
659
|
+
},
|
|
660
|
+
finalize() {
|
|
661
|
+
const value = query.get('_locale');
|
|
662
|
+
if (!value) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (Array.isArray(value)) {
|
|
666
|
+
if (value.length) {
|
|
667
|
+
query.and({
|
|
668
|
+
aposLocale: { $in: value.map(v => `${v}:draft`) }
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
query.and({ aposLocale: `${value}:draft` });
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
async choices() {
|
|
676
|
+
// Pass permission: 'edit' so distinctFromQuery applies
|
|
677
|
+
// core permission criteria. Modules that extend
|
|
678
|
+
// permission.criteria() (e.g. advanced-permission)
|
|
679
|
+
// automatically filter by allowed locales.
|
|
680
|
+
const distinctLocales = await self.distinctFromQuery(
|
|
681
|
+
query, 'aposLocale', { permission: 'edit' }
|
|
682
|
+
);
|
|
683
|
+
const localesConfig = self.apos.i18n?.locales || {};
|
|
684
|
+
const seen = new Set();
|
|
685
|
+
|
|
686
|
+
return distinctLocales
|
|
687
|
+
.filter(Boolean)
|
|
688
|
+
.map(locale => locale.split(':')[0])
|
|
689
|
+
.filter(locale => {
|
|
690
|
+
if (!locale || seen.has(locale)) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
seen.add(locale);
|
|
694
|
+
return true;
|
|
695
|
+
})
|
|
696
|
+
.map(locale => ({
|
|
697
|
+
value: locale,
|
|
698
|
+
label: localesConfig[locale]?.label
|
|
699
|
+
? `${localesConfig[locale].label} (${locale})`
|
|
700
|
+
: locale
|
|
701
|
+
}))
|
|
702
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
_status: {
|
|
706
|
+
def: null,
|
|
707
|
+
launder(value) {
|
|
708
|
+
if (Array.isArray(value)) {
|
|
709
|
+
return self.apos.launder.strings(value)
|
|
710
|
+
.filter(v => self.filterChoiceRegistry.status[v]);
|
|
711
|
+
}
|
|
712
|
+
const laundered = self.apos.launder.string(value);
|
|
713
|
+
return self.filterChoiceRegistry.status[laundered] ? laundered : null;
|
|
714
|
+
},
|
|
715
|
+
prefinalize() {
|
|
716
|
+
const value = query.get('_status');
|
|
717
|
+
if (!value) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const cutoffDate = query.get('cutoffDate');
|
|
721
|
+
if (Array.isArray(value)) {
|
|
722
|
+
if (!value.length) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const orClauses = [];
|
|
726
|
+
let needsArchivedNull = false;
|
|
727
|
+
for (const v of value) {
|
|
728
|
+
const entry = self.filterChoiceRegistry.status[v];
|
|
729
|
+
if (!entry) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (entry.archived) {
|
|
733
|
+
needsArchivedNull = true;
|
|
734
|
+
const criteria = self.getFilterCriteria(entry, cutoffDate);
|
|
735
|
+
if (criteria) {
|
|
736
|
+
orClauses.push({ $and: [ { archived: true }, criteria ] });
|
|
737
|
+
} else {
|
|
738
|
+
orClauses.push({ archived: true });
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
const criteria = self.getFilterCriteria(entry, cutoffDate);
|
|
742
|
+
if (criteria) {
|
|
743
|
+
orClauses.push(criteria);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (needsArchivedNull) {
|
|
748
|
+
query.archived(null);
|
|
749
|
+
}
|
|
750
|
+
if (orClauses.length) {
|
|
751
|
+
query.and({ $or: orClauses });
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
const entry = self.filterChoiceRegistry.status[value];
|
|
755
|
+
if (entry) {
|
|
756
|
+
if (entry.archived) {
|
|
757
|
+
query.archived(true);
|
|
758
|
+
}
|
|
759
|
+
const criteria = self.getFilterCriteria(entry, cutoffDate);
|
|
760
|
+
if (criteria) {
|
|
761
|
+
query.and(criteria);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
choices() {
|
|
767
|
+
return Object.entries(
|
|
768
|
+
self.filterChoiceRegistry.status
|
|
769
|
+
).map(([ value, config ]) => ({
|
|
770
|
+
value,
|
|
771
|
+
label: config.label
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
_action: {
|
|
776
|
+
def: null,
|
|
777
|
+
launder(value) {
|
|
778
|
+
if (Array.isArray(value)) {
|
|
779
|
+
return self.apos.launder.strings(value)
|
|
780
|
+
.filter(v => self.filterChoiceRegistry.action[v]);
|
|
781
|
+
}
|
|
782
|
+
const laundered = self.apos.launder.string(value);
|
|
783
|
+
return self.filterChoiceRegistry.action[laundered] ? laundered : null;
|
|
784
|
+
},
|
|
785
|
+
prefinalize() {
|
|
786
|
+
const value = query.get('_action');
|
|
787
|
+
if (!value) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const cutoffDate = query.get('cutoffDate');
|
|
791
|
+
if (Array.isArray(value)) {
|
|
792
|
+
if (!value.length) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const orClauses = [];
|
|
796
|
+
for (const v of value) {
|
|
797
|
+
const entry = self.filterChoiceRegistry.action[v];
|
|
798
|
+
if (!entry) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const criteria = self.getFilterCriteria(entry, cutoffDate);
|
|
802
|
+
if (criteria) {
|
|
803
|
+
orClauses.push(criteria);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (orClauses.length) {
|
|
807
|
+
query.and({ $or: orClauses });
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
const entry = self.filterChoiceRegistry.action[value];
|
|
811
|
+
if (entry) {
|
|
812
|
+
const criteria = self.getFilterCriteria(entry, cutoffDate);
|
|
813
|
+
if (criteria) {
|
|
814
|
+
query.and(criteria);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
choices() {
|
|
820
|
+
return Object.entries(
|
|
821
|
+
self.filterChoiceRegistry.action
|
|
822
|
+
).map(([ value, config ]) => ({
|
|
823
|
+
value,
|
|
824
|
+
label: config.label
|
|
825
|
+
}));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
};
|