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
|
@@ -1261,6 +1261,17 @@ module.exports = {
|
|
|
1261
1261
|
result = await actionModule.update(toReq, update);
|
|
1262
1262
|
}
|
|
1263
1263
|
|
|
1264
|
+
// Record when this document was localized. Uses a direct DB
|
|
1265
|
+
// update so the timestamp survives regardless of the schema
|
|
1266
|
+
// convert pipeline. Covers both single and batch localization
|
|
1267
|
+
// (localizeBatch calls this method per document).
|
|
1268
|
+
const localizedAt = new Date();
|
|
1269
|
+
await self.apos.doc.db.updateOne(
|
|
1270
|
+
{ _id: result._id },
|
|
1271
|
+
{ $set: { localizedAt } }
|
|
1272
|
+
);
|
|
1273
|
+
result.localizedAt = localizedAt;
|
|
1274
|
+
|
|
1264
1275
|
await self.emit('afterLocalize', req, draft, result, eventOptions);
|
|
1265
1276
|
|
|
1266
1277
|
return result;
|
|
@@ -1610,6 +1621,59 @@ module.exports = {
|
|
|
1610
1621
|
return doc;
|
|
1611
1622
|
},
|
|
1612
1623
|
|
|
1624
|
+
// Returns true if the named filter is permitted to expose distinct
|
|
1625
|
+
// values through the `choices` / `counts` query builders given the
|
|
1626
|
+
// publicApiProjection. When no publicApiProjection is in effect
|
|
1627
|
+
// (authenticated API callers), all fields are permitted.
|
|
1628
|
+
//
|
|
1629
|
+
// This guards against leaking distinct values of fields excluded by
|
|
1630
|
+
// `publicApiProjection`, since MongoDB's `distinct` operator ignores
|
|
1631
|
+
// projections. Projections set explicitly by authenticated users are
|
|
1632
|
+
// not restricted — they are a voluntary narrowing of query results,
|
|
1633
|
+
// not a security boundary.
|
|
1634
|
+
choicesFieldAllowedByProjection(filter, projection) {
|
|
1635
|
+
if (!projection || !Object.keys(projection).length) {
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
// Builders that aren't named after a top-level schema field are
|
|
1639
|
+
// not gated by the projection.
|
|
1640
|
+
const field = self.schema.find(f => f.name === filter);
|
|
1641
|
+
if (!field) {
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
const topLevel = filter.split('.')[0];
|
|
1645
|
+
const values = Object.values(projection);
|
|
1646
|
+
const hasInclusion = values.some(v => v && v !== 0);
|
|
1647
|
+
const hasExclusion = values.some(v => v === 0 || v === false);
|
|
1648
|
+
if (hasInclusion) {
|
|
1649
|
+
// Inclusion projection: field must be explicitly included.
|
|
1650
|
+
return Boolean(projection[topLevel]);
|
|
1651
|
+
}
|
|
1652
|
+
if (hasExclusion) {
|
|
1653
|
+
// Exclusion projection: field must not be explicitly excluded.
|
|
1654
|
+
return projection[topLevel] !== 0 && projection[topLevel] !== false;
|
|
1655
|
+
}
|
|
1656
|
+
return true;
|
|
1657
|
+
},
|
|
1658
|
+
|
|
1659
|
+
// Returns true if the current user is permitted to view the named
|
|
1660
|
+
// schema field according to any schema-level `viewPermission`.
|
|
1661
|
+
// Non-schema filters are permitted. A dot-notated filter is matched
|
|
1662
|
+
// against the top-level schema field, since `viewPermission` is
|
|
1663
|
+
// declared on top-level fields (see `removeForbiddenFields`).
|
|
1664
|
+
choicesFieldAllowedByViewPermission(req, filter) {
|
|
1665
|
+
const topLevel = filter.split('.')[0];
|
|
1666
|
+
const field = self.schema.find(f => f.name === topLevel);
|
|
1667
|
+
if (!field || !field.viewPermission) {
|
|
1668
|
+
return true;
|
|
1669
|
+
}
|
|
1670
|
+
return self.apos.permission.can(
|
|
1671
|
+
req,
|
|
1672
|
+
field.viewPermission.action,
|
|
1673
|
+
field.viewPermission.type
|
|
1674
|
+
);
|
|
1675
|
+
},
|
|
1676
|
+
|
|
1613
1677
|
composeFilters() {
|
|
1614
1678
|
// TODO: keep in sync with page/index.js composeFilters
|
|
1615
1679
|
self.filters = Object.entries(self.filters)
|
|
@@ -2157,24 +2221,6 @@ module.exports = {
|
|
|
2157
2221
|
}
|
|
2158
2222
|
},
|
|
2159
2223
|
|
|
2160
|
-
// `.attachments(true)` annotates all attachment fields in the
|
|
2161
|
-
// returned documents with URLs as documented for the
|
|
2162
|
-
// `apos.attachment.all` method. Used by our REST APIs.
|
|
2163
|
-
|
|
2164
|
-
attachments: {
|
|
2165
|
-
def: true,
|
|
2166
|
-
after(results) {
|
|
2167
|
-
const attachments = query.get('attachments');
|
|
2168
|
-
|
|
2169
|
-
if (attachments) {
|
|
2170
|
-
self.apos.attachment.all(results, { annotate: true });
|
|
2171
|
-
}
|
|
2172
|
-
},
|
|
2173
|
-
launder(b) {
|
|
2174
|
-
return self.apos.launder.boolean(b);
|
|
2175
|
-
}
|
|
2176
|
-
},
|
|
2177
|
-
|
|
2178
2224
|
// `.autocomplete('sta')` limits results to docs which are a good match
|
|
2179
2225
|
// for a partial string beginning with `sta`, for instance `station`.
|
|
2180
2226
|
// Appropriate words must exist in the title or other text schema fields
|
|
@@ -2427,6 +2473,28 @@ module.exports = {
|
|
|
2427
2473
|
}
|
|
2428
2474
|
},
|
|
2429
2475
|
|
|
2476
|
+
// `.attachments(true)` annotates all attachment fields in the
|
|
2477
|
+
// returned documents with URLs as documented for the
|
|
2478
|
+
// `apos.attachment.all` method. Used by our REST APIs.
|
|
2479
|
+
//
|
|
2480
|
+
// Must appear after the `relationships` builder so that
|
|
2481
|
+
// relationship `_fields` (e.g. crop coordinates) are
|
|
2482
|
+
// available when `attachment.all` generates `_urls`.
|
|
2483
|
+
|
|
2484
|
+
attachments: {
|
|
2485
|
+
def: true,
|
|
2486
|
+
after(results) {
|
|
2487
|
+
const attachments = query.get('attachments');
|
|
2488
|
+
|
|
2489
|
+
if (attachments) {
|
|
2490
|
+
self.apos.attachment.all(results, { annotate: true });
|
|
2491
|
+
}
|
|
2492
|
+
},
|
|
2493
|
+
launder(b) {
|
|
2494
|
+
return self.apos.launder.boolean(b);
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
|
|
2430
2498
|
// `.addUrls(true)`. Invokes the `addUrls` method of all doc type
|
|
2431
2499
|
// managers with relevant docs among the results, if they have one.
|
|
2432
2500
|
//
|
|
@@ -2450,20 +2518,47 @@ module.exports = {
|
|
|
2450
2518
|
if (!val) {
|
|
2451
2519
|
return;
|
|
2452
2520
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
byType
|
|
2456
|
-
|
|
2521
|
+
|
|
2522
|
+
async function addUrlsByType(reqForUrls, docs) {
|
|
2523
|
+
const byType = {};
|
|
2524
|
+
for (const doc of docs) {
|
|
2525
|
+
byType[doc.type] = byType[doc.type] || [];
|
|
2526
|
+
byType[doc.type].push(doc);
|
|
2527
|
+
}
|
|
2528
|
+
for (const type of Object.keys(byType)) {
|
|
2529
|
+
const manager = self.apos.doc.getManager(type);
|
|
2530
|
+
if (manager?.addUrls) {
|
|
2531
|
+
await manager.addUrls(reqForUrls, byType[type]);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2457
2534
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2535
|
+
|
|
2536
|
+
// When locale(null) was used, docs may span multiple
|
|
2537
|
+
// locales. Group by locale and resolve URLs with a
|
|
2538
|
+
// locale-appropriate req so each doc gets the correct
|
|
2539
|
+
// _url for its own locale.
|
|
2540
|
+
if (query.get('locale') === null) {
|
|
2541
|
+
const locales = self.apos.i18n.locales;
|
|
2542
|
+
const byLocale = {};
|
|
2543
|
+
for (const doc of results) {
|
|
2544
|
+
const locale =
|
|
2545
|
+
doc.aposLocale?.split(':')[0] || req.locale;
|
|
2546
|
+
if (!locales[locale]) {
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
byLocale[locale] = byLocale[locale] || [];
|
|
2550
|
+
byLocale[locale].push(doc);
|
|
2551
|
+
}
|
|
2552
|
+
for (const [ locale, localeDocs ] of Object.entries(byLocale)) {
|
|
2553
|
+
const localeReq = locale === req.locale
|
|
2554
|
+
? req
|
|
2555
|
+
: req.clone({ locale });
|
|
2556
|
+
await addUrlsByType(localeReq, localeDocs);
|
|
2557
|
+
}
|
|
2558
|
+
return;
|
|
2466
2559
|
}
|
|
2560
|
+
|
|
2561
|
+
await addUrlsByType(req, results);
|
|
2467
2562
|
}
|
|
2468
2563
|
},
|
|
2469
2564
|
|
|
@@ -2510,10 +2605,34 @@ module.exports = {
|
|
|
2510
2605
|
pageUrl: {
|
|
2511
2606
|
def: true,
|
|
2512
2607
|
after(results) {
|
|
2608
|
+
const req = query.req;
|
|
2609
|
+
const crossLocale = query.get('locale') === null;
|
|
2610
|
+
const localeReqs = {};
|
|
2513
2611
|
for (const result of results) {
|
|
2514
|
-
if (
|
|
2515
|
-
result.
|
|
2612
|
+
if (
|
|
2613
|
+
(!result.archived) &&
|
|
2614
|
+
result.slug &&
|
|
2615
|
+
self.apos.page.isPage(result)
|
|
2616
|
+
) {
|
|
2617
|
+
const urlReq = getReqForLocale(result);
|
|
2618
|
+
result._url = `${urlReq.prefix}${result.slug}`;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function getReqForLocale(doc) {
|
|
2622
|
+
if (!crossLocale || !doc.aposLocale) {
|
|
2623
|
+
return req;
|
|
2624
|
+
}
|
|
2625
|
+
const locale = doc.aposLocale.split(':')[0];
|
|
2626
|
+
if (!self.apos.i18n.locales[locale]) {
|
|
2627
|
+
return req;
|
|
2628
|
+
}
|
|
2629
|
+
if (!localeReqs[locale]) {
|
|
2630
|
+
localeReqs[locale] =
|
|
2631
|
+
locale === req.locale
|
|
2632
|
+
? req
|
|
2633
|
+
: req.clone({ locale });
|
|
2516
2634
|
}
|
|
2635
|
+
return localeReqs[locale];
|
|
2517
2636
|
}
|
|
2518
2637
|
}
|
|
2519
2638
|
},
|
|
@@ -2641,6 +2760,7 @@ module.exports = {
|
|
|
2641
2760
|
const choices = {};
|
|
2642
2761
|
const baseQuery = query.get('choices-query-prefinalize');
|
|
2643
2762
|
baseQuery.set('choices-query-prefinalize', null);
|
|
2763
|
+
const publicApiProjection = query.get('publicApiProjection');
|
|
2644
2764
|
for (const filter of filters) {
|
|
2645
2765
|
// The choices for each filter should reflect the effect of all
|
|
2646
2766
|
// filters except this one (filtering by topic pairs down the list
|
|
@@ -2656,6 +2776,19 @@ module.exports = {
|
|
|
2656
2776
|
if (!query.builders[filter].launder) {
|
|
2657
2777
|
continue;
|
|
2658
2778
|
}
|
|
2779
|
+
// Do not leak distinct values of fields excluded by the
|
|
2780
|
+
// publicApiProjection. MongoDB's `distinct` ignores projections,
|
|
2781
|
+
// so we must enforce this ourselves here. Only applies to the
|
|
2782
|
+
// public API; authenticated users who set their own projections
|
|
2783
|
+
// are not restricted.
|
|
2784
|
+
if (!self.choicesFieldAllowedByProjection(filter, publicApiProjection)) {
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
// Do not leak distinct values of fields the current user does
|
|
2788
|
+
// not have permission to view via a schema-level viewPermission.
|
|
2789
|
+
if (!self.choicesFieldAllowedByViewPermission(query.req, filter)) {
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2659
2792
|
// Now shut it off
|
|
2660
2793
|
_query[filter](null);
|
|
2661
2794
|
choices[filter] = await _query.toChoices(filter, { counts: query.get('counts') });
|
|
@@ -92,6 +92,16 @@ export default {
|
|
|
92
92
|
moduleLabels: {
|
|
93
93
|
type: Object,
|
|
94
94
|
default: null
|
|
95
|
+
},
|
|
96
|
+
showUnpublish: {
|
|
97
|
+
type: Boolean,
|
|
98
|
+
default() {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
crossLocale: {
|
|
103
|
+
type: Boolean,
|
|
104
|
+
default: false
|
|
95
105
|
}
|
|
96
106
|
},
|
|
97
107
|
emits: [ 'menu-open', 'menu-close', 'close' ],
|
|
@@ -105,6 +115,12 @@ export default {
|
|
|
105
115
|
},
|
|
106
116
|
computed: {
|
|
107
117
|
menu() {
|
|
118
|
+
// TODO: Register core context actions (edit, preview,
|
|
119
|
+
// copy, localize, archive, etc.) as custom operations
|
|
120
|
+
// via `addContextOperation`. This would let us unify
|
|
121
|
+
// cross-locale gating through the `crossLocale`
|
|
122
|
+
// flag on each operation, eliminating the manual
|
|
123
|
+
// `!this.crossLocale` wrapping below.
|
|
108
124
|
const menu = [
|
|
109
125
|
// TODO
|
|
110
126
|
// ...(this.isModifiedFromPublished ? [
|
|
@@ -128,64 +144,76 @@ export default {
|
|
|
128
144
|
}
|
|
129
145
|
]
|
|
130
146
|
: []),
|
|
131
|
-
|
|
147
|
+
// In cross-locale mode, only Edit and Preview are safe.
|
|
148
|
+
// All other built-in ops are hidden.
|
|
149
|
+
...(!this.crossLocale
|
|
132
150
|
? [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
...((this.showDismissSubmission && this.canDismissSubmission)
|
|
152
|
+
? [
|
|
153
|
+
{
|
|
154
|
+
label: 'apostrophe:dismissSubmission',
|
|
155
|
+
action: 'dismissSubmission'
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
: []),
|
|
159
|
+
...(this.showCopy && this.canCopy
|
|
160
|
+
? [
|
|
161
|
+
{
|
|
162
|
+
label: 'apostrophe:duplicate',
|
|
163
|
+
action: 'copy'
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
: []),
|
|
167
|
+
...(this.canLocalize
|
|
168
|
+
? [
|
|
169
|
+
{
|
|
170
|
+
label: 'apostrophe:localize',
|
|
171
|
+
action: 'localize'
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
: [])
|
|
153
175
|
]
|
|
154
176
|
: []),
|
|
155
177
|
...this.customMenusByContext,
|
|
156
|
-
...(
|
|
178
|
+
...(!this.crossLocale
|
|
157
179
|
? [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
180
|
+
...((this.showDiscardDraft && this.canDiscardDraft)
|
|
181
|
+
? [
|
|
182
|
+
{
|
|
183
|
+
label: this.context.lastPublishedAt
|
|
184
|
+
? 'apostrophe:discardDraft'
|
|
185
|
+
: 'apostrophe:deleteDraft',
|
|
186
|
+
action: 'discardDraft',
|
|
187
|
+
modifiers: [ 'danger' ]
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
: []),
|
|
191
|
+
...(this.showArchive && this.canArchive
|
|
192
|
+
? [
|
|
193
|
+
{
|
|
194
|
+
label: 'apostrophe:archive',
|
|
195
|
+
action: 'archive',
|
|
196
|
+
modifiers: [ 'danger' ]
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
: []),
|
|
200
|
+
...(this.showUnpublish && this.canUnpublish
|
|
201
|
+
? [
|
|
202
|
+
{
|
|
203
|
+
label: 'apostrophe:unpublish',
|
|
204
|
+
action: 'unpublish',
|
|
205
|
+
modifiers: [ 'danger' ]
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
: []),
|
|
209
|
+
...(this.showRestore && this.canRestore
|
|
210
|
+
? [
|
|
211
|
+
{
|
|
212
|
+
label: 'apostrophe:restore',
|
|
213
|
+
action: 'restore'
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
: [])
|
|
189
217
|
]
|
|
190
218
|
: [])
|
|
191
219
|
];
|
|
@@ -208,8 +236,14 @@ export default {
|
|
|
208
236
|
},
|
|
209
237
|
customOperationsByContext() {
|
|
210
238
|
return this.customOperations.filter(({
|
|
211
|
-
manuallyPublished, hasUrl, conditions, context,
|
|
239
|
+
manuallyPublished, hasUrl, conditions, context,
|
|
240
|
+
if: ifProps, moduleIf, crossLocale
|
|
212
241
|
}) => {
|
|
242
|
+
// In cross-locale mode, only show operations explicitly marked as safe
|
|
243
|
+
if (this.crossLocale && !crossLocale) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
213
247
|
if (typeof manuallyPublished === 'boolean' && manuallyPublished !== this.manuallyPublished) {
|
|
214
248
|
return false;
|
|
215
249
|
}
|
|
@@ -433,13 +467,19 @@ export default {
|
|
|
433
467
|
this[item.action](this.context);
|
|
434
468
|
},
|
|
435
469
|
async edit(doc) {
|
|
470
|
+
const props = {
|
|
471
|
+
moduleName: this.moduleName,
|
|
472
|
+
docId: doc._id,
|
|
473
|
+
type: doc.type
|
|
474
|
+
};
|
|
475
|
+
// Cross-locale mode suported.
|
|
476
|
+
if (this.crossLocale && doc.aposLocale) {
|
|
477
|
+
props.locale = doc.aposLocale.split(':')[0];
|
|
478
|
+
}
|
|
436
479
|
await apos.modal.execute(
|
|
437
480
|
doc._aposEditorModal || this.moduleOptions.components.editorModal,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
docId: doc._id,
|
|
441
|
-
type: doc.type
|
|
442
|
-
});
|
|
481
|
+
props
|
|
482
|
+
);
|
|
443
483
|
},
|
|
444
484
|
preview(doc) {
|
|
445
485
|
window.open(doc._url, '_blank').focus();
|
|
@@ -490,6 +530,11 @@ export default {
|
|
|
490
530
|
...docProps(doc),
|
|
491
531
|
...operation.props
|
|
492
532
|
};
|
|
533
|
+
// In cross-locale mode, pass the doc's locale so the modal
|
|
534
|
+
// opens in the correct locale context.
|
|
535
|
+
if (this.crossLocale && doc.aposLocale) {
|
|
536
|
+
props.locale = doc.aposLocale.split(':')[0];
|
|
537
|
+
}
|
|
493
538
|
if (operation.type === 'event') {
|
|
494
539
|
apos.bus.$emit(operation.action, props);
|
|
495
540
|
return;
|
|
@@ -105,19 +105,112 @@ module.exports = {
|
|
|
105
105
|
attachmentDocIds: []
|
|
106
106
|
};
|
|
107
107
|
},
|
|
108
|
-
|
|
108
|
+
// Returns the pretty URL base path (e.g. `/files`) when
|
|
109
|
+
// pretty URLs are enabled, or `null` otherwise.
|
|
110
|
+
// The returned path is prefix-qualified. If `options.relative`
|
|
111
|
+
// is true, the path is relative (no origin).
|
|
112
|
+
getPrettyUrlBase(req, { relative = false } = {}) {
|
|
113
|
+
if (!self.options.prettyUrls) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const base = relative
|
|
117
|
+
? self.apos.url.getBaseUrl(req, {
|
|
118
|
+
relative: true,
|
|
119
|
+
localePrefix: true
|
|
120
|
+
})
|
|
121
|
+
: req.prefix;
|
|
122
|
+
return `${base}${self.options.prettyUrlDir}`;
|
|
123
|
+
},
|
|
124
|
+
// Returns the pretty-URL path component for a file doc.
|
|
125
|
+
// E.g. `/my-document.pdf` — just the filename portion,
|
|
126
|
+
// without any base, prefix, or origin.
|
|
127
|
+
// Returns `null` when pretty URLs are disabled or the
|
|
128
|
+
// doc has no attachment. Works with any object that has
|
|
129
|
+
// `slug` and `attachment.extension` (including the
|
|
130
|
+
// lightweight projections used by `applyPrettyUrlPaths`).
|
|
131
|
+
getPrettyPath(doc) {
|
|
132
|
+
if (!self.options.prettyUrls || !doc.attachment) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const slug = doc.slug.replace(self.options.slugPrefix || '', '');
|
|
136
|
+
return `/${slug}.${doc.attachment.extension}`;
|
|
137
|
+
},
|
|
138
|
+
// When `options.relative` is true, pretty URLs are built
|
|
139
|
+
// with only the prefix + prettyUrlDir (no origin).
|
|
140
|
+
addUrls(req, files, { relative = false } = {}) {
|
|
141
|
+
const prettyBase = self.getPrettyUrlBase(req, { relative });
|
|
109
142
|
for (const file of files) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const { extension } = file.attachment;
|
|
114
|
-
const baseUrl = self.apos.url.getBaseUrl(req, { prefix: true });
|
|
115
|
-
file._url = `${baseUrl}${self.options.prettyUrlDir}/${file.slug.replace(self.options.slugPrefix || '', '')}.${extension}`;
|
|
143
|
+
const prettyPath = self.getPrettyPath(file);
|
|
144
|
+
if (prettyBase && prettyPath) {
|
|
145
|
+
file._url = `${prettyBase}${prettyPath}`;
|
|
116
146
|
file.attachment._prettyUrl = file._url;
|
|
117
|
-
} else {
|
|
147
|
+
} else if (file.attachment) {
|
|
118
148
|
file._url = self.apos.attachment.url(file.attachment);
|
|
119
149
|
}
|
|
120
150
|
}
|
|
151
|
+
},
|
|
152
|
+
// Mutates `attachmentMeta.results` in-place, replacing
|
|
153
|
+
// uploadfs paths with pretty URL paths for file pieces
|
|
154
|
+
// that have pretty URLs enabled. Adds a `base` property
|
|
155
|
+
// to each affected entry so the consumer can override the
|
|
156
|
+
// global `uploadsUrl` for those entries.
|
|
157
|
+
//
|
|
158
|
+
// Uses a lightweight direct DB query (no builders) to
|
|
159
|
+
// find file docs by attachment ID. Returns nothing —
|
|
160
|
+
// the mutation is performed on the input object.
|
|
161
|
+
//
|
|
162
|
+
// Does nothing when pretty URLs are disabled.
|
|
163
|
+
// For the shape of the `attachmentMeta` object, see the
|
|
164
|
+
// `@apostrophecms/url:getAllAttachmentMetadata` method documentation.
|
|
165
|
+
async applyPrettyUrlPaths(req, attachmentMeta) {
|
|
166
|
+
if (!self.options.prettyUrls || !attachmentMeta?.results?.length) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const base = self.getPrettyUrlBase(req, { relative: true });
|
|
171
|
+
const batchSize = 100;
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < attachmentMeta.results.length; i += batchSize) {
|
|
174
|
+
const batch = attachmentMeta.results.slice(i, i + batchSize);
|
|
175
|
+
const batchIds = batch.map(a => a._id);
|
|
176
|
+
|
|
177
|
+
// Lightweight direct query — no builders, no relationship
|
|
178
|
+
// resolution. Only needs slug and attachment ID/extension.
|
|
179
|
+
const fileDocs = await self.apos.doc.db.find({
|
|
180
|
+
type: self.options.name,
|
|
181
|
+
'attachment._id': { $in: batchIds },
|
|
182
|
+
aposLocale: `${req.locale}:${req.mode}`,
|
|
183
|
+
archived: { $ne: true }
|
|
184
|
+
}, {
|
|
185
|
+
projection: {
|
|
186
|
+
slug: 1,
|
|
187
|
+
'attachment._id': 1,
|
|
188
|
+
'attachment.extension': 1
|
|
189
|
+
}
|
|
190
|
+
}).toArray();
|
|
191
|
+
|
|
192
|
+
if (!fileDocs.length) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const byAttId = new Map();
|
|
197
|
+
for (const entry of batch) {
|
|
198
|
+
byAttId.set(entry._id, entry);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const doc of fileDocs) {
|
|
202
|
+
const entry = byAttId.get(doc.attachment._id);
|
|
203
|
+
if (!entry) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const prettyPath = self.getPrettyPath(doc);
|
|
207
|
+
if (!prettyPath) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
entry.urls = [ { path: prettyPath } ];
|
|
211
|
+
entry.base = base;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
121
214
|
}
|
|
122
215
|
};
|
|
123
216
|
},
|
|
@@ -155,7 +248,14 @@ module.exports = {
|
|
|
155
248
|
const uglyUrl = self.apos.attachment.url(file.attachment, {
|
|
156
249
|
prettyUrl: false
|
|
157
250
|
});
|
|
158
|
-
|
|
251
|
+
// For relative URLs (local uploadfs, not CDN), resolve
|
|
252
|
+
// against the current server's origin so the proxy can
|
|
253
|
+
// make the self-request. During static builds
|
|
254
|
+
// `attachment.url()` may return only a path.
|
|
255
|
+
const proxyUrl = uglyUrl.startsWith('/')
|
|
256
|
+
? `${req.protocol}://${req.get('host')}${uglyUrl}`
|
|
257
|
+
: uglyUrl;
|
|
258
|
+
return await streamProxy(req, proxyUrl, { error: self.apos.util.error });
|
|
159
259
|
} catch (e) {
|
|
160
260
|
self.apos.util.error('Error in pretty URL route:', e);
|
|
161
261
|
return res.status(500).send('error');
|
|
@@ -71,7 +71,6 @@
|
|
|
71
71
|
"batchRestoreProgress": "Wiederherstellung {{ type }}...",
|
|
72
72
|
"batchTagProgress": "Taggen von {{ count }} {{ type }}...",
|
|
73
73
|
"batchUntagProgress": "Entfernen der Markierung von {{ count }} {{ type }}...",
|
|
74
|
-
"batchRestoreProgress": "Wiederherstellung {{ type }}...",
|
|
75
74
|
"boxFieldAriaLabelAll": "Wert - alle Seiten",
|
|
76
75
|
"boxFieldAriaLabelIndividual": "- {{ side }} Wert",
|
|
77
76
|
"boxFieldEditAll": "Alle Werte bearbeiten",
|
|
@@ -325,7 +324,6 @@
|
|
|
325
324
|
"localizeLocalized": "Lokalisierte",
|
|
326
325
|
"localizeNewRelated": "Übersetze alle neuen zugehörige Dokumente",
|
|
327
326
|
"localizeNotYetLocalized": "Noch nicht lokalisiert",
|
|
328
|
-
"localized": "{{ type }} {{ title }} auf {{ locale }} übersetzt",
|
|
329
327
|
"localizedBatch": "Verarbeitet {{ count }} {{ type }}",
|
|
330
328
|
"localizedBatchFailed": "Lokalisierung von {{ type }} fehlgeschlagen",
|
|
331
329
|
"localizedBatchWithFailures": "Verarbeitet {{ count }} {{ type }} ({{ bad }} von {{ total }} fehlgeschlagen)",
|