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
|
@@ -97,17 +97,29 @@ export default {
|
|
|
97
97
|
};
|
|
98
98
|
},
|
|
99
99
|
widgetPrimaryControls() {
|
|
100
|
+
const removeForSingleWidget = [ 'nudgeUp', 'nudgeDown' ];
|
|
100
101
|
// Custom widget operations displayed in the primary controls
|
|
101
|
-
return this.widgetPrimaryOperations
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
return this.widgetPrimaryOperations
|
|
103
|
+
.map(operation => {
|
|
104
|
+
const disabled = this.disabled || isOperationDisabled(operation, this.$props);
|
|
105
|
+
const tooltip = getOperationTooltip(operation, { disabled });
|
|
106
|
+
return {
|
|
107
|
+
...this.widgetDefaultControl,
|
|
108
|
+
...operation,
|
|
109
|
+
disabled,
|
|
110
|
+
tooltip
|
|
111
|
+
};
|
|
112
|
+
})
|
|
113
|
+
.filter(operation => {
|
|
114
|
+
if (
|
|
115
|
+
removeForSingleWidget.includes(operation.action) &&
|
|
116
|
+
this.first &&
|
|
117
|
+
this.last
|
|
118
|
+
) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
111
123
|
},
|
|
112
124
|
widgetSecondaryControls() {
|
|
113
125
|
const renderOperation = (operation) => {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { createId } from '@paralleldrive/cuid2';
|
|
2
|
+
import { unref } from 'vue';
|
|
2
3
|
import { mapState, mapActions } from 'pinia';
|
|
3
4
|
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
|
|
4
5
|
import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';
|
|
5
6
|
import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
|
|
6
7
|
import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
|
|
8
|
+
import { useWidgetGraphStore } from 'Modules/@apostrophecms/ui/stores/widgetGraph';
|
|
7
9
|
import cloneWidget from 'Modules/@apostrophecms/area/lib/clone-widget.js';
|
|
8
10
|
import { klona } from 'klona';
|
|
9
11
|
|
|
10
12
|
export default {
|
|
11
13
|
mixins: [ AposThemeMixin ],
|
|
14
|
+
inject: {
|
|
15
|
+
aposGraphKey: {
|
|
16
|
+
from: 'aposGraphKey',
|
|
17
|
+
default: null
|
|
18
|
+
}
|
|
19
|
+
},
|
|
12
20
|
props: {
|
|
13
21
|
docId: {
|
|
14
22
|
type: String,
|
|
@@ -130,6 +138,35 @@ export default {
|
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
return this.next.findIndex(widget => widget._id === this.focusedWidget);
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Set of widget _ids (from `next`) that should have a raised z-index
|
|
144
|
+
* because they are, or contain, the currently focused widget.
|
|
145
|
+
* Computed once per focusedWidget / graph change; O(depth) ancestor
|
|
146
|
+
* walk + O(1) per-widget lookup in the template.
|
|
147
|
+
*/
|
|
148
|
+
raisedWidgets() {
|
|
149
|
+
const raised = new Set();
|
|
150
|
+
if (!this.focusedWidget) {
|
|
151
|
+
return raised;
|
|
152
|
+
}
|
|
153
|
+
const graphKey = unref(this.aposGraphKey);
|
|
154
|
+
if (!graphKey) {
|
|
155
|
+
// No graph — fall back to exact match only
|
|
156
|
+
if (this.next.some(w => w._id === this.focusedWidget)) {
|
|
157
|
+
raised.add(this.focusedWidget);
|
|
158
|
+
}
|
|
159
|
+
return raised;
|
|
160
|
+
}
|
|
161
|
+
const ancestors = this.storeGetAncestors(graphKey, this.focusedWidget);
|
|
162
|
+
const chain = new Set([ this.focusedWidget, ...ancestors ]);
|
|
163
|
+
|
|
164
|
+
for (const widget of this.next) {
|
|
165
|
+
if (chain.has(widget._id)) {
|
|
166
|
+
raised.add(widget._id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return raised;
|
|
133
170
|
}
|
|
134
171
|
},
|
|
135
172
|
watch: {
|
|
@@ -176,6 +213,9 @@ export default {
|
|
|
176
213
|
methods: {
|
|
177
214
|
...mapActions(useWidgetStore, [ 'setFocusedArea', 'setFocusedWidget' ]),
|
|
178
215
|
...mapActions(useModalStore, [ 'isOnTop' ]),
|
|
216
|
+
...mapActions(useWidgetGraphStore, {
|
|
217
|
+
storeGetAncestors: 'getAncestors'
|
|
218
|
+
}),
|
|
179
219
|
bindEventListeners() {
|
|
180
220
|
apos.bus.$on('area-updated', this.areaUpdatedHandler);
|
|
181
221
|
apos.bus.$on('command-menu-area-copy-widget', this.handleCopy);
|
|
@@ -1187,9 +1187,10 @@ module.exports = {
|
|
|
1187
1187
|
if (!self.shouldRefreshOnRestart()) {
|
|
1188
1188
|
return '';
|
|
1189
1189
|
}
|
|
1190
|
+
const prefix = self.apos.prefix || '';
|
|
1190
1191
|
return self.apos.template.safe(
|
|
1191
|
-
`<script data-apos-refresh-on-restart="${self.action}/restart-id" ` +
|
|
1192
|
-
`src="${self.action}/refresh-on-restart"></script>`
|
|
1192
|
+
`<script data-apos-refresh-on-restart="${prefix}${self.action}/restart-id" ` +
|
|
1193
|
+
`src="${prefix}${self.action}/refresh-on-restart"></script>`
|
|
1193
1194
|
);
|
|
1194
1195
|
},
|
|
1195
1196
|
// Return the URL of the release asset with the given path, taking into
|
|
@@ -124,6 +124,9 @@ module.exports = {
|
|
|
124
124
|
await self.db.createIndex({ archivedDocIds: 1 });
|
|
125
125
|
self.addLegacyMigrations();
|
|
126
126
|
self.addSvgSanitizationMigration();
|
|
127
|
+
|
|
128
|
+
// Lazy cache for types whose schema contains attachment fields.
|
|
129
|
+
self.typesWithAttachmentFields = new Map();
|
|
127
130
|
},
|
|
128
131
|
|
|
129
132
|
tasks(self) {
|
|
@@ -633,6 +636,15 @@ module.exports = {
|
|
|
633
636
|
if (!attachment) {
|
|
634
637
|
return self.getMissingAttachmentUrl();
|
|
635
638
|
}
|
|
639
|
+
// file module supports these, optionally
|
|
640
|
+
// (performance tradeoff). It's not enough to
|
|
641
|
+
// pass the prettyUrl: true option to this method,
|
|
642
|
+
// and that's not necessary. Setting it to false is
|
|
643
|
+
// an internal option for determining the real URL
|
|
644
|
+
// behind the pretty URL.
|
|
645
|
+
if (attachment._prettyUrl && (options.prettyUrl !== false)) {
|
|
646
|
+
return attachment._prettyUrl;
|
|
647
|
+
}
|
|
636
648
|
let path = '/attachments/' + attachment._id + '-' + attachment.name;
|
|
637
649
|
if (!options.uploadfsPath) {
|
|
638
650
|
path = self.uploadfs.getUrl() + path;
|
|
@@ -882,6 +894,264 @@ module.exports = {
|
|
|
882
894
|
}
|
|
883
895
|
}
|
|
884
896
|
},
|
|
897
|
+
// Check whether a schema array (recursively through
|
|
898
|
+
// array and object sub-schemas) contains at least one
|
|
899
|
+
// field with `type: 'attachment'`.
|
|
900
|
+
schemaHasAttachmentField(schema) {
|
|
901
|
+
if (!Array.isArray(schema)) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
for (const field of schema) {
|
|
905
|
+
if (field.type === 'attachment') {
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
if (
|
|
909
|
+
(field.type === 'array' || field.type === 'object') &&
|
|
910
|
+
field.schema
|
|
911
|
+
) {
|
|
912
|
+
if (self.schemaHasAttachmentField(field.schema)) {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
// Return a Set of doc type names whose module schema
|
|
921
|
+
// contains at least one `type: 'attachment'` field.
|
|
922
|
+
// Result is lazy cached.
|
|
923
|
+
hasAttachmentFields(type) {
|
|
924
|
+
if (self.typesWithAttachmentFields.has(type)) {
|
|
925
|
+
return self.typesWithAttachmentFields.get(type);
|
|
926
|
+
}
|
|
927
|
+
const module = self.apos.modules[type];
|
|
928
|
+
if (!module?.schema) {
|
|
929
|
+
self.typesWithAttachmentFields.set(type, false);
|
|
930
|
+
} else {
|
|
931
|
+
self.typesWithAttachmentFields.set(
|
|
932
|
+
type,
|
|
933
|
+
self.schemaHasAttachmentField(module.schema)
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return self.typesWithAttachmentFields.get(type);
|
|
937
|
+
},
|
|
938
|
+
|
|
939
|
+
// Check whether a relationship field's `withType`
|
|
940
|
+
// (which may be a virtual type like
|
|
941
|
+
// `@apostrophecms/any-page-type`) resolves to a type
|
|
942
|
+
// that has attachment fields.
|
|
943
|
+
relationshipHasAttachmentFields(withType) {
|
|
944
|
+
if (self.hasAttachmentFields(withType)) {
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
if (
|
|
948
|
+
withType === '@apostrophecms/any-page-type' ||
|
|
949
|
+
withType === '@apostrophecms/page'
|
|
950
|
+
) {
|
|
951
|
+
const cacheKey = '@apostrophecms/any-page-type';
|
|
952
|
+
if (!self.typesWithAttachmentFields.has(cacheKey)) {
|
|
953
|
+
self.typesWithAttachmentFields.set(
|
|
954
|
+
cacheKey,
|
|
955
|
+
self.apos
|
|
956
|
+
.instancesOf('@apostrophecms/page-type')
|
|
957
|
+
.some(module => self.hasAttachmentFields(module.__meta.name))
|
|
958
|
+
);
|
|
959
|
+
self.typesWithAttachmentFields.set(
|
|
960
|
+
'@apostrophecms/page',
|
|
961
|
+
self.typesWithAttachmentFields.get(cacheKey)
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
return self.typesWithAttachmentFields.get(cacheKey);
|
|
965
|
+
}
|
|
966
|
+
return false;
|
|
967
|
+
},
|
|
968
|
+
|
|
969
|
+
// Collect the full `_id` values (e.g. `abc:en:published`)
|
|
970
|
+
// of docs that are referenced via relationship fields
|
|
971
|
+
// from the given `doc` and belong to types whose schema
|
|
972
|
+
// contains at least one `type: 'attachment'` field.
|
|
973
|
+
//
|
|
974
|
+
// These IDs can be matched against the `docIds` array
|
|
975
|
+
// stored on each attachment record, allowing a "used"
|
|
976
|
+
// scope that only includes attachments actively referenced
|
|
977
|
+
// by published content.
|
|
978
|
+
//
|
|
979
|
+
// Returns an array of full `_id` strings. The method is
|
|
980
|
+
// synchronous because it only inspects the in-memory
|
|
981
|
+
// document data — no database queries are needed.
|
|
982
|
+
collectUsedDocIds(req, doc) {
|
|
983
|
+
const docIds = new Set();
|
|
984
|
+
if (self.hasAttachmentFields(doc.type)) {
|
|
985
|
+
docIds.add(doc.aposDocId);
|
|
986
|
+
}
|
|
987
|
+
const locale = req.locale || self.apos.i18n?.defaultLocale || 'en';
|
|
988
|
+
const mode = req.mode || 'published';
|
|
989
|
+
|
|
990
|
+
self.collectDocAttachmentRefIds(doc, docIds);
|
|
991
|
+
|
|
992
|
+
// Convert aposDocId values to full _id format
|
|
993
|
+
return [ ...docIds ].map(
|
|
994
|
+
aposDocId => `${aposDocId}:${locale}:${mode}`
|
|
995
|
+
);
|
|
996
|
+
},
|
|
997
|
+
|
|
998
|
+
// Given a single document (raw MongoDB data), walk its
|
|
999
|
+
// schema recursively and collect `idsStorage` values from
|
|
1000
|
+
// relationship fields that point to types with attachment
|
|
1001
|
+
// fields. Also handles the special case of rich-text
|
|
1002
|
+
// widget inline images (`imageIds`) and widgets whose own
|
|
1003
|
+
// schema contains a direct `type: 'attachment'` field.
|
|
1004
|
+
//
|
|
1005
|
+
// Found IDs are added to the `target` Set
|
|
1006
|
+
// (aposDocId values, locale-agnostic).
|
|
1007
|
+
collectDocAttachmentRefIds(doc, target) {
|
|
1008
|
+
const handlers = {
|
|
1009
|
+
relationship: (field, contextDoc) => {
|
|
1010
|
+
if (self.relationshipHasAttachmentFields(field.withType)) {
|
|
1011
|
+
for (const id of (contextDoc[field.idsStorage] || [])) {
|
|
1012
|
+
target.add(id);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
// Rich-text widgets store inline image references in
|
|
1017
|
+
// `imageIds` outside of any schema relationship field.
|
|
1018
|
+
// Widgets whose own schema has a direct `type: 'attachment'`
|
|
1019
|
+
// field (e.g. a custom widget storing a file directly) also
|
|
1020
|
+
// need to include the parent doc's ID.
|
|
1021
|
+
widget: (field, widget) => {
|
|
1022
|
+
if (self.hasAttachmentFields(widget.type)) {
|
|
1023
|
+
target.add(doc.aposDocId);
|
|
1024
|
+
}
|
|
1025
|
+
if (
|
|
1026
|
+
widget.type === '@apostrophecms/rich-text' &&
|
|
1027
|
+
Array.isArray(widget.imageIds)
|
|
1028
|
+
) {
|
|
1029
|
+
for (const id of widget.imageIds) {
|
|
1030
|
+
target.add(id);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
self.apos.doc.walkByMetaType(doc, handlers);
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
// Return metadata for attachments suitable for a static build.
|
|
1039
|
+
// Uses batch-of-100 pattern for memory efficiency (see self.each()).
|
|
1040
|
+
//
|
|
1041
|
+
// Options (all optional):
|
|
1042
|
+
//
|
|
1043
|
+
// `docIds`: array of full locale-qualified document `_id`s.
|
|
1044
|
+
// When provided, only attachments whose `docIds` array
|
|
1045
|
+
// intersects with the given list are returned ("used"
|
|
1046
|
+
// scope). When not provided (undefined), all non-archived
|
|
1047
|
+
// attachments are returned ("all" scope).
|
|
1048
|
+
//
|
|
1049
|
+
// Archived attachments are always excluded.
|
|
1050
|
+
//
|
|
1051
|
+
// `sizes`: array of size names to include (e.g. `['full',
|
|
1052
|
+
// 'one-half']`). When provided, only these sizes are
|
|
1053
|
+
// emitted. Ignored for non-sized attachments (SVG, office
|
|
1054
|
+
// files) which always get a single entry with `path` only.
|
|
1055
|
+
//
|
|
1056
|
+
// `skipSizes`: array of size names to exclude. Applied
|
|
1057
|
+
// after `sizes` (or after the full size list when `sizes`
|
|
1058
|
+
// is not given). Common use: `['original']` to skip the
|
|
1059
|
+
// potentially very large original upload.
|
|
1060
|
+
//
|
|
1061
|
+
// Cropped variants, when present on the attachment record,
|
|
1062
|
+
// are also subject to `sizes` and `skipSizes` filtering.
|
|
1063
|
+
//
|
|
1064
|
+
// Returns an array of objects:
|
|
1065
|
+
//
|
|
1066
|
+
// ```js
|
|
1067
|
+
// {
|
|
1068
|
+
// _id: 'abc123',
|
|
1069
|
+
// urls: [
|
|
1070
|
+
// // Sized attachments include a `size` property:
|
|
1071
|
+
// { size: 'full', path: '/attachments/abc-photo.full.jpg' },
|
|
1072
|
+
// { size: 'one-half', path: '/attachments/abc-photo.one-half.jpg' },
|
|
1073
|
+
// // crop variants (same sizes):
|
|
1074
|
+
// { size: 'full', path: '/attachments/abc-photo.10.20.300.400.full.jpg' },
|
|
1075
|
+
// ...
|
|
1076
|
+
// // Non-sized attachments (SVG, office docs) have `path` only:
|
|
1077
|
+
// { path: '/attachments/def-document.pdf' },
|
|
1078
|
+
// ]
|
|
1079
|
+
// }
|
|
1080
|
+
// ```
|
|
1081
|
+
//
|
|
1082
|
+
// `path` is an uploadfs-relative path (no host prefix).
|
|
1083
|
+
async getStaticMetadata({
|
|
1084
|
+
docIds, sizes, skipSizes
|
|
1085
|
+
} = {}) {
|
|
1086
|
+
const criteria = {
|
|
1087
|
+
archived: { $ne: true }
|
|
1088
|
+
};
|
|
1089
|
+
if (Array.isArray(docIds)) {
|
|
1090
|
+
criteria.docIds = { $in: docIds };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const allSizeNames = self.imageSizes.map((s) => s.name)
|
|
1094
|
+
.concat([ 'original' ]);
|
|
1095
|
+
let effectiveSizes;
|
|
1096
|
+
if (Array.isArray(sizes) && sizes.length) {
|
|
1097
|
+
effectiveSizes = sizes.filter((s) => allSizeNames.includes(s));
|
|
1098
|
+
} else {
|
|
1099
|
+
effectiveSizes = [ ...allSizeNames ];
|
|
1100
|
+
}
|
|
1101
|
+
if (Array.isArray(skipSizes) && skipSizes.length) {
|
|
1102
|
+
effectiveSizes = effectiveSizes.filter(
|
|
1103
|
+
(s) => !skipSizes.includes(s)
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const results = [];
|
|
1108
|
+
|
|
1109
|
+
await self.each(criteria, async (attachment) => {
|
|
1110
|
+
const urls = [];
|
|
1111
|
+
const isSized = self.isSized(attachment);
|
|
1112
|
+
|
|
1113
|
+
if (isSized) {
|
|
1114
|
+
// Sized image: emit one entry per requested size
|
|
1115
|
+
for (const size of effectiveSizes) {
|
|
1116
|
+
urls.push({
|
|
1117
|
+
size,
|
|
1118
|
+
path: self.url(attachment, {
|
|
1119
|
+
uploadfsPath: true,
|
|
1120
|
+
crop: false,
|
|
1121
|
+
size
|
|
1122
|
+
})
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
// Crop variants — respect the same size constraints
|
|
1126
|
+
for (const crop of (attachment.crops || [])) {
|
|
1127
|
+
for (const size of effectiveSizes) {
|
|
1128
|
+
urls.push({
|
|
1129
|
+
size,
|
|
1130
|
+
path: self.url(attachment, {
|
|
1131
|
+
uploadfsPath: true,
|
|
1132
|
+
crop,
|
|
1133
|
+
size
|
|
1134
|
+
})
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
} else {
|
|
1139
|
+
// Non-sized (SVG, office docs): path only, no size property
|
|
1140
|
+
urls.push({
|
|
1141
|
+
path: self.url(attachment, { uploadfsPath: true })
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (urls.length) {
|
|
1146
|
+
results.push({
|
|
1147
|
+
_id: attachment._id,
|
|
1148
|
+
urls
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
return results;
|
|
1154
|
+
},
|
|
885
1155
|
// Returns true if, based on the provided attachment object,
|
|
886
1156
|
// a valid focal point has been specified. Useful to avoid
|
|
887
1157
|
// the default of `background-position: center center` if
|
|
@@ -53,7 +53,8 @@ module.exports = {
|
|
|
53
53
|
// a 404 if you request a document you cannot edit.
|
|
54
54
|
async getOne(req, _id) {
|
|
55
55
|
_id = self.apos.i18n.inferIdLocaleAndMode(req, _id);
|
|
56
|
-
const
|
|
56
|
+
const aposDocId = _id.split(':')[0];
|
|
57
|
+
const doc = await self.find(req, { $or: [ { _id }, { _id: aposDocId } ] }).permission('edit').toObject();
|
|
57
58
|
if (!doc) {
|
|
58
59
|
throw self.apos.error('notfound');
|
|
59
60
|
}
|
|
@@ -1794,11 +1795,13 @@ module.exports = {
|
|
|
1794
1795
|
|
|
1795
1796
|
// Iterate through the document fields and call the provided handlers
|
|
1796
1797
|
// for each item of an array, object and relationship field type.
|
|
1798
|
+
// Invoke the `widget` handler for widgets in areas if provided.
|
|
1797
1799
|
walkByMetaType(doc, handlers) {
|
|
1798
1800
|
const defaultHandlers = {
|
|
1799
1801
|
arrayItem: () => {},
|
|
1800
1802
|
object: () => {},
|
|
1801
|
-
relationship: () => {}
|
|
1803
|
+
relationship: () => {},
|
|
1804
|
+
widget: null
|
|
1802
1805
|
};
|
|
1803
1806
|
|
|
1804
1807
|
handlers = {
|
|
@@ -1827,6 +1830,9 @@ module.exports = {
|
|
|
1827
1830
|
for (const field of schema) {
|
|
1828
1831
|
if (field.type === 'area' && doc[field.name] && doc[field.name].items) {
|
|
1829
1832
|
for (const widget of doc[field.name].items) {
|
|
1833
|
+
if (handlers.widget) {
|
|
1834
|
+
handlers.widget(field, widget);
|
|
1835
|
+
}
|
|
1830
1836
|
self.walkByMetaType(widget, handlers);
|
|
1831
1837
|
}
|
|
1832
1838
|
} else if (field.type === 'array') {
|
|
@@ -553,6 +553,9 @@ module.exports = {
|
|
|
553
553
|
// in an autocomplete menu. Default behavior is to
|
|
554
554
|
// return only the `title`, `_id` and `slug` properties.
|
|
555
555
|
// Removing any of these three is not recommended.
|
|
556
|
+
// `aposDocId` is required for building template filters when static
|
|
557
|
+
// url's are enabled, `type` is required for various features including
|
|
558
|
+
// permissions - do not remove these unless you know what you are doing.
|
|
556
559
|
//
|
|
557
560
|
// `query.field` will contain the schema field definition for
|
|
558
561
|
// the relationship the user is attempting to match titles from.
|
|
@@ -564,6 +567,7 @@ module.exports = {
|
|
|
564
567
|
title: 1,
|
|
565
568
|
type: 1,
|
|
566
569
|
_id: 1,
|
|
570
|
+
aposDocId: 1,
|
|
567
571
|
_url: 1,
|
|
568
572
|
slug: 1
|
|
569
573
|
};
|
|
@@ -1648,7 +1652,80 @@ module.exports = {
|
|
|
1648
1652
|
name: key,
|
|
1649
1653
|
...self.columns[key]
|
|
1650
1654
|
}));
|
|
1655
|
+
},
|
|
1656
|
+
|
|
1657
|
+
// Returns an object with `metadata` (array of URL metadata
|
|
1658
|
+
// entries) and `attachmentDocIds` (array of full `_id` strings
|
|
1659
|
+
// for docs referenced via relationships to types with
|
|
1660
|
+
// attachment fields).
|
|
1661
|
+
//
|
|
1662
|
+
// When `options.attachments` is truthy, each document is also
|
|
1663
|
+
// inspected for attachment references via
|
|
1664
|
+
// `attachment.collectUsedDocIds()`, otherwise `attachmentDocIds`
|
|
1665
|
+
// is returned as an empty array.
|
|
1666
|
+
async getAllUrlMetadata(req, { attachments = false } = {}) {
|
|
1667
|
+
const result = {
|
|
1668
|
+
metadata: [],
|
|
1669
|
+
attachmentDocIds: []
|
|
1670
|
+
};
|
|
1671
|
+
let skip = 0;
|
|
1672
|
+
let docs = [];
|
|
1673
|
+
|
|
1674
|
+
do {
|
|
1675
|
+
// Paginate through 100 at a time to avoid exhausting
|
|
1676
|
+
// memory
|
|
1677
|
+
docs = await self.getUrlMetadataQuery(req)
|
|
1678
|
+
.skip(skip)
|
|
1679
|
+
.limit(100)
|
|
1680
|
+
.toArray();
|
|
1681
|
+
await Promise.all(docs.map(async doc => {
|
|
1682
|
+
result.metadata.push(...await self.getUrlMetadata(req, doc));
|
|
1683
|
+
if (attachments) {
|
|
1684
|
+
result.attachmentDocIds.push(
|
|
1685
|
+
...self.apos.attachment.collectUsedDocIds(req, doc)
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
}));
|
|
1689
|
+
skip += docs.length;
|
|
1690
|
+
} while (docs.length > 0);
|
|
1691
|
+
|
|
1692
|
+
return result;
|
|
1693
|
+
},
|
|
1694
|
+
|
|
1695
|
+
// Used to build sitemaps and assist in static site builds. Extend to
|
|
1696
|
+
// return all URLs that provide views of this document and
|
|
1697
|
+
// should be included in sitemaps and static builds. You may
|
|
1698
|
+
// use `async` when extending
|
|
1699
|
+
getUrlMetadata(req, doc) {
|
|
1700
|
+
if (!doc._url) {
|
|
1701
|
+
return [];
|
|
1702
|
+
}
|
|
1703
|
+
return [
|
|
1704
|
+
{
|
|
1705
|
+
url: doc._url,
|
|
1706
|
+
type: doc.type,
|
|
1707
|
+
aposDocId: doc.aposDocId,
|
|
1708
|
+
i18nId: doc.aposDocId,
|
|
1709
|
+
_id: doc._id,
|
|
1710
|
+
// For legacy reasons. Google 100% ignores this
|
|
1711
|
+
changefreq: 'daily',
|
|
1712
|
+
// For legacy reasons. Google 100% ignores this
|
|
1713
|
+
priority: 1.0
|
|
1714
|
+
}
|
|
1715
|
+
];
|
|
1716
|
+
},
|
|
1717
|
+
|
|
1718
|
+
// Extend to change the query used when fetching documents of this type
|
|
1719
|
+
// for purposes of building sitemaps and static sites. Should be efficient
|
|
1720
|
+
// while still capturing enough information to generate all URLs for
|
|
1721
|
+
// this particular type of document. By default no relationships are fetched
|
|
1722
|
+
// and widget loaders for areas are not run
|
|
1723
|
+
getUrlMetadataQuery(req) {
|
|
1724
|
+
return self.find(req, {})
|
|
1725
|
+
.relationships(false)
|
|
1726
|
+
.areas(false);
|
|
1651
1727
|
}
|
|
1728
|
+
|
|
1652
1729
|
};
|
|
1653
1730
|
},
|
|
1654
1731
|
extendMethods(self) {
|
|
@@ -2815,7 +2892,10 @@ module.exports = {
|
|
|
2815
2892
|
const counts = query.get('distinctCounts');
|
|
2816
2893
|
if (counts && ((typeof counts) === 'object')) {
|
|
2817
2894
|
for (const result of results) {
|
|
2818
|
-
|
|
2895
|
+
// For relationship slug builders the value is a slug, but
|
|
2896
|
+
// distinctCounts is keyed by aposDocId (from idsStorage).
|
|
2897
|
+
// Fall back to aposDocId when the value key yields nothing.
|
|
2898
|
+
result.count = counts[result.value] ?? counts[result.aposDocId];
|
|
2819
2899
|
}
|
|
2820
2900
|
}
|
|
2821
2901
|
return results;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
:modal="modal"
|
|
5
5
|
:modal-title="modalTitle"
|
|
6
6
|
:modal-data="modalData"
|
|
7
|
+
:graph-key="graphKey"
|
|
7
8
|
@inactive="modal.active = false"
|
|
8
9
|
@show-modal="modal.showModal = true"
|
|
9
10
|
@esc="confirmAndCancel"
|
|
@@ -134,6 +135,7 @@
|
|
|
134
135
|
|
|
135
136
|
<script>
|
|
136
137
|
import { klona } from 'klona';
|
|
138
|
+
import { computed } from 'vue';
|
|
137
139
|
import { mapActions } from 'pinia';
|
|
138
140
|
import AposModifiedMixin from 'Modules/@apostrophecms/ui/mixins/AposModifiedMixin';
|
|
139
141
|
import AposModalTabsMixin from 'Modules/@apostrophecms/modal/mixins/AposModalTabsMixin';
|
|
@@ -144,6 +146,7 @@ import AposAdvisoryLockMixin from 'Modules/@apostrophecms/ui/mixins/AposAdvisory
|
|
|
144
146
|
import AposDocErrorsMixin from 'Modules/@apostrophecms/modal/mixins/AposDocErrorsMixin';
|
|
145
147
|
import { detectDocChange } from 'Modules/@apostrophecms/schema/lib/detectChange';
|
|
146
148
|
import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
|
|
149
|
+
import { useWidgetGraphStore } from 'Modules/@apostrophecms/ui/stores/widgetGraph.js';
|
|
147
150
|
|
|
148
151
|
export default {
|
|
149
152
|
name: 'AposDocEditor',
|
|
@@ -156,10 +159,11 @@ export default {
|
|
|
156
159
|
AposArchiveMixin,
|
|
157
160
|
AposDocErrorsMixin
|
|
158
161
|
],
|
|
159
|
-
provide
|
|
162
|
+
provide() {
|
|
160
163
|
return {
|
|
161
164
|
originalDoc: this.originalDoc,
|
|
162
|
-
liveOriginalDoc: this.docFields
|
|
165
|
+
liveOriginalDoc: this.docFields,
|
|
166
|
+
aposGraphKey: computed(() => this.graphKey)
|
|
163
167
|
};
|
|
164
168
|
},
|
|
165
169
|
props: {
|
|
@@ -322,6 +326,11 @@ export default {
|
|
|
322
326
|
type: this.$t(this.moduleOptions.label)
|
|
323
327
|
};
|
|
324
328
|
},
|
|
329
|
+
graphKey() {
|
|
330
|
+
return this.modalData?.id && this.currentId
|
|
331
|
+
? `${this.modalData.id}:${this.currentId}`
|
|
332
|
+
: null;
|
|
333
|
+
},
|
|
325
334
|
saveLabel() {
|
|
326
335
|
if (this.restoreOnly) {
|
|
327
336
|
return 'apostrophe:restore';
|
|
@@ -417,9 +426,16 @@ export default {
|
|
|
417
426
|
},
|
|
418
427
|
unmounted() {
|
|
419
428
|
apos.bus.$off('content-changed', this.onContentChanged);
|
|
429
|
+
// Destroy the widget graph for this modal's editing context
|
|
430
|
+
if (this.graphKey) {
|
|
431
|
+
this.storeDestroyGraph(this.graphKey);
|
|
432
|
+
}
|
|
420
433
|
},
|
|
421
434
|
methods: {
|
|
422
435
|
...mapActions(useModalStore, [ 'updateModalData' ]),
|
|
436
|
+
...mapActions(useWidgetGraphStore, {
|
|
437
|
+
storeDestroyGraph: 'destroyGraph'
|
|
438
|
+
}),
|
|
423
439
|
async instantiateExistingDoc() {
|
|
424
440
|
await this.loadDoc();
|
|
425
441
|
this.evaluateConditions();
|
|
@@ -272,6 +272,11 @@ module.exports = {
|
|
|
272
272
|
return res.status(403).send('forbidden');
|
|
273
273
|
}
|
|
274
274
|
req.aposExternalFront = true;
|
|
275
|
+
if (req.headers['x-apos-static-base-url'] === '1') {
|
|
276
|
+
// Downstream code (page.getBaseUrl) checks this to decide
|
|
277
|
+
// which base URL to use for this request.
|
|
278
|
+
self.applyStaticBuildHeaders(req);
|
|
279
|
+
}
|
|
275
280
|
res.redirect = function(...args) {
|
|
276
281
|
// The external front end needs to issue the actual redirect,
|
|
277
282
|
// not us
|
|
@@ -290,6 +295,20 @@ module.exports = {
|
|
|
290
295
|
};
|
|
291
296
|
return next();
|
|
292
297
|
},
|
|
298
|
+
// Allow direct API calls (without externalFrontKey) to opt into
|
|
299
|
+
// static-build URL behavior by sending x-apos-static-base-url: 1.
|
|
300
|
+
// This is harmless — it only changes how URLs are built in the
|
|
301
|
+
// response, granting no elevated permissions.
|
|
302
|
+
staticBuildHeader: {
|
|
303
|
+
url: '/api/v1',
|
|
304
|
+
middleware(req, res, next) {
|
|
305
|
+
// Only act if not already set by externalFront middleware
|
|
306
|
+
if (!req.aposStaticBuild && req.headers['x-apos-static-base-url'] === '1') {
|
|
307
|
+
self.applyStaticBuildHeaders(req);
|
|
308
|
+
}
|
|
309
|
+
return next();
|
|
310
|
+
}
|
|
311
|
+
},
|
|
293
312
|
attachUtilityMethods(req, res, next) {
|
|
294
313
|
// We apply the super pattern variously to res.redirect,
|
|
295
314
|
// make sure the original version is always available
|
|
@@ -385,7 +404,7 @@ module.exports = {
|
|
|
385
404
|
// for the token to be usable to log in.
|
|
386
405
|
$or: [
|
|
387
406
|
{ requirementsToVerify: { $exists: false } },
|
|
388
|
-
{ requirementsToVerify: { $
|
|
407
|
+
{ requirementsToVerify: { $size: 0 } }
|
|
389
408
|
]
|
|
390
409
|
});
|
|
391
410
|
return bearer && bearer.userId;
|
|
@@ -460,6 +479,16 @@ module.exports = {
|
|
|
460
479
|
}
|
|
461
480
|
},
|
|
462
481
|
|
|
482
|
+
applyStaticBuildHeaders(req) {
|
|
483
|
+
if (req.aposStaticBuild) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
req.aposStaticBuild = true;
|
|
487
|
+
if (self.apos.staticBaseUrl) {
|
|
488
|
+
req.staticBaseUrl = self.apos.staticBaseUrl;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
|
|
463
492
|
logAllRoutes() {
|
|
464
493
|
const superUse = self.apos.app.use.bind(self.apos.app);
|
|
465
494
|
const methods = [ 'get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all' ];
|