apostrophe 3.28.0 → 3.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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.29.0 (2022-10-03)
4
+
5
+ ### Adds
6
+
7
+ * Areas now support an `expanded: true` option to display previews for widgets. The Expanded Widget Preview Menu also supports grouping and display columns for each group.
8
+ * Add "showQuery" in piece-page-type in order to override the query for the "show" page as "indexQuery" does it for the index page
9
+
10
+ ### Fixes
11
+
12
+ * Resolved a bug in which users making a password error in the presence of pre-login checks such as a CAPTCHA were unable to try again until they refreshed the page.
13
+
14
+ ## 3.28.1 (2022-09-15)
15
+
16
+ ### Fixes
17
+
18
+ * `AposInputBoolean` can now be `required` and have the value `false`.
19
+ * Schema fields containing boolean filters can now list both `yes` and `no` choices according to available values in the database.
20
+ * Fix attachment `getHeight()` and `getWidth()` template helpers by changing the assignment of the `attachment._crop` property.
21
+ * Change assignment of `attachment._focalPoint` for consistency.
22
+
3
23
  ## 3.28.0 (2022-08-31)
4
24
 
5
25
  ### Fixes
@@ -99,7 +119,7 @@ Hotfix: always waits for the DOM to be ready before initializing the Apostrophe
99
119
 
100
120
  ### Fixes
101
121
 
102
- * Fix a Webpack cache issue leading to modules symlinked in `node_modules` not being rebuilt.
122
+ * Fix a Webpack cache issue leading to modules symlinked in `node_modules` not being rebuilt.
103
123
  * Fixes login maximum attempts error message that wasn't showing the plural when lockoutMinutes is more than 1.
104
124
  * Fixes the text color of the current array item's slat label in the array editor modal.
105
125
  * Fixes the maximum width of an array item's slat label so as to not obscure the Remove button in narrow viewports.
@@ -113,13 +133,13 @@ Hotfix: always waits for the DOM to be ready before initializing the Apostrophe
113
133
  ### Fixes
114
134
 
115
135
  * Work around backwards compatibility break in `sass` module by pinning to `sass` `1.50.x` while we investigate. If you saw the error `RangeError: Invalid value: Not in inclusive range 0..145: -1` you can now fix that by upgrading with `npm update`. If it does not immediately clear up the issue in development, try `node app @apostrophecms/asset:clear-cache`.
116
-
136
+
117
137
  ## 3.21.0 (2022-05-25)
118
138
 
119
139
  ### Adds
120
140
 
121
141
  * Trigger only the relevant build when in a watch mode (development). The build paths should not contain comma (`,`).
122
- * Adds an `unpublish` method, available for any doc-type.
142
+ * Adds an `unpublish` method, available for any doc-type.
123
143
  An _Unpublish_ option has also been added to the context menu of the modal when editing a piece or a page.
124
144
  * Allows developers to group fields in relationships the same way it's done for normal schemas.
125
145
 
@@ -196,9 +216,9 @@ An _Unpublish_ option has also been added to the context menu of the modal when
196
216
  * Adds possibility for modules to [add extra frontend bundles for scss and js](https://v3.docs.apostrophecms.org/guide/webpack.html). This is useful when the `ui/src` build would otherwise be very large due to code used on rarely accessed pages.
197
217
  * Loads the right bundles on the right pages depending on the page template and the loaded widgets. Logged-in users have all the bundles on every page, because they might introduce widgets at any time.
198
218
  * Fixes deprecation warnings displayed after running `npm install`, for dependencies that are directly included by this package.
199
- * Implement custom ETags emission when `etags` cache option is enabled. [See the documentation for more information](https://v3.docs.apostrophecms.org/guide/caching.html).
200
- It allows caching of pages and pieces, using a cache invalidation mechanism that takes into account related (and reverse related) document updates, thanks to backlinks mentioned above.
201
- Note that for now, only single pages and pieces benefit from the ETags caching system (pages' and pieces' `getOne` REST API route, and regular served pages).
219
+ * Implement custom ETags emission when `etags` cache option is enabled. [See the documentation for more information](https://v3.docs.apostrophecms.org/guide/caching.html).
220
+ It allows caching of pages and pieces, using a cache invalidation mechanism that takes into account related (and reverse related) document updates, thanks to backlinks mentioned above.
221
+ Note that for now, only single pages and pieces benefit from the ETags caching system (pages' and pieces' `getOne` REST API route, and regular served pages).
202
222
  The cache of an index page corresponding to the type of a piece that was just saved will automatically be invalidated. However, please consider that it won't be effective when a related piece is saved, therefore the cache will automatically be invalidated _after_ the cache lifetime set in `maxAge` cache option.
203
223
 
204
224
  ### Fixes
@@ -43,9 +43,10 @@ module.exports = {
43
43
  throw self.apos.error('invalid');
44
44
  }
45
45
 
46
- let options = field.options && field.options.widgets && field.options.widgets[type];
46
+ const widgets = self.getWidgets(field.options);
47
+
48
+ const options = widgets[type] || {};
47
49
 
48
- options = options || {};
49
50
  const manager = self.getWidgetManager(type);
50
51
  if (!manager) {
51
52
  self.warnMissingWidgetType(type);
@@ -96,6 +97,20 @@ module.exports = {
96
97
  setWidgetManager(name, manager) {
97
98
  self.widgetManagers[name] = manager;
98
99
  },
100
+ getWidgets(options) {
101
+ let widgets = options.widgets || {};
102
+
103
+ if (options.groups) {
104
+ for (const group of Object.keys(options.groups)) {
105
+ widgets = {
106
+ ...widgets,
107
+ ...options.groups[group].widgets
108
+ };
109
+ }
110
+ }
111
+
112
+ return widgets;
113
+ },
99
114
  // Get the manager object for the given widget type name.
100
115
  getWidgetManager(name) {
101
116
  return self.widgetManagers[name];
@@ -140,7 +155,12 @@ module.exports = {
140
155
  in an options property.
141
156
  `);
142
157
  }
143
- _.each(options.widgets, function (options, name) {
158
+
159
+ const widgets = self.getWidgets(options);
160
+
161
+ options.widgets = widgets;
162
+
163
+ _.each(widgets, function (options, name) {
144
164
  const manager = self.widgetManagers[name];
145
165
  if (manager) {
146
166
  choices.push({
@@ -254,7 +274,8 @@ module.exports = {
254
274
  options = options || {};
255
275
  const result = [];
256
276
  const errors = [];
257
- const widgetsOptions = options.widgets || {};
277
+ const widgetsOptions = self.getWidgets(options);
278
+
258
279
  for (let i = 0; i < items.length; i++) {
259
280
  const item = items[i];
260
281
  if ((item == null) || typeof item !== 'object' || typeof item.type !== 'string') {
@@ -0,0 +1,382 @@
1
+ <template>
2
+ <div class="apos-area-menu" :class="{'apos-is-focused': groupIsFocused}">
3
+ <AposContextMenu
4
+ :disabled="isDisabled"
5
+ :button="buttonOptions"
6
+ v-bind="extendedContextMenuOptions"
7
+ @open="menuOpen"
8
+ @close="menuClose"
9
+ ref="contextMenu"
10
+ :popover-modifiers="inContext ? ['z-index-in-context'] : []"
11
+ >
12
+ <ul class="apos-area-menu__wrapper">
13
+ <li
14
+ class="apos-area-menu__item"
15
+ v-for="(item, itemIndex) in myMenu"
16
+ :key="item.type ? `${item.type}_${item.label}` : item.label"
17
+ :class="{'apos-has-group': item.items}"
18
+ :ref="`item-${itemIndex}`"
19
+ >
20
+ <dl v-if="item.items" class="apos-area-menu__group">
21
+ <dt>
22
+ <button
23
+ :for="item.label" class="apos-area-menu__group-label"
24
+ v-if="item.items" tabindex="0"
25
+ :id="`${menuId}-trigger-${itemIndex}`"
26
+ :aria-controls="`${menuId}-group-${itemIndex}`"
27
+ @focus="groupFocused"
28
+ @blur="groupBlurred"
29
+ @click="toggleGroup(itemIndex)"
30
+ @keydown.prevent.space="toggleGroup(itemIndex)"
31
+ @keydown.prevent.enter="toggleGroup(itemIndex)"
32
+ @keydown.prevent.arrow-down="switchGroup(itemIndex, 1)"
33
+ @keydown.prevent.arrow-up="switchGroup(itemIndex, -1)"
34
+ @keydown.prevent.home="switchGroup(itemIndex, 0)"
35
+ @keydown.prevent.end="switchGroup(itemIndex, null)"
36
+ ref="groupButton"
37
+ >
38
+ <span>{{ item.label }}</span>
39
+ <chevron-up-icon
40
+ class="apos-area-menu__group-chevron"
41
+ :class="{'apos-is-active': itemIndex === active}" :size="13"
42
+ />
43
+ </button>
44
+ </dt>
45
+ <dd class="apos-area-menu__group-list" role="region">
46
+ <ul
47
+ class="apos-area-menu__items apos-area-menu__items--accordion"
48
+ :class="{'apos-is-active': active === itemIndex}"
49
+ :id="`${menuId}-group-${itemIndex}`"
50
+ :aria-labelledby="`${menuId}-trigger-${itemIndex}`"
51
+ :aria-expanded="active === itemIndex ? 'true' : 'false'"
52
+ >
53
+ <li
54
+ class="apos-area-menu__item"
55
+ v-for="(child, childIndex) in item.items"
56
+ :key="child.name"
57
+ :ref="`child-${index}-${childIndex}`"
58
+ >
59
+ <AposAreaMenuItem
60
+ @click="add(child)"
61
+ :item="child"
62
+ :tabbable="itemIndex === active"
63
+ @up="switchItem(`child-${itemIndex}-${childIndex - 1}`, -1)"
64
+ @down="switchItem(`child-${itemIndex}-${childIndex + 1}`, 1)"
65
+ />
66
+ </li>
67
+ </ul>
68
+ </dd>
69
+ </dl>
70
+ <AposAreaMenuItem
71
+ v-else
72
+ @click="add(item)"
73
+ :item="item"
74
+ @up="switchItem(`item-${itemIndex - 1}`, -1)"
75
+ @down="switchItem(`item-${itemIndex + 1}`, 1)"
76
+ />
77
+ </li>
78
+ </ul>
79
+ </AposContextMenu>
80
+ </div>
81
+ </template>
82
+
83
+ <script>
84
+ import cuid from 'cuid';
85
+
86
+ export default {
87
+ name: 'AposAreaContextualMenu',
88
+ props: {
89
+ buttonOptions: {
90
+ type: Object,
91
+ required: true
92
+ },
93
+ contextMenuOptions: {
94
+ type: Object,
95
+ required: true
96
+ },
97
+ index: {
98
+ type: Number,
99
+ default: 0
100
+ },
101
+ options: {
102
+ type: Object,
103
+ required: true
104
+ },
105
+ maxReached: {
106
+ type: Boolean
107
+ },
108
+ disabled: {
109
+ type: Boolean,
110
+ default: false
111
+ },
112
+ // NOTE: Left for backwards compatibility.
113
+ // Should use options now instead.
114
+ widgetOptions: {
115
+ type: Object,
116
+ default: function() {
117
+ return {};
118
+ }
119
+ }
120
+ },
121
+ emits: [ 'menu-close', 'menu-open', 'add' ],
122
+ data() {
123
+ return {
124
+ active: 0,
125
+ groupIsFocused: false,
126
+ inContext: true
127
+ };
128
+ },
129
+ computed: {
130
+ moduleOptions() {
131
+ return window.apos.area;
132
+ },
133
+ isDisabled() {
134
+ let flag = this.disabled;
135
+ if (this.maxReached) {
136
+ flag = true;
137
+ }
138
+ return flag;
139
+ },
140
+ extendedContextMenuOptions() {
141
+ const modifiers = [ 'unpadded' ];
142
+ if (!this.groupedMenus) {
143
+ modifiers.push('tb-padded');
144
+ }
145
+ return {
146
+ menuPlacement: 'bottom',
147
+ menuOffset: 15,
148
+ ...this.contextMenuOptions,
149
+ modifiers
150
+ };
151
+ },
152
+ groupedMenus() {
153
+ let flag = false;
154
+ this.contextMenuOptions.menu.forEach((e) => {
155
+ if (e.items) {
156
+ flag = true;
157
+ }
158
+ });
159
+ return flag;
160
+ },
161
+ myMenu() {
162
+ const clipboard = apos.area.widgetClipboard.get();
163
+ const menu = [ ...this.contextMenuOptions.menu ];
164
+ if (clipboard) {
165
+ const widget = clipboard;
166
+ const matchingChoice = menu.find(option => option.name === widget.type);
167
+ if (matchingChoice) {
168
+ menu.unshift({
169
+ type: 'clipboard',
170
+ ...matchingChoice,
171
+ label: {
172
+ key: 'apostrophe:pasteWidget',
173
+ widget: this.$t(matchingChoice.label)
174
+ },
175
+ clipboard: widget
176
+ });
177
+ }
178
+ }
179
+ if (this.groupedMenus) {
180
+ return this.composeGroups(menu);
181
+ } else {
182
+ return menu;
183
+ }
184
+ },
185
+ menuId() {
186
+ return `areaMenu-${cuid()}`;
187
+ }
188
+ },
189
+ mounted() {
190
+ // if this area is not in-context then it is assumed in a schema's modal and we need to bump
191
+ // the z-index of menus above them
192
+ this.inContext = !apos.util.closest(this.$el, '[data-apos-schema-area]');
193
+ },
194
+ methods: {
195
+ menuClose(e) {
196
+ this.$emit('menu-close', e);
197
+ },
198
+ menuOpen(e) {
199
+ this.$emit('menu-open', e);
200
+ },
201
+ async add(item) {
202
+ // Potential TODO: If we find ourselves manually flipping these bits in other AposContextMenu overrides
203
+ // we should consider refactoring contextmenus to be able to self close when any click takes place within their el
204
+ // as it is often the logical experience (not always, see tag menus and filters)
205
+ this.$refs.contextMenu.isOpen = false;
206
+ this.$emit('add', {
207
+ ...item,
208
+ index: this.index
209
+ });
210
+ },
211
+ groupFocused() {
212
+ this.groupIsFocused = true;
213
+ },
214
+ groupBlurred() {
215
+ this.groupIsFocused = false;
216
+ },
217
+ composeGroups(menu) {
218
+ const ungrouped = {
219
+ label: 'apostrophe:ungroupedWidgets',
220
+ items: []
221
+ };
222
+ const myMenu = [];
223
+
224
+ menu.forEach((item) => {
225
+ if (!item.items) {
226
+ ungrouped.items.push(item);
227
+ } else {
228
+ myMenu.push(item);
229
+ }
230
+ });
231
+
232
+ if (ungrouped.items.length) {
233
+ myMenu.push(ungrouped);
234
+ }
235
+ return myMenu;
236
+ },
237
+
238
+ toggleGroup(index) {
239
+ if (this.active !== index) {
240
+ this.active = index;
241
+ } else {
242
+ this.active = null;
243
+ }
244
+ },
245
+
246
+ switchGroup(index, dir) {
247
+ let target;
248
+
249
+ if (dir > 0) {
250
+ target = index < this.$refs.groupButton.length - 1 ? index + 1 : 0;
251
+ }
252
+
253
+ if (dir < 0) {
254
+ target = index === 0 ? this.$refs.groupButton.length - 1 : index - 1;
255
+ }
256
+
257
+ if (dir === 0) {
258
+ target = 0;
259
+ }
260
+
261
+ if (!dir) {
262
+ target = this.$refs.groupButton.length - 1;
263
+ }
264
+
265
+ this.$nextTick(() => {
266
+ this.$refs.groupButton[target].focus();
267
+ });
268
+ },
269
+
270
+ switchItem(name, dir) {
271
+ if (this.$refs[name]) {
272
+ this.$refs[name][0].querySelector('button').focus();
273
+ }
274
+ }
275
+ }
276
+ };
277
+ </script>
278
+
279
+ <style lang="scss" scoped>
280
+
281
+ .apos-area-menu.apos-is-focused ::v-deep .apos-context-menu__inner {
282
+ border: 1px solid var(--a-base-4);
283
+ }
284
+
285
+ .apos-area-menu.apos-is-focused ::v-deep .apos-context-menu__tip-outline {
286
+ stroke: var(--a-base-4);
287
+ }
288
+
289
+ .apos-area-menu__wrapper,
290
+ .apos-area-menu__items,
291
+ .apos-area-menu__group-list {
292
+ @include apos-list-reset();
293
+ }
294
+
295
+ .apos-area-menu__wrapper {
296
+ min-width: 250px;
297
+ }
298
+
299
+ .apos-area-menu__button {
300
+ @include apos-button-reset();
301
+ @include type-base;
302
+ box-sizing: border-box;
303
+ width: 100%;
304
+ padding: 5px 20px;
305
+ color: var(--a-base-1);
306
+
307
+ &:hover,
308
+ &:focus {
309
+ & ::v-deep .apos-area-menu__item-icon {
310
+ color: var(--a-primary);
311
+ }
312
+ }
313
+
314
+ &:hover {
315
+ cursor: pointer;
316
+ color: var(--a-text-primary);
317
+ }
318
+
319
+ &:focus {
320
+ outline: none;
321
+ color: var(--a-text-primary);
322
+ }
323
+
324
+ &:active {
325
+ color: var(--a-base-1);
326
+ }
327
+ }
328
+
329
+ .apos-area-menu__accordion-trigger {
330
+ z-index: $z-index-under;
331
+ opacity: 0;
332
+ position: absolute;
333
+ }
334
+
335
+ .apos-area-menu__group-label {
336
+ @include apos-button-reset();
337
+ box-sizing: border-box;
338
+ display: flex;
339
+ width: 100%;
340
+ justify-content: space-between;
341
+ padding: 10px 20px;
342
+ &:hover {
343
+ cursor: pointer;
344
+ }
345
+
346
+ &:focus {
347
+ background-color: var(--a-base-10);
348
+ outline: 1px solid var(--a-base-4);
349
+ }
350
+ }
351
+
352
+ .apos-area-menu__group-chevron {
353
+ @include apos-transition();
354
+ transform: rotate(90deg);
355
+ }
356
+
357
+ .apos-area-menu__group-chevron.apos-is-active {
358
+ transform: rotate(180deg);
359
+ }
360
+
361
+ .apos-area-menu__group {
362
+ border-bottom: 1px solid var(--a-base-8);
363
+ padding-bottom: 10px;
364
+ margin: 10px 0;
365
+ }
366
+ .apos-area-menu__item:last-child.apos-has-group .apos-area-menu__group {
367
+ border-bottom: none;
368
+ margin-bottom: 0;
369
+ }
370
+
371
+ .apos-area-menu__items--accordion {
372
+ overflow: hidden;
373
+ max-height: 0;
374
+ @include apos-transition($duration:0.3s);
375
+ }
376
+
377
+ .apos-area-menu__items--accordion.apos-is-active {
378
+ transition-delay: 0.25s;
379
+ max-height: 20rem;
380
+ }
381
+
382
+ </style>
@@ -26,9 +26,10 @@
26
26
  :context-menu-options="contextMenuOptions"
27
27
  :empty="true"
28
28
  :index="0"
29
- :widget-options="options.widgets"
29
+ :options="options"
30
30
  :max-reached="maxReached"
31
31
  :disabled="field && field.readOnly"
32
+ :widget-options="options.widgets"
32
33
  />
33
34
  </template>
34
35
  </div>
@@ -138,7 +139,8 @@ export default {
138
139
  contextMenuOptions: {
139
140
  menu: this.choices
140
141
  },
141
- edited: {}
142
+ edited: {},
143
+ widgets: {}
142
144
  };
143
145
  },
144
146
  computed: {
@@ -164,7 +166,7 @@ export default {
164
166
  return window.apos.area;
165
167
  },
166
168
  types() {
167
- return Object.keys(this.options.widgets);
169
+ return Object.keys(this.widgets);
168
170
  },
169
171
  maxReached() {
170
172
  return this.options.max && this.next.length >= this.options.max;
@@ -195,6 +197,16 @@ export default {
195
197
  this.next = this.getValidItems();
196
198
  }
197
199
  },
200
+ created() {
201
+ if (this.options.groups) {
202
+ for (const group of Object.keys(this.options.groups)) {
203
+ this.widgets = {
204
+ ...this.options.groups[group].widgets,
205
+ ...this.widgets
206
+ };
207
+ }
208
+ }
209
+ },
198
210
  mounted() {
199
211
  apos.bus.$on('area-updated', this.areaUpdatedHandler);
200
212
  apos.bus.$on('widget-hover', this.updateWidgetHovered);
@@ -433,7 +445,7 @@ export default {
433
445
  apos.area.activeEditor = this;
434
446
  const widget = await apos.modal.execute(componentName, {
435
447
  value: null,
436
- options: this.options.widgets[name],
448
+ options: this.widgetOptionsByType(name),
437
449
  type: name,
438
450
  docId: this.docId
439
451
  });
@@ -446,6 +458,18 @@ export default {
446
458
  }
447
459
  }
448
460
  },
461
+ widgetOptionsByType(name) {
462
+ if (this.options.widgets) {
463
+ return this.options.widgets[name];
464
+ } else if (this.options.expanded) {
465
+ for (const info of Object.values(this.options.groups || {})) {
466
+ if (info?.widgets?.[name]) {
467
+ return info.widgets[name];
468
+ }
469
+ }
470
+ }
471
+ return null;
472
+ },
449
473
  contextualWidgetDefaultData(type) {
450
474
  return this.moduleOptions.contextualWidgetDefaultData[type];
451
475
  },