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
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const _ = require('lodash');
|
|
2
|
-
|
|
3
1
|
// A subclass of `@apostrophecms/piece-type`, `@apostrophecms/file` establishes
|
|
4
2
|
// a library of uploaded files, which may be of any type acceptable to the
|
|
5
3
|
// [@apostrophecms/attachment](../@apostrophecms/attachment/index.html) module.
|
|
@@ -8,6 +6,8 @@ const _ = require('lodash');
|
|
|
8
6
|
// module provides a simple way to add downloadable PDFs and the like to a
|
|
9
7
|
// website, and to manage a library of them for reuse.
|
|
10
8
|
|
|
9
|
+
const streamProxy = require('../../../lib/stream-proxy.js');
|
|
10
|
+
|
|
11
11
|
module.exports = {
|
|
12
12
|
extend: '@apostrophecms/piece-type',
|
|
13
13
|
options: {
|
|
@@ -26,7 +26,9 @@ module.exports = {
|
|
|
26
26
|
// Files should by default be considered "related documents" when localizing
|
|
27
27
|
// another document that references them
|
|
28
28
|
relatedDocument: true,
|
|
29
|
-
relationshipSuggestionIcon: 'file-document-icon'
|
|
29
|
+
relationshipSuggestionIcon: 'file-document-icon',
|
|
30
|
+
prettyUrls: false,
|
|
31
|
+
prettyUrlDir: '/files'
|
|
30
32
|
},
|
|
31
33
|
fields: {
|
|
32
34
|
remove: [ 'visibility' ],
|
|
@@ -92,10 +94,72 @@ module.exports = {
|
|
|
92
94
|
},
|
|
93
95
|
methods(self) {
|
|
94
96
|
return {
|
|
97
|
+
// File docs are attachment containers themselves — their
|
|
98
|
+
// attachments are discovered via relationship walking from
|
|
99
|
+
// the content docs that reference them. Iterating all
|
|
100
|
+
// published files here would make the "used" scope
|
|
101
|
+
// equivalent to "all", defeating scoped attachment builds.
|
|
102
|
+
async getAllUrlMetadata() {
|
|
103
|
+
return {
|
|
104
|
+
metadata: [],
|
|
105
|
+
attachmentDocIds: []
|
|
106
|
+
};
|
|
107
|
+
},
|
|
95
108
|
addUrls(req, files) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
// Watch out for projections with no attachment property
|
|
111
|
+
// (the slug-taken route does that)
|
|
112
|
+
if (self.options.prettyUrls && file.attachment) {
|
|
113
|
+
const { extension } = file.attachment;
|
|
114
|
+
file._url = `${req.prefix}${self.options.prettyUrlDir}/${file.slug.replace(self.options.slugPrefix || '', '')}.${extension}`;
|
|
115
|
+
file.attachment._prettyUrl = file._url;
|
|
116
|
+
} else {
|
|
117
|
+
file._url = self.apos.attachment.url(file.attachment);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
routes(self) {
|
|
124
|
+
if (!self.options.prettyUrls) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
get: {
|
|
129
|
+
async [`${self.options.prettyUrlDir}/*`](req, res) {
|
|
130
|
+
try {
|
|
131
|
+
const matches = (req.params[0] || '').match(/^([^.]+)\.\w+$/);
|
|
132
|
+
if (!matches) {
|
|
133
|
+
return res.status(400).send('invalid');
|
|
134
|
+
}
|
|
135
|
+
const [ , slug ] = matches;
|
|
136
|
+
if (slug.includes('..') || slug.includes('/')) {
|
|
137
|
+
return res.status(403).send('forbidden');
|
|
138
|
+
}
|
|
139
|
+
const file = await self.find(req, {
|
|
140
|
+
slug: `${self.options.slugPrefix}${slug}`
|
|
141
|
+
}).toObject();
|
|
142
|
+
if (!file) {
|
|
143
|
+
return res.status(404).send('not found');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Determine the normal, "ugly" URL and stream the
|
|
147
|
+
// response from it, passing on the most important
|
|
148
|
+
// headers. Supporting range requests, which PDF viewers
|
|
149
|
+
// like to use for pagination, was considered but
|
|
150
|
+
// the potential interacts with gzip encoding are complex
|
|
151
|
+
// and we currently do use it by default with s3 for PDFs.
|
|
152
|
+
// Viewers will still display the document after
|
|
153
|
+
// completing the download
|
|
154
|
+
const uglyUrl = self.apos.attachment.url(file.attachment, {
|
|
155
|
+
prettyUrl: false
|
|
156
|
+
});
|
|
157
|
+
return await streamProxy(req, uglyUrl, { error: self.apos.util.error });
|
|
158
|
+
} catch (e) {
|
|
159
|
+
self.apos.util.error('Error in pretty URL route:', e);
|
|
160
|
+
return res.status(500).send('error');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
99
163
|
}
|
|
100
164
|
};
|
|
101
165
|
}
|
|
@@ -222,7 +222,26 @@ module.exports = {
|
|
|
222
222
|
const doc = localizations.find(doc => doc.aposLocale.split(':')[0] === name);
|
|
223
223
|
if (doc && self.apos.permission.can(req, 'view', doc)) {
|
|
224
224
|
doc.available = true;
|
|
225
|
-
|
|
225
|
+
// WARNING: the `addUrls` call below has a serious
|
|
226
|
+
// performance impact (extra DB queries per locale).
|
|
227
|
+
// Keep this gated for static builds only.
|
|
228
|
+
if (self.apos.url.isExternalFront(req) && self.apos.url.options.static) {
|
|
229
|
+
// Static builds can't follow the API redirect route
|
|
230
|
+
// (there is no server to handle it), so compute the
|
|
231
|
+
// real URL using the same mechanisms the query builders
|
|
232
|
+
// would.
|
|
233
|
+
if (self.apos.page.isPage(doc)) {
|
|
234
|
+
doc._url = `${localeReq.prefix}${doc.slug}`;
|
|
235
|
+
} else if (manager.addUrls) {
|
|
236
|
+
await manager.addUrls(localeReq, [ doc ]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Fall back to the API redirect route when no real URL
|
|
240
|
+
// was resolved (e.g. traditional Nunjucks frontend, non static
|
|
241
|
+
// external frontend).
|
|
242
|
+
if (!doc._url) {
|
|
243
|
+
doc._url = `${self.apos.prefix}${manager.action}/${context._id}/locale/${name}`;
|
|
244
|
+
}
|
|
226
245
|
if (doc._id === context._id) {
|
|
227
246
|
doc.current = true;
|
|
228
247
|
}
|
|
@@ -400,6 +400,17 @@ module.exports = {
|
|
|
400
400
|
},
|
|
401
401
|
methods(self) {
|
|
402
402
|
return {
|
|
403
|
+
// Image docs are attachment containers themselves — their
|
|
404
|
+
// attachments are discovered via relationship walking from
|
|
405
|
+
// the content docs that reference them. Iterating all
|
|
406
|
+
// published images here would make the "used" scope
|
|
407
|
+
// equivalent to "all", defeating scoped attachment builds.
|
|
408
|
+
async getAllUrlMetadata() {
|
|
409
|
+
return {
|
|
410
|
+
metadata: [],
|
|
411
|
+
attachmentDocIds: []
|
|
412
|
+
};
|
|
413
|
+
},
|
|
403
414
|
// This method is available as a template helper: apos.image.first
|
|
404
415
|
//
|
|
405
416
|
// Find the first image attachment referenced within an object that may
|
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
:data-apos-area="areaId"
|
|
5
5
|
data-tablet-full="true"
|
|
6
6
|
class="apos-area"
|
|
7
|
-
:class="
|
|
7
|
+
:class="[
|
|
8
|
+
themeClass,
|
|
9
|
+
{
|
|
10
|
+
'apos-area--empty': next.length === 0
|
|
11
|
+
}
|
|
12
|
+
]"
|
|
8
13
|
:style="{
|
|
9
14
|
'--colspan': gridModuleOptions.columns,
|
|
10
15
|
'--colstart': 1,
|
|
11
16
|
'--justify': 'stretch',
|
|
12
17
|
'--align': 'stretch'
|
|
13
18
|
}"
|
|
14
|
-
@click="
|
|
19
|
+
@click="setFocusedWidget(parentOptions.widgetId, areaId)"
|
|
15
20
|
>
|
|
16
21
|
<div
|
|
17
22
|
v-if="next.length === 0 && !foreign && layoutMode === 'debug'"
|
|
@@ -92,6 +97,11 @@
|
|
|
92
97
|
:rendering="rendering(widget)"
|
|
93
98
|
:controls-disabled="true"
|
|
94
99
|
:breadcrumb-disabled="true"
|
|
100
|
+
:style="{
|
|
101
|
+
'z-index': raisedWidgets.has(widget._id)
|
|
102
|
+
? next.length + 1
|
|
103
|
+
: null
|
|
104
|
+
}"
|
|
95
105
|
@up="up"
|
|
96
106
|
@down="down"
|
|
97
107
|
@remove="remove"
|
|
@@ -203,12 +213,17 @@ export default {
|
|
|
203
213
|
// Intercept the columns focus, and emphasize the layout widget instead.
|
|
204
214
|
async focusedWidget(widgetId) {
|
|
205
215
|
if (!widgetId || !this.parentOptions.widgetId) {
|
|
216
|
+
this.switchLayoutMode({
|
|
217
|
+
widgetId: this.parentOptions.widgetId,
|
|
218
|
+
data: { value: 'content' }
|
|
219
|
+
});
|
|
220
|
+
this.setFocusedWidget(null, null);
|
|
206
221
|
return;
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
await this.$nextTick();
|
|
210
225
|
if (widgetId === this.parentOptions.widgetId && this.layoutMode === 'layout') {
|
|
211
|
-
|
|
226
|
+
apos.bus.$emit('suppress-focused-widget-controls');
|
|
212
227
|
this.emphasizeGrid();
|
|
213
228
|
return;
|
|
214
229
|
}
|
|
@@ -218,6 +233,16 @@ export default {
|
|
|
218
233
|
} else {
|
|
219
234
|
this.deEmphasizeGrid();
|
|
220
235
|
}
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
!this.layoutColumnWidgetDeepIds.includes(widgetId) &&
|
|
239
|
+
widgetId !== this.parentOptions.widgetId
|
|
240
|
+
) {
|
|
241
|
+
this.switchLayoutMode({
|
|
242
|
+
widgetId: this.parentOptions.widgetId,
|
|
243
|
+
data: { value: 'content' }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
221
246
|
},
|
|
222
247
|
// Steal the columns hover, set it on the layout widget instead.
|
|
223
248
|
hoveredWidget(widgetId) {
|
|
@@ -230,10 +255,9 @@ export default {
|
|
|
230
255
|
},
|
|
231
256
|
layoutMode(newMode) {
|
|
232
257
|
if (newMode === 'layout') {
|
|
233
|
-
|
|
258
|
+
apos.bus.$emit('suppress-focused-widget-controls');
|
|
234
259
|
this.emphasizeGrid();
|
|
235
260
|
} else {
|
|
236
|
-
this.setFocusedWidget(this.parentOptions.widgetId, this.areaId);
|
|
237
261
|
this.deEmphasizeGrid();
|
|
238
262
|
}
|
|
239
263
|
}
|
|
@@ -257,7 +281,8 @@ export default {
|
|
|
257
281
|
'updateWidget',
|
|
258
282
|
'setHoveredWidget',
|
|
259
283
|
'addEmphasizedWidget',
|
|
260
|
-
'removeEmphasizedWidget'
|
|
284
|
+
'removeEmphasizedWidget',
|
|
285
|
+
'setFocusedWidget'
|
|
261
286
|
]),
|
|
262
287
|
clickOnGrid() {
|
|
263
288
|
if (this.parentOptions.widgetId) {
|
|
@@ -348,29 +348,31 @@ export default {
|
|
|
348
348
|
margin-left: auto;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
&__grid.manage
|
|
352
|
-
&__grid.manage
|
|
351
|
+
&__grid.manage .apos-area--empty,
|
|
352
|
+
&__grid.manage .apos-empty-area {
|
|
353
353
|
padding-top: 0;
|
|
354
354
|
padding-bottom: 0;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
&__grid.manage
|
|
358
|
-
&__grid.manage
|
|
359
|
-
&__grid.manage
|
|
360
|
-
&__grid.manage
|
|
361
|
-
&__grid.manage
|
|
362
|
-
&__grid.manage
|
|
357
|
+
&__grid.manage .apos-layout__item-content,
|
|
358
|
+
&__grid.manage .apos-area-widget-wrapper,
|
|
359
|
+
&__grid.manage .apos-area-widget-inner,
|
|
360
|
+
&__grid.manage .apos-area-widget-rendered-widget > div,
|
|
361
|
+
&__grid.manage .apos-area-widget-rendered-widget,
|
|
362
|
+
&__grid.manage .apos-area,
|
|
363
|
+
&__grid.manage .apos-empty-area {
|
|
363
364
|
height: 100%;
|
|
364
365
|
}
|
|
365
366
|
|
|
366
|
-
&__grid.manage
|
|
367
|
+
&__grid.manage .apos-empty-area::before {
|
|
367
368
|
position: absolute;
|
|
368
369
|
font-family: var(--a-family-default);
|
|
370
|
+
font-size: var(--a-type-base);
|
|
369
371
|
text-align: center;
|
|
370
372
|
content: var(--empty-area-text);
|
|
371
373
|
}
|
|
372
374
|
|
|
373
|
-
&__grid.manage
|
|
375
|
+
&__grid.manage .apos-area-menu .apos-button {
|
|
374
376
|
display: none;
|
|
375
377
|
}
|
|
376
378
|
|
|
@@ -61,6 +61,8 @@ module.exports = {
|
|
|
61
61
|
localLogin: true,
|
|
62
62
|
passwordReset: false,
|
|
63
63
|
passwordResetHours: 48,
|
|
64
|
+
// Maximum time to complete login (1 hour)
|
|
65
|
+
incompleteLifetime: 60 * 60 * 1000,
|
|
64
66
|
scene: 'apos',
|
|
65
67
|
csrfExceptions: [
|
|
66
68
|
'login'
|
|
@@ -85,6 +87,7 @@ module.exports = {
|
|
|
85
87
|
await self.enableBearerTokens();
|
|
86
88
|
self.addToAdminBar();
|
|
87
89
|
self.addCaseInsensitiveMigration();
|
|
90
|
+
self.cleanupInterval = setInterval(self.cleanup, self.options.incompleteLifetime);
|
|
88
91
|
},
|
|
89
92
|
handlers(self) {
|
|
90
93
|
return {
|
|
@@ -97,6 +100,14 @@ module.exports = {
|
|
|
97
100
|
async checkForUser() {
|
|
98
101
|
await self.checkForUserAndAlert();
|
|
99
102
|
}
|
|
103
|
+
},
|
|
104
|
+
'apostrophe:destroy': {
|
|
105
|
+
clearIntervals() {
|
|
106
|
+
if (self.cleanupInterval) {
|
|
107
|
+
clearInterval(self.cleanupInterval);
|
|
108
|
+
self.cleanupInterval = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
100
111
|
}
|
|
101
112
|
};
|
|
102
113
|
},
|
|
@@ -725,16 +736,12 @@ module.exports = {
|
|
|
725
736
|
|
|
726
737
|
const user = await self.deserializeUser(token.userId);
|
|
727
738
|
if (!user) {
|
|
728
|
-
await
|
|
729
|
-
_id: token.userId
|
|
730
|
-
});
|
|
739
|
+
await removeToken();
|
|
731
740
|
throw self.apos.error('notfound');
|
|
732
741
|
}
|
|
733
742
|
|
|
734
743
|
if (session) {
|
|
735
|
-
await
|
|
736
|
-
_id: token.userId
|
|
737
|
-
});
|
|
744
|
+
await removeToken();
|
|
738
745
|
await self.passportLogin(req, user);
|
|
739
746
|
// No access to login attempts in the final phase.
|
|
740
747
|
self.logInfo(req, 'complete', {
|
|
@@ -742,9 +749,12 @@ module.exports = {
|
|
|
742
749
|
});
|
|
743
750
|
} else {
|
|
744
751
|
delete token.requirementsToVerify;
|
|
745
|
-
self.bearerTokens.updateOne(token, {
|
|
752
|
+
await self.bearerTokens.updateOne(token, {
|
|
746
753
|
$unset: {
|
|
747
754
|
requirementsToVerify: 1
|
|
755
|
+
},
|
|
756
|
+
$set: {
|
|
757
|
+
expires: Date.now() + self.getBearerTokenLifetime()
|
|
748
758
|
}
|
|
749
759
|
});
|
|
750
760
|
self.logInfo(req, 'complete', {
|
|
@@ -754,6 +764,12 @@ module.exports = {
|
|
|
754
764
|
token
|
|
755
765
|
};
|
|
756
766
|
}
|
|
767
|
+
|
|
768
|
+
async function removeToken() {
|
|
769
|
+
await self.bearerTokens.removeOne({
|
|
770
|
+
_id: token._id
|
|
771
|
+
});
|
|
772
|
+
}
|
|
757
773
|
},
|
|
758
774
|
|
|
759
775
|
// Implementation detail of the login route and the requirementProps
|
|
@@ -765,7 +781,9 @@ module.exports = {
|
|
|
765
781
|
_id: self.apos.launder.string(token),
|
|
766
782
|
requirementsToVerify: {
|
|
767
783
|
$exists: true,
|
|
768
|
-
$
|
|
784
|
+
$not: {
|
|
785
|
+
$size: 0
|
|
786
|
+
}
|
|
769
787
|
},
|
|
770
788
|
expires: {
|
|
771
789
|
$gte: new Date()
|
|
@@ -858,7 +876,7 @@ module.exports = {
|
|
|
858
876
|
// Default lifetime of 1 hour is generous to permit situations
|
|
859
877
|
// like installing a TOTP app for the first time
|
|
860
878
|
expires: new Date(
|
|
861
|
-
|
|
879
|
+
Date.now() + self.options.incompleteLifetime
|
|
862
880
|
)
|
|
863
881
|
});
|
|
864
882
|
|
|
@@ -884,8 +902,8 @@ module.exports = {
|
|
|
884
902
|
_id: token,
|
|
885
903
|
userId: user._id,
|
|
886
904
|
expires: new Date(
|
|
887
|
-
|
|
888
|
-
|
|
905
|
+
Date.now() +
|
|
906
|
+
self.getBearerTokenLifetime()
|
|
889
907
|
)
|
|
890
908
|
});
|
|
891
909
|
|
|
@@ -1071,6 +1089,20 @@ module.exports = {
|
|
|
1071
1089
|
{ failed: duplicatedUsernames }
|
|
1072
1090
|
);
|
|
1073
1091
|
}
|
|
1092
|
+
},
|
|
1093
|
+
getBearerTokenLifetime() {
|
|
1094
|
+
return (self.options.bearerTokens.lifetime || (86400 * 7 * 2)) * 1000;
|
|
1095
|
+
},
|
|
1096
|
+
// Periodic cleanup. Expired bearer tokens are only honored until they expire, but
|
|
1097
|
+
// it also makes sense to free the resources after expiration
|
|
1098
|
+
async cleanup() {
|
|
1099
|
+
return self.bearerTokens.deleteMany(
|
|
1100
|
+
{
|
|
1101
|
+
expires: {
|
|
1102
|
+
$lt: new Date()
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
);
|
|
1074
1106
|
}
|
|
1075
1107
|
};
|
|
1076
1108
|
},
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
aria-modal="true"
|
|
14
14
|
:aria-labelledby="props.modalData.id"
|
|
15
15
|
data-apos-modal
|
|
16
|
+
:data-apos-graph-key="props.graphKey || undefined"
|
|
16
17
|
tabindex="0"
|
|
17
18
|
@focus.capture="captureFocus"
|
|
18
19
|
@keyup.tab="onKeyup"
|
|
@@ -188,6 +189,10 @@ const props = defineProps({
|
|
|
188
189
|
modalData: {
|
|
189
190
|
type: Object,
|
|
190
191
|
required: true
|
|
192
|
+
},
|
|
193
|
+
graphKey: {
|
|
194
|
+
type: String,
|
|
195
|
+
default: null
|
|
191
196
|
}
|
|
192
197
|
});
|
|
193
198
|
|
|
@@ -2855,18 +2855,16 @@ database.`);
|
|
|
2855
2855
|
);
|
|
2856
2856
|
},
|
|
2857
2857
|
// Returns the effective base URL for the given request.
|
|
2858
|
-
//
|
|
2859
|
-
//
|
|
2860
|
-
//
|
|
2861
|
-
//
|
|
2862
|
-
//
|
|
2863
|
-
//
|
|
2858
|
+
// Delegates to `apos.url.getBaseUrl(req)` which is
|
|
2859
|
+
// now the single source of truth.
|
|
2860
|
+
// See `@apostrophecms/url` module for full documentation
|
|
2861
|
+
// and the `strict` option.
|
|
2862
|
+
//
|
|
2863
|
+
// Note: `prefix: false` preserves backward compatibility —
|
|
2864
|
+
// callers of `page.getBaseUrl` historically received only
|
|
2865
|
+
// the origin, without the global prefix.
|
|
2864
2866
|
getBaseUrl(req) {
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
return hostname
|
|
2868
|
-
? `${req.protocol}://${hostname}`
|
|
2869
|
-
: (self.apos.baseUrl || '');
|
|
2867
|
+
return self.apos.url.getBaseUrl(req, { prefix: false });
|
|
2870
2868
|
},
|
|
2871
2869
|
|
|
2872
2870
|
// Implements a simple batch operation like publish or unpublish.
|
|
@@ -107,12 +107,17 @@ module.exports = {
|
|
|
107
107
|
'slug'
|
|
108
108
|
]);
|
|
109
109
|
self.rules = {};
|
|
110
|
-
self.dispatchAll();
|
|
111
110
|
self.composeFilters();
|
|
112
111
|
self.composeColumns();
|
|
113
112
|
},
|
|
114
113
|
handlers(self) {
|
|
115
114
|
return {
|
|
115
|
+
'apostrophe:modulesRegistered': {
|
|
116
|
+
dispatchAll() {
|
|
117
|
+
// Late enough that all subclasses have contributed things to self
|
|
118
|
+
self.dispatchAll();
|
|
119
|
+
}
|
|
120
|
+
},
|
|
116
121
|
'@apostrophecms/page:serve': {
|
|
117
122
|
async dispatchPage(req) {
|
|
118
123
|
if (!req.data.bestPage) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// ### `piecesFilters`
|
|
17
17
|
//
|
|
18
|
-
// If present, this is an array of objects with `name` properties.This works
|
|
18
|
+
// If present, this is an array of objects with `name` properties. This works
|
|
19
19
|
// only if the corresponding query builders exist and have a `launder` method.
|
|
20
20
|
// An array of choices for each is populated in `req.data.piecesFilters`. The
|
|
21
21
|
// choices in the array are objects with `label` and `value` properties.
|
|
@@ -199,6 +199,26 @@ module.exports = {
|
|
|
199
199
|
|
|
200
200
|
dispatchAll() {
|
|
201
201
|
self.dispatch('/', self.indexPage);
|
|
202
|
+
if (self.apos.url.options.static) {
|
|
203
|
+
// 1. SEO friendly pagination URLs
|
|
204
|
+
self.dispatch('/page/:pagenum', req => {
|
|
205
|
+
req.query.page = req.params.pagenum;
|
|
206
|
+
return self.indexPage(req);
|
|
207
|
+
});
|
|
208
|
+
for (const filter of self.piecesFilters) {
|
|
209
|
+
// 2. SEO friendly filter URLs
|
|
210
|
+
self.dispatch(`/${filter.name}/:filterValue`, req => {
|
|
211
|
+
req.query[filter.name] = req.params.filterValue;
|
|
212
|
+
return self.indexPage(req);
|
|
213
|
+
});
|
|
214
|
+
// 3. SEO friendly filter + pagination URLs
|
|
215
|
+
self.dispatch(`/${filter.name}/:filterValue/page/:pagenum`, req => {
|
|
216
|
+
req.query[filter.name] = req.params.filterValue;
|
|
217
|
+
req.query.page = req.params.pagenum;
|
|
218
|
+
return self.indexPage(req);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
202
222
|
self.dispatch('/:slug', self.showPage);
|
|
203
223
|
},
|
|
204
224
|
|
|
@@ -325,30 +345,62 @@ module.exports = {
|
|
|
325
345
|
return !!(await self.find(req, {}).areas(false).relationships(false).toObject());
|
|
326
346
|
},
|
|
327
347
|
|
|
328
|
-
// Populate `req.data.
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
348
|
+
// Populate `req.data.filters` based on the `piecesFilters` option.
|
|
349
|
+
// Each entry in the option array must have a `name` property, and a
|
|
350
|
+
// corresponding query builder with a `launder` method must exist
|
|
351
|
+
// (note that most schema fields automatically get one).
|
|
352
|
+
//
|
|
353
|
+
// `req.data.filters` is an array of filter objects, each containing
|
|
354
|
+
// the original configuration properties (e.g. `name`, `counts`) plus
|
|
355
|
+
// a `choices` array. Each choice has `label`, `value`, `_url`, and
|
|
356
|
+
// optionally `active` and `count` properties. Choices are reduced by
|
|
357
|
+
// the other active filters; for instance, "tags" might only reveal
|
|
335
358
|
// choices not ruled out by the current "topic" filter setting.
|
|
336
359
|
//
|
|
337
|
-
// If a filter
|
|
338
|
-
//
|
|
339
|
-
//
|
|
360
|
+
// If a filter has its `counts` property set to `true`, each choice
|
|
361
|
+
// will also include a `count`. This has a performance impact.
|
|
362
|
+
//
|
|
363
|
+
// Legacy: `req.data.piecesFilters` is also populated as an object
|
|
364
|
+
// keyed by filter name, where each value is that filter's choices
|
|
365
|
+
// array. Still supported for backward compatibility but
|
|
366
|
+
// `req.data.filters` is preferred.
|
|
340
367
|
|
|
341
368
|
async populatePiecesFilters(query) {
|
|
342
369
|
const req = query.req;
|
|
343
|
-
|
|
370
|
+
const filtersWithChoices = await self.getFiltersWithChoices(query);
|
|
371
|
+
req.data.filters = filtersWithChoices;
|
|
372
|
+
// for bc (less useful)
|
|
373
|
+
req.data.piecesFilters = {};
|
|
374
|
+
for (const filter of filtersWithChoices) {
|
|
375
|
+
req.data.piecesFilters[filter.name] = filter.choices;
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
async getFiltersWithChoices(query, { allCounts = false } = {}) {
|
|
380
|
+
const results = [];
|
|
344
381
|
for (const filter of self.piecesFilters) {
|
|
345
382
|
// The choices for each filter should reflect the effect of all
|
|
346
383
|
// filters except this one (filtering by topic pares down the list of
|
|
347
384
|
// categories and vice versa)
|
|
348
385
|
const _query = query.clone();
|
|
349
386
|
_query[filter.name](undefined);
|
|
350
|
-
|
|
387
|
+
const countsOption = allCounts ? { counts: true } : _.pick(filter, 'counts');
|
|
388
|
+
const choices = await _query.toChoices(filter.name, countsOption);
|
|
389
|
+
for (const choice of choices) {
|
|
390
|
+
if (query.req.data.page) {
|
|
391
|
+
choice._url = query.req.data.page._url +
|
|
392
|
+
self.apos.url.getChoiceFilter(filter.name, choice.value, 1);
|
|
393
|
+
}
|
|
394
|
+
if (query.req.query[filter.name] === choice.value) {
|
|
395
|
+
choice.active = true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
results.push({
|
|
399
|
+
...filter,
|
|
400
|
+
choices
|
|
401
|
+
});
|
|
351
402
|
}
|
|
403
|
+
return results;
|
|
352
404
|
}
|
|
353
405
|
};
|
|
354
406
|
|
|
@@ -368,6 +420,41 @@ module.exports = {
|
|
|
368
420
|
} else {
|
|
369
421
|
return data;
|
|
370
422
|
}
|
|
423
|
+
},
|
|
424
|
+
async getUrlMetadata(_super, req, doc) {
|
|
425
|
+
const metadata = await _super(req, doc);
|
|
426
|
+
if (!metadata.length) {
|
|
427
|
+
return metadata;
|
|
428
|
+
}
|
|
429
|
+
const [ pm ] = metadata;
|
|
430
|
+
const query = self.indexQuery(req);
|
|
431
|
+
const filters = await self.getFiltersWithChoices(query, { allCounts: true });
|
|
432
|
+
|
|
433
|
+
// 1. Enumerate every filter + choice combination
|
|
434
|
+
for (const filter of filters) {
|
|
435
|
+
for (const choice of filter.choices) {
|
|
436
|
+
const totalPages = Math.max(1, Math.ceil(choice.count / self.perPage));
|
|
437
|
+
for (let p = 1; p <= totalPages; p++) {
|
|
438
|
+
metadata.push({
|
|
439
|
+
...pm,
|
|
440
|
+
i18nId: `${pm.i18nId}.${self.apos.util.slugify(filter.name)}.${self.apos.util.slugify(choice.value)}.${p}`,
|
|
441
|
+
url: pm.url + self.apos.url
|
|
442
|
+
.getChoiceFilter(filter.name, choice.value, p)
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
await query.toCount();
|
|
448
|
+
const totalPages = query.get('totalPages');
|
|
449
|
+
// 2. Enumerate pagination (starting at page 2; page 1 is the base URL)
|
|
450
|
+
for (let p = 2; p <= totalPages; p++) {
|
|
451
|
+
metadata.push({
|
|
452
|
+
...pm,
|
|
453
|
+
i18nId: `${pm.i18nId}.${p}`,
|
|
454
|
+
url: pm.url + self.apos.url.getPageFilter(p)
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return metadata;
|
|
371
458
|
}
|
|
372
459
|
};
|
|
373
460
|
}
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue
CHANGED
|
@@ -191,6 +191,7 @@ export default {
|
|
|
191
191
|
...this.schemaHtmlAttributes.reduce((acc, field) => {
|
|
192
192
|
const value = this.docFields.data[field.name];
|
|
193
193
|
if (field.type === 'checkboxes' && !value?.[0]) {
|
|
194
|
+
acc[field.htmlAttribute] = null;
|
|
194
195
|
return acc;
|
|
195
196
|
}
|
|
196
197
|
if (field.type === 'boolean') {
|