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.
Files changed (57) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +142 -0
  4. package/index.js +3 -0
  5. package/lib/stream-proxy.js +49 -0
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
  7. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
  8. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
  10. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
  11. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
  12. package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
  13. package/modules/@apostrophecms/asset/index.js +3 -2
  14. package/modules/@apostrophecms/attachment/index.js +270 -0
  15. package/modules/@apostrophecms/doc/index.js +8 -2
  16. package/modules/@apostrophecms/doc-type/index.js +81 -1
  17. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
  18. package/modules/@apostrophecms/express/index.js +30 -1
  19. package/modules/@apostrophecms/file/index.js +70 -6
  20. package/modules/@apostrophecms/i18n/index.js +20 -1
  21. package/modules/@apostrophecms/image/index.js +11 -0
  22. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
  23. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
  24. package/modules/@apostrophecms/login/index.js +43 -11
  25. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
  27. package/modules/@apostrophecms/page/index.js +9 -11
  28. package/modules/@apostrophecms/page-type/index.js +6 -1
  29. package/modules/@apostrophecms/piece-page-type/index.js +100 -13
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
  31. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
  32. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  34. package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
  35. package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
  36. package/modules/@apostrophecms/styles/lib/methods.js +35 -12
  37. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
  38. package/modules/@apostrophecms/task/index.js +9 -1
  39. package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
  40. package/modules/@apostrophecms/ui/index.js +2 -0
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
  42. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
  43. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
  44. package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
  45. package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
  46. package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
  47. package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
  48. package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
  49. package/modules/@apostrophecms/uploadfs/index.js +15 -1
  50. package/modules/@apostrophecms/url/index.js +419 -1
  51. package/package.json +5 -5
  52. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
  53. package/test/external-front.js +1 -0
  54. package/test/files.js +264 -0
  55. package/test/login-requirements.js +145 -3
  56. package/test/static-build.js +2701 -0
  57. package/test/universal-graph.js +1135 -0
@@ -10,16 +10,7 @@
10
10
  v-if="editor"
11
11
  plugin-key="richTextMenu"
12
12
  class="bubble-menu"
13
- :tippy-options="{
14
- maxWidth: 'none',
15
- duration: 300,
16
- zIndex: 999,
17
- animation: 'fade',
18
- inertia: true,
19
- placement: 'bottom',
20
- hideOnClick: false,
21
- onHide: onBubbleHide
22
- }"
13
+ :tippy-options="bubbleMenuTippyOptions"
23
14
  :editor="editor"
24
15
  >
25
16
  <AposContextMenuDialog
@@ -122,6 +113,7 @@ import {
122
113
  BubbleMenu,
123
114
  FloatingMenu
124
115
  } from '@tiptap/vue-3';
116
+
125
117
  import AposTiptapTableControls from './AposTiptapTableControls.vue';
126
118
  // Starter Kit extensions
127
119
  import BlockQuote from '@tiptap/extension-blockquote';
@@ -155,6 +147,7 @@ import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstanc
155
147
  import merge from 'lodash/merge';
156
148
  import { useAposStyles } from 'Modules/@apostrophecms/styles/composables/AposStyles.js';
157
149
  import { useModalStore } from 'Modules/@apostrophecms/ui/stores/modal';
150
+ import { useWidgetStore } from 'Modules/@apostrophecms/ui/stores/widget';
158
151
 
159
152
  export default {
160
153
  name: 'AposRichTextWidgetEditor',
@@ -199,7 +192,7 @@ export default {
199
192
  default: false
200
193
  }
201
194
  },
202
- emits: [ 'update', 'suppressWidgetControls' ],
195
+ emits: [ 'update', 'suppressWidgetControls', 'suppressAddContentButtons' ],
203
196
  setup() {
204
197
  return useAposStyles();
205
198
  },
@@ -219,12 +212,26 @@ export default {
219
212
  activeInsertMenuComponent: false,
220
213
  suppressInsertMenu: false,
221
214
  suppressWidgetControls: false,
215
+ suppressAddContentButtons: false,
222
216
  hasSelection: false,
223
217
  openedPopover: false
224
218
  };
225
219
  },
226
220
  computed: {
227
221
  ...mapState(useModalStore, [ 'getAdminDirectionClass' ]),
222
+ ...mapState(useWidgetStore, [ 'focusedWidget' ]),
223
+ bubbleMenuTippyOptions() {
224
+ return {
225
+ maxWidth: 'none',
226
+ duration: 300,
227
+ zIndex: 999,
228
+ animation: 'fade',
229
+ inertia: true,
230
+ placement: 'bottom',
231
+ hideOnClick: false,
232
+ onHide: this.onBubbleHide
233
+ };
234
+ },
228
235
  // Note that context menu class-list expects a string
229
236
  contextMenuClasses() {
230
237
  const directionClass = this.getAdminDirectionClass();
@@ -374,9 +381,15 @@ export default {
374
381
  this.$emit('suppressWidgetControls');
375
382
  }
376
383
  },
384
+ suppressAddContentButtons(newVal) {
385
+ if (newVal) {
386
+ this.$emit('suppressAddContentButtons');
387
+ }
388
+ },
377
389
  isFocused(newVal) {
378
390
  if (!newVal) {
379
391
  this.suppressWidgetControls = false;
392
+ this.suppressAddContentButtons = false;
380
393
  if (this.pending) {
381
394
  this.emitWidgetUpdate();
382
395
  }
@@ -482,9 +495,11 @@ export default {
482
495
  });
483
496
  },
484
497
  onSelectionUpdate: ({ editor }) => {
498
+ this.hasSelection = !editor.view.state.selection.empty;
485
499
  this.$nextTick(() => {
486
- if (!editor.view.state.selection.empty) {
500
+ if (this.hasSelection) {
487
501
  this.suppressWidgetControls = true;
502
+ this.suppressAddContentButtons = true;
488
503
  }
489
504
  });
490
505
  }
@@ -579,6 +594,7 @@ export default {
579
594
  this.suppressInsertMenu = false;
580
595
  }
581
596
  this.suppressWidgetControls = true;
597
+ this.suppressAddContentButtons = true;
582
598
  },
583
599
  doSuppressInsertMenu() {
584
600
  this.suppressInsertMenu = true;
@@ -179,6 +179,7 @@ export default {
179
179
  const attrs = this.schemaHtmlAttributes.reduce((acc, field) => {
180
180
  const value = this.docFields.data[field.name];
181
181
  if (field.type === 'checkboxes' && !value?.[0]) {
182
+ acc[field.htmlAttribute] = null;
182
183
  return acc;
183
184
  }
184
185
  if (field.type === 'boolean') {
@@ -28,7 +28,7 @@
28
28
  v-apos-tooltip="getTooltip(item)"
29
29
  aria-selected="false"
30
30
  :class="getClasses(item, index)"
31
- @pointerdown.prevent="select(item)"
31
+ @click="select(item)"
32
32
  >
33
33
  <div
34
34
  v-if="item?.attachment?._urls?.['one-sixth']"
@@ -2,12 +2,32 @@ module.exports = self => {
2
2
  return {
3
3
  get: {
4
4
  // This route serves the existing styles stylesheet,
5
- // constructed from the global object
6
- // We do it this way so the browser can cache the styles as often as possible
5
+ // constructed from the global object.
6
+ // We do it this way so the browser can cache the styles as often as possible.
7
+ //
8
+ // The locale-qualified alias (`stylesheet/locale/:locale/:mode`)
9
+ // produces a distinct path per locale so that static-build
10
+ // tools that key on the URL path (e.g. Astro) don't
11
+ // overwrite one locale's stylesheet with another's.
7
12
  stylesheet(req) {
8
- req.res.setHeader('Content-Type', 'text/css');
9
- req.res.setHeader('Cache-Control', 'public, max-age=31557600');
10
- return req.data.global.stylesStylesheet;
13
+ return self.serveStylesheet(req);
14
+ },
15
+ 'stylesheet/locale/:locale/:mode': async function(req) {
16
+ const { locale, mode } = req.params;
17
+ if (!locale || !self.apos.i18n.isValidLocale(locale)) {
18
+ throw self.apos.error('invalid');
19
+ }
20
+ const validModes = [ 'published', 'draft' ];
21
+ const safeMode = validModes.includes(mode) ? mode : 'published';
22
+ if (safeMode === 'draft' && !self.apos.permission.can(req, 'view-draft')) {
23
+ throw self.apos.error('forbidden');
24
+ }
25
+ req.locale = locale;
26
+ req.mode = safeMode;
27
+ // Force re-fetch global for the correct locale
28
+ delete req.data.global;
29
+ await self.apos.global.addGlobalToData(req);
30
+ return self.serveStylesheet(req);
11
31
  }
12
32
  },
13
33
  post: {
@@ -45,6 +45,25 @@ node app @apostrophecms-pro/palette:migrate-to-styles
45
45
  }
46
46
  self.apos.template.addBodyClass(req, classes.join(' '));
47
47
  }
48
+ },
49
+ '@apostrophecms/url:getAllUrlMetadata': {
50
+ // Provide a literal content entry so static builds
51
+ // can include the dynamically generated stylesheet.
52
+ // Uses the locale-path URL so each locale produces a
53
+ // distinct path for static-build tools.
54
+ // The URL must be relative and prefix-free — the
55
+ // consumer prepends the prefix when fetching.
56
+ addStylesheetUrl(req, results) {
57
+ if (!req.data.global?.stylesStylesheet) {
58
+ return;
59
+ }
60
+ results.push({
61
+ url: self.getStylesheetUrl(req, { relative: true }),
62
+ contentType: 'text/css',
63
+ i18nId: '@apostrophecms/styles:stylesheet',
64
+ sitemap: false
65
+ });
66
+ }
48
67
  }
49
68
  };
50
69
  };
@@ -82,6 +82,27 @@ module.exports = (self, options) => {
82
82
 
83
83
  return expanded;
84
84
  },
85
+ // Returns the locale-qualified stylesheet URL for the given
86
+ // request. The path encodes the locale so that static-build
87
+ // tools produce a distinct file per locale.
88
+ //
89
+ // Options:
90
+ // `relative` - if true, return a prefix-free path suitable
91
+ // for route-relative contexts (e.g. `getAllUrlMetadata`).
92
+ // Defaults to `false`, which prepends `apos.prefix` so the
93
+ // URL works in rendered HTML (`<link>` tags, etc.).
94
+ getStylesheetUrl(req, { relative = false } = {}) {
95
+ const prefix = relative ? '' : (self.apos.prefix || '');
96
+ const version = req.data.global?.stylesStylesheetVersion;
97
+ return `${prefix}${self.action}/stylesheet/locale/${req.locale}/${req.mode}${version ? `?version=${version}` : ''}`;
98
+ },
99
+ // Shared implementation for the stylesheet API routes.
100
+ // Sets cache headers and returns the raw CSS string.
101
+ serveStylesheet(req) {
102
+ req.res.setHeader('Content-Type', 'text/css');
103
+ req.res.setHeader('Cache-Control', 'public, max-age=31557600');
104
+ return req.data.global?.stylesStylesheet || '';
105
+ },
85
106
  stylesheet(req) {
86
107
  // Stylesheet node should be created only for logged in users.
87
108
  if (!req.data.global) {
@@ -94,7 +115,7 @@ module.exports = (self, options) => {
94
115
  // and similar features from those who should just get the link tag
95
116
  const hasLink = !self.apos.permission.can(req, 'view-draft');
96
117
  if (req.data.global.stylesStylesheet && hasLink) {
97
- const href = `${self.action}/stylesheet?version=${req.data.global.stylesStylesheetVersion}&aposLocale=${req.locale}:${req.mode}`;
118
+ const href = self.getStylesheetUrl(req);
98
119
  nodes.push({
99
120
  name: 'link',
100
121
  attrs: {
@@ -103,17 +124,19 @@ module.exports = (self, options) => {
103
124
  }
104
125
  });
105
126
  }
106
- nodes.push({
107
- name: 'style',
108
- attrs: {
109
- id: 'apos-styles-stylesheet'
110
- },
111
- body: [
112
- {
113
- text: req.data.global.stylesStylesheet || ''
114
- }
115
- ]
116
- });
127
+ if (!hasLink) {
128
+ nodes.push({
129
+ name: 'style',
130
+ attrs: {
131
+ id: 'apos-styles-stylesheet'
132
+ },
133
+ body: [
134
+ {
135
+ raw: req.data.global.stylesStylesheet || ''
136
+ }
137
+ ]
138
+ });
139
+ }
117
140
  return nodes;
118
141
  },
119
142
  ui(req) {
@@ -431,6 +431,7 @@ export default {
431
431
  );
432
432
  },
433
433
  async setStyleMarkup({ css, classes }) {
434
+ const stylesheetEl = document.querySelector('#apos-styles-stylesheet');
434
435
  this.setBodyClasses(classes);
435
436
  if (apos.adminBar.breakpointPreviewMode?.enable) {
436
437
  const processed = breakpointPreviewTransformer(css, {
@@ -438,11 +439,15 @@ export default {
438
439
  debug: apos.adminBar.breakpointPreviewMode?.debug === true,
439
440
  transform: apos.adminBar.breakpointPreviewMode?.transform || null
440
441
  });
441
- document.querySelector('#apos-styles-stylesheet').textContent = processed;
442
+ if (stylesheetEl) {
443
+ stylesheetEl.textContent = processed;
444
+ }
442
445
  return;
443
446
  }
444
447
 
445
- document.querySelector('#apos-styles-stylesheet').textContent = css;
448
+ if (stylesheetEl) {
449
+ stylesheetEl.textContent = css;
450
+ }
446
451
  },
447
452
  setBodyClasses(classes) {
448
453
  const previousClasses = document.body
@@ -300,7 +300,15 @@ module.exports = {
300
300
  };
301
301
  addCloneMethod(req);
302
302
  req.res.__ = req.__;
303
- const { role: _role, ...properties } = options || {};
303
+ const {
304
+ role: _role, staticBuild: _staticBuild, ...properties
305
+ } = options || {};
306
+
307
+ // When staticBuild is requested, configure the req object
308
+ // with the same properties that Express middlewares
309
+ if (_staticBuild) {
310
+ self.apos.modules['@apostrophecms/express'].applyStaticBuildHeaders(req);
311
+ }
304
312
 
305
313
  Object.assign(req, properties);
306
314
  self.apos.i18n.setPrefixUrls(req);
@@ -1,6 +1,9 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="{% block locale %}{{ data.locale }}{% endblock %}" dir="{% block direction %}{{ data.i18n.direction or 'ltr' }}{% endblock %}" {% block extraHtml %}{% endblock %}>
3
3
  <head>
4
+ {% block encoding %}
5
+ <meta charset="utf-8">
6
+ {% endblock %}
4
7
  {% block startHead %}
5
8
  {% endblock %}
6
9
  {% component '@apostrophecms/template:inject' with { where: 'head', end: 'prepend', when: 'hmr' } %}
@@ -9,6 +9,7 @@ module.exports = {
9
9
  },
10
10
  init(self) {
11
11
  self.enableBrowserData();
12
+ self.uiDebug = self.options.debug ?? process.env.NODE_ENV !== 'production';
12
13
  },
13
14
  methods(self) {
14
15
  return {
@@ -23,6 +24,7 @@ module.exports = {
23
24
  theme.primary = req.data.user.aposThemePrimary;
24
25
  }
25
26
  return {
27
+ debug: self.uiDebug,
26
28
  theme,
27
29
  widgetMargin: self.options.widgetMargin
28
30
  };
@@ -48,7 +48,7 @@ export default {
48
48
  <style lang="scss" scoped>
49
49
  .apos-button-group {
50
50
  background-color: var(--a-background-primary);
51
- border-radius: var(--a-border-radius);
51
+ border-radius: var(--a-border-radius-large);
52
52
  display: inline-flex;
53
53
  }
54
54
 
@@ -86,6 +86,7 @@
86
86
  :active-item="activeItem"
87
87
  :is-open="isOpen"
88
88
  :has-tip="hasTip"
89
+ :ignore-unfocus="ignoreUnfocus"
89
90
  @item-clicked="menuItemClicked"
90
91
  @set-arrow="setArrow"
91
92
  >
@@ -210,6 +211,10 @@ const props = defineProps({
210
211
  teleportContent: {
211
212
  type: Boolean,
212
213
  default: false
214
+ },
215
+ ignoreUnfocus: {
216
+ type: Boolean,
217
+ default: false
213
218
  }
214
219
  });
215
220
 
@@ -3,6 +3,7 @@
3
3
  class="apos-primary-scrollbar apos-context-menu__dialog"
4
4
  :class="classes"
5
5
  role="dialog"
6
+ :data-apos-ignore-unfocus="props.ignoreUnfocus"
6
7
  >
7
8
  <AposContextMenuTip
8
9
  v-if="hasTip"
@@ -37,6 +38,10 @@
37
38
  import { computed } from 'vue';
38
39
 
39
40
  const props = defineProps({
41
+ ignoreUnfocus: {
42
+ type: Boolean,
43
+ default: false
44
+ },
40
45
  menuPlacement: {
41
46
  type: String,
42
47
  required: true
@@ -6,6 +6,8 @@ import VueAposI18Next from './i18next';
6
6
 
7
7
  const pinia = createPinia();
8
8
 
9
+ export { pinia };
10
+
9
11
  export default (appConfig, props = {}) => {
10
12
  const app = createApp(appConfig, props);
11
13
 
@@ -49,13 +49,18 @@ export const useWidgetStore = defineStore('widget', () => {
49
49
 
50
50
  const headerHeight = window.apos.adminBar.height;
51
51
  const bufferSpace = 40;
52
- const targetTop = $el.getBoundingClientRect().top;
53
- const scrollPos = targetTop - headerHeight - bufferSpace;
54
-
55
- window.scrollBy({
56
- top: scrollPos,
57
- behavior: 'smooth'
58
- });
52
+ const rect = $el.getBoundingClientRect();
53
+ const visibleTop = headerHeight + bufferSpace;
54
+ const visibleBottom = window.innerHeight - bufferSpace;
55
+ const isInView = rect.top >= visibleTop && rect.bottom <= visibleBottom;
56
+
57
+ if (!isInView) {
58
+ const scrollPos = rect.top - headerHeight - bufferSpace;
59
+ window.scrollBy({
60
+ top: scrollPos,
61
+ behavior: 'smooth'
62
+ });
63
+ }
59
64
 
60
65
  $el.focus({
61
66
  preventScroll: true