apostrophe 4.22.0 → 4.23.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/defaults.js +2 -0
  3. package/index.js +16 -6
  4. package/lib/image.js +48 -0
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +7 -3
  6. package/modules/@apostrophecms/area/index.js +98 -22
  7. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +16 -4
  8. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -697
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +587 -278
  10. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +416 -0
  11. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbSwitch.vue +212 -0
  12. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +97 -110
  13. package/modules/@apostrophecms/area/ui/apos/lib/operations.js +22 -0
  14. package/modules/@apostrophecms/area/ui/apos/lib/package.json +4 -0
  15. package/modules/@apostrophecms/area/ui/apos/lib/walk-widgets.js +57 -0
  16. package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +642 -0
  17. package/modules/@apostrophecms/area/views/area.html +39 -9
  18. package/modules/@apostrophecms/asset/lib/build/internals.js +1 -1
  19. package/modules/@apostrophecms/asset/lib/globalIcons.js +11 -0
  20. package/modules/@apostrophecms/color-field/index.js +1 -5
  21. package/modules/@apostrophecms/color-field/ui/apos/components/AposColor.vue +2 -26
  22. package/modules/@apostrophecms/color-field/ui/apos/components/AposInputColor.vue +86 -18
  23. package/modules/@apostrophecms/color-field/ui/apos/logic/AposInputColor.js +14 -1
  24. package/modules/@apostrophecms/color-field/ui/apos/logic/finalOptions.js +28 -0
  25. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocLocalePicker.vue +5 -0
  26. package/modules/@apostrophecms/express/index.js +1 -0
  27. package/modules/@apostrophecms/i18n/i18n/de.json +2 -0
  28. package/modules/@apostrophecms/i18n/i18n/en.json +22 -0
  29. package/modules/@apostrophecms/i18n/i18n/es.json +2 -0
  30. package/modules/@apostrophecms/i18n/i18n/fr.json +2 -0
  31. package/modules/@apostrophecms/i18n/i18n/it.json +2 -0
  32. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +2 -0
  33. package/modules/@apostrophecms/i18n/i18n/sk.json +2 -0
  34. package/modules/@apostrophecms/image/index.js +18 -2
  35. package/modules/@apostrophecms/image/ui/apos/apps/AposImageRelationshipQueryFilter.js +1 -1
  36. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +9 -30
  37. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +40 -29
  38. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploaderUi.vue +25 -4
  39. package/modules/@apostrophecms/image-widget/ui/apos/components/AposImageWidget.vue +12 -6
  40. package/modules/@apostrophecms/layout-column-widget/index.js +188 -0
  41. package/modules/@apostrophecms/layout-column-widget/views/widget.html +1 -0
  42. package/modules/@apostrophecms/layout-widget/index.js +371 -0
  43. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +439 -0
  44. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +408 -0
  45. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +1292 -0
  46. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposLayoutColControlDialog.vue +171 -0
  47. package/modules/@apostrophecms/layout-widget/ui/apos/layout.css +72 -0
  48. package/modules/@apostrophecms/layout-widget/ui/apos/lib/grid-manager.js +665 -0
  49. package/modules/@apostrophecms/layout-widget/ui/apos/lib/grid-state.mjs +1727 -0
  50. package/modules/@apostrophecms/layout-widget/ui/src/layout.css +97 -0
  51. package/modules/@apostrophecms/layout-widget/views/column.html +19 -0
  52. package/modules/@apostrophecms/layout-widget/views/widget.html +15 -0
  53. package/modules/@apostrophecms/login/index.js +116 -26
  54. package/modules/@apostrophecms/migration/index.js +2 -4
  55. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +5 -0
  56. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +1 -1
  57. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +12 -0
  58. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +18 -2
  59. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +29 -3
  60. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +0 -1
  61. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +11 -42
  62. package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +3 -2
  63. package/modules/@apostrophecms/template/index.js +8 -5
  64. package/modules/@apostrophecms/ui/ui/apos/apps/AposBreakpointPreview.js +5 -0
  65. package/modules/@apostrophecms/ui/ui/apos/apps/AposWidgetStore.js +9 -0
  66. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +56 -15
  67. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +115 -10
  68. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +1 -0
  69. package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +9 -1
  70. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +29 -4
  71. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +1 -0
  72. package/modules/@apostrophecms/ui/ui/apos/stores/breakpointPreview.js +15 -0
  73. package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +116 -0
  74. package/modules/@apostrophecms/user/index.js +14 -0
  75. package/modules/@apostrophecms/widget-type/index.js +198 -24
  76. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +4 -1
  77. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +4 -2
  78. package/package.json +3 -2
  79. package/test/areas.js +96 -89
  80. package/test/images.js +102 -0
  81. package/test/layout-widget-ui.js +1411 -0
  82. package/test/login.js +497 -3
  83. package/test/walk-widgets.js +549 -0
  84. package/test/widgets.js +12 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.23.0 (2025-10-30)
4
+
5
+ ### Adds
6
+
7
+ * Add locale picker in the page and piece manager modals.
8
+ * Support for the `render-areas` query parameter in the REST API when using Astro as an external frontend, provided the Astro project has the corresponding route. This allows section template library previews to work in Astro projects. For ease of migration, if Astro cannot satisfy the request, ApostropheCMS will also attempt to render the widget natively.
9
+ * Made `self.apos.externalFrontKey` available, simplifying API calls back to Astro.
10
+ * Layout widget for dynamic grid layouts.
11
+ * `widgetOperations` support for `placement: 'breadcrumb'` to add operations to the breadcrumb menu of widgets. Extend the widget operations configuration to support various features when in the breadcrumb menu.
12
+ * Area template (Nunjucks) support for `aposStyle`, `aposClassName`, `aposParentOptions` and `aposAttrs` contextual named variables (`with {}` syntax).
13
+ * New login option `caseInsensitive` to force login usernames and emails to be case insensitive. New task `login-case-insensitive` updating all login names / email to lowercase, used by a new migration when switching to `caseInsensitive`.
14
+ * Adds `disableIfProps` and `disableTooltip` to widget operations and breadcrumb operations.
15
+
16
+ ### Changes
17
+
18
+ * Enable `/api/v1/@apostrophecms/login/logout` and `/api/v1/@apostrophecms/login/whoami` routes when `localLogin` is `false`.
19
+ * Refactored complex logic regarding data updates in `AposSchema`.
20
+ * Cleaned up `annotateAreaForExternalFront` logic and added context so developers understand the reason if it fails due to a widget type with no matching module in the project.
21
+ * Color fields now display their preset color swatches in the field UI rather than just the color picker popup
22
+ * Moves widget operations to backend with new `action` and `nativeAction` properties.
23
+ * Moves `mode` from breakpoint preview to it's own store (to be used by layout).
24
+
25
+ ### Fixes
26
+
27
+ * The `render-areas` query parameter now works correctly with areas nested in array items.
28
+ * Fix min size calculation for image widgets configured with an aspect ratio.
29
+ * Added missing `await` in helper library function for the asset module, ensuring JS assets build reliably.
30
+ * Autodetection of bundles, and automatic activation of "improvements" shipped in those bundles, now works correctly when the bundle is delivered in ES module format rather than commonjs format.
31
+
3
32
  ## 4.22.0 (2025-10-01)
4
33
 
5
34
  ### Adds
package/defaults.js CHANGED
@@ -41,6 +41,8 @@ module.exports = {
41
41
  '@apostrophecms/search': {},
42
42
  '@apostrophecms/any-page-type': {},
43
43
  '@apostrophecms/area': {},
44
+ '@apostrophecms/layout-widget': {},
45
+ '@apostrophecms/layout-column-widget': {},
44
46
  '@apostrophecms/rich-text-widget': {},
45
47
  '@apostrophecms/html-widget': {},
46
48
  '@apostrophecms/color-field': {},
package/index.js CHANGED
@@ -14,6 +14,8 @@ const glob = require('./lib/glob.js');
14
14
  const moogRequire = require('./lib/moog-require');
15
15
  let defaults = require('./defaults.js');
16
16
 
17
+ const importFresh = moduleName => import(`${moduleName}?${Date.now()}`);
18
+
17
19
  // ## Top-level options
18
20
  //
19
21
  // `cluster`
@@ -435,16 +437,24 @@ async function apostrophe(options, telemetry, rootSpan) {
435
437
  continue;
436
438
  }
437
439
 
438
- const apostropheModule = await self.root.import(npmPath);
439
- if (apostropheModule.bundle) {
440
- self.options.bundles = (self.options.bundles || []).concat(apostropheModuleName);
441
- const bundleModules = apostropheModule.bundle.modules;
440
+ const { default: apostropheModule } = await importFresh(npmPath);
441
+ const bundle = apostropheModule.bundle;
442
+ if (bundle) {
443
+ self.options.bundles = [
444
+ ...new Set(
445
+ [
446
+ ...(self.options.bundles || []),
447
+ apostropheModuleName
448
+ ]
449
+ )
450
+ ];
451
+ const bundleModules = bundle.modules;
442
452
  for (const bundleModuleName of bundleModules) {
443
453
  if (!apostropheModules.includes(bundleModuleName)) {
444
- const bundledModule = await self.root.import(
454
+ const { default: bundledModule } = await importFresh(
445
455
  path.resolve(
446
456
  path.dirname(npmPath),
447
- apostropheModule.bundle.directory,
457
+ bundle.directory,
448
458
  bundleModuleName,
449
459
  'index.js'
450
460
  )
package/lib/image.js ADDED
@@ -0,0 +1,48 @@
1
+ function computeMinSizes([ minWidth, minHeight ], aspectRatio) {
2
+ const aspectRatioFloat = Array.isArray(aspectRatio)
3
+ ? aspectRatio[0] / aspectRatio[1]
4
+ : aspectRatio;
5
+
6
+ if (!aspectRatioFloat) {
7
+ return {
8
+ minWidth,
9
+ minHeight
10
+ };
11
+ }
12
+
13
+ // If ratio wants a square,
14
+ // we simply take the higher min size
15
+ if (aspectRatioFloat === 1) {
16
+ const higherValue = Math.max(minWidth, minHeight);
17
+
18
+ return {
19
+ minWidth: higherValue,
20
+ minHeight: higherValue
21
+ };
22
+ }
23
+
24
+ const diff = minWidth / minHeight - aspectRatioFloat;
25
+
26
+ if (diff > 0) {
27
+ return {
28
+ minWidth,
29
+ minHeight: minWidth / aspectRatioFloat
30
+ };
31
+ }
32
+
33
+ if (diff < 0) {
34
+ return {
35
+ minWidth: minHeight * aspectRatioFloat,
36
+ minHeight
37
+ };
38
+ }
39
+
40
+ return {
41
+ minWidth,
42
+ minHeight
43
+ };
44
+ };
45
+
46
+ module.exports = {
47
+ computeMinSizes
48
+ };
@@ -56,6 +56,9 @@
56
56
  </div>
57
57
  </template>
58
58
  <script>
59
+ import { mapState, mapActions } from 'pinia';
60
+ import { useBreakpointPreviewStore } from 'Modules/@apostrophecms/ui/stores/breakpointPreview.js';
61
+
59
62
  export default {
60
63
  name: 'TheAposContextBreakpointPreviewMode',
61
64
  props: {
@@ -83,7 +86,6 @@ export default {
83
86
  emits: [ 'switch-breakpoint-preview-mode', 'reset-breakpoint-preview-mode' ],
84
87
  data() {
85
88
  return {
86
- mode: null,
87
89
  originalBodyBackground: null,
88
90
  shortcuts: this.getShortcuts(),
89
91
  breakpoints: this.getBreakpointItems(),
@@ -94,6 +96,7 @@ export default {
94
96
  };
95
97
  },
96
98
  computed: {
99
+ ...mapState(useBreakpointPreviewStore, [ 'mode' ]),
97
100
  activeScreen() {
98
101
  return this.mode && this.screens[this.mode];
99
102
  },
@@ -154,6 +157,7 @@ export default {
154
157
  );
155
158
  },
156
159
  methods: {
160
+ ...mapActions(useBreakpointPreviewStore, [ 'setMode' ]),
157
161
  observerCallback(mutationList, observer) {
158
162
  for (const mutation of mutationList) {
159
163
  if (
@@ -205,7 +209,7 @@ export default {
205
209
  refreshableEl.style.width = width;
206
210
  refreshableEl.style.height = height;
207
211
 
208
- this.mode = mode;
212
+ this.setMode(mode);
209
213
  this.$emit('switch-breakpoint-preview-mode', {
210
214
  mode,
211
215
  label,
@@ -249,7 +253,7 @@ export default {
249
253
  refreshableEl.style.removeProperty('width');
250
254
  refreshableEl.style.removeProperty('height');
251
255
 
252
- this.mode = null;
256
+ this.setMode(null);
253
257
  this.$emit('reset-breakpoint-preview-mode');
254
258
  this.saveState({ mode: this.mode });
255
259
  },
@@ -1,5 +1,6 @@
1
1
  const _ = require('lodash');
2
2
  const { stripIndent } = require('common-tags');
3
+ const cheerio = require('cheerio');
3
4
 
4
5
  // An area is a series of zero or more widgets, in which users can add
5
6
  // and remove widgets and drag them to reorder them. This module implements
@@ -270,7 +271,9 @@ module.exports = {
270
271
  self.missingWidgetTypes = {};
271
272
  }
272
273
  if (!self.missingWidgetTypes[name]) {
273
- self.apos.util.error('WARNING: widget type ' + name + ' exists in content but is not configured');
274
+ self.apos.util.error(`WARNING: widget type ${name} exists in your database but is not configured.\n` +
275
+ `You probably do not have a ${name}-widget module in your project.`
276
+ );
274
277
  self.missingWidgetTypes[name] = true;
275
278
  }
276
279
  },
@@ -292,6 +295,10 @@ module.exports = {
292
295
  // If `inline` is true then the rendering of each widget is attached
293
296
  // to the widget as a `_rendered` property, bypassing normal full-area
294
297
  // HTML responses, and the return value of this method is `null`.
298
+ //
299
+ // If an external front key is configured, ApostropheCMS will attempt
300
+ // to render the widget via Astro before attempting to render it
301
+ // natively.
295
302
  async renderArea(req, area, _with, { inline = false } = {}) {
296
303
  if (!area._id) {
297
304
  throw new Error('All areas must have an _id property in A3.x. Area details:\n\n' + JSON.stringify(area));
@@ -337,27 +344,86 @@ module.exports = {
337
344
  // just use the helpers
338
345
  self.apos.attachment.all(area, { annotate: true });
339
346
  }
340
- if (inline) {
341
- for (const item of area.items) {
342
- item._rendered = await self.renderWidget(
343
- req,
344
- item.type,
345
- item,
346
- widgets[item.type]
347
- );
347
+
348
+ let externalError = null;
349
+ if (!self.apos.externalFrontKey) {
350
+ return renderNatively();
351
+ }
352
+
353
+ try {
354
+ return await renderViaExternalFront();
355
+ } catch (e) {
356
+ externalError = e;
357
+ }
358
+ try {
359
+ return await renderNatively();
360
+ } catch (e) {
361
+ throw new Error('Could not render area for API, neither via the external frontend nor natively.\n\n' +
362
+ 'Check your Astro server logs as well.\n\n' +
363
+ niceError(externalError) + '\n\n' +
364
+ niceError(e)
365
+ );
366
+ }
367
+
368
+ async function renderViaExternalFront() {
369
+ if (!self.apos.baseUrl && self.apos.externalFrontKey) {
370
+ throw new Error('APOS_BASE_URL and APOS_EXTERNAL_FRONT_KEY must both be set in order to render\nvia the external frontend');
371
+ }
372
+ // Astro can render components or return JSON but not both, at least not without
373
+ // using its experimental container API which would potentially not have
374
+ // the same configuration as the main Astro project. So we let Astro be Astro,
375
+ // then we pull out the individual renderings with Cheerio. -Tom
376
+ const response = await fetch(`${self.apos.baseUrl}/api/apos-external-front/render-area`, {
377
+ method: 'POST',
378
+ headers: {
379
+ 'apos-external-front-key': self.apos.externalFrontKey
380
+ },
381
+ body: JSON.stringify({
382
+ area
383
+ })
384
+ });
385
+ if (response.status >= 400) {
386
+ throw response;
387
+ }
388
+ const html = await response.text();
389
+ const $ = cheerio.load(`<div id="root">${html}</div>`);
390
+ if (inline) {
391
+ for (let i = 0; (i < area.items.length); i++) {
392
+ area.items[i]._rendered = $(`#root [data-widget-id="${area.items[i]._id}"]`).html() || '';
393
+ }
394
+ return null;
395
+ } else {
396
+ const $children = $('#root [data-widget-id]');
397
+ return $children.map(function() {
398
+ return $(this).html();
399
+ }).join('\n');
400
+ }
401
+ }
402
+
403
+ async function renderNatively() {
404
+ if (inline) {
405
+ for (const item of area.items) {
406
+ item._rendered = await self.renderWidget(
407
+ req,
408
+ item.type,
409
+ item,
410
+ widgets[item.type]
411
+ );
412
+ }
413
+ return null;
414
+ } else {
415
+ return self.render(req, 'area', {
416
+ // TODO filter area to exclude big relationship objects, but
417
+ // not so sloppy this time please
418
+ area,
419
+ field,
420
+ options,
421
+ choices,
422
+ _with,
423
+ canEdit
424
+ });
348
425
  }
349
- return null;
350
426
  }
351
- return self.render(req, 'area', {
352
- // TODO filter area to exclude big relationship objects, but
353
- // not so sloppy this time please
354
- area,
355
- field,
356
- options,
357
- choices,
358
- _with,
359
- canEdit
360
- });
361
427
  },
362
428
  // Replace documents' area objects with rendered HTML for each area.
363
429
  // This is used by GET requests including the `render-areas` query
@@ -373,6 +439,10 @@ module.exports = {
373
439
  let index = 0;
374
440
  // Loop over the docs in the array passed in.
375
441
  for (const doc of within) {
442
+ if (self.apos.externalFrontKey) {
443
+ self.apos.template.annotateDocForExternalFront(doc);
444
+ }
445
+
376
446
  const rendered = [];
377
447
 
378
448
  const areasToRender = {};
@@ -409,8 +479,8 @@ module.exports = {
409
479
  index++;
410
480
  }
411
481
 
412
- async function render(area, path, context, opts) {
413
- const preppedArea = self.prepForRender(area, context, path);
482
+ async function render(area, path, context) {
483
+ const preppedArea = self.prepForRender(area, context, path.split('.').at(-1));
414
484
 
415
485
  const areaRendered = await self.apos.area.renderArea(
416
486
  req,
@@ -903,3 +973,9 @@ module.exports = {
903
973
  };
904
974
  }
905
975
  };
976
+
977
+ function niceError(e) {
978
+ // Node.js includes the error message in the stack property, it's
979
+ // actually a complete rendering plus the stack 🤷
980
+ return e.stack;
981
+ }
@@ -3,8 +3,6 @@ import createApp from 'Modules/@apostrophecms/ui/lib/vue';
3
3
 
4
4
  export default function() {
5
5
 
6
- const component = apos.vueComponents.AposAreaEditor;
7
-
8
6
  let widgetsRendering = 0;
9
7
 
10
8
  apos.area.widgetOptions = [];
@@ -66,13 +64,25 @@ export default function() {
66
64
  }
67
65
 
68
66
  function createAreaApp(el) {
69
- const options = JSON.parse(el.getAttribute('data-options'));
70
- const data = JSON.parse(el.getAttribute('data'));
67
+ const options = JSON.parse(el.getAttribute('data-options')) || {};
68
+ const data = JSON.parse(el.getAttribute('data')) || {};
71
69
  const fieldId = el.getAttribute('data-field-id');
70
+ const moduleName = el.getAttribute('data-module');
72
71
  const choices = JSON.parse(el.getAttribute('data-choices'));
73
72
  const renderings = {};
74
73
  const _docId = data._docId;
75
74
 
75
+ const parentOptionsStr = el.getAttribute('data-parent-options');
76
+ const parentOptions = parentOptionsStr ? JSON.parse(parentOptionsStr) : null;
77
+
78
+ let componentName = options.editorComponent || 'AposAreaEditor';
79
+ if (!apos.vueComponents[componentName]) {
80
+ // eslint-disable-next-line no-console
81
+ console.error(`Area Editor component "${componentName}" not found. Switching to default.`);
82
+ componentName = 'AposAreaEditor';
83
+ }
84
+ const component = apos.vueComponents[componentName];
85
+
76
86
  for (const widgetEl of el.querySelectorAll('[data-apos-widget]')) {
77
87
  const _id = widgetEl.getAttribute('data-apos-widget');
78
88
  const item = data.items.find(item => _id === item._id);
@@ -109,6 +119,8 @@ export default function() {
109
119
  choices,
110
120
  docId: _docId,
111
121
  fieldId,
122
+ moduleName,
123
+ parentOptions,
112
124
  renderings
113
125
  });
114
126