apostrophe 3.51.0 → 3.52.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/.eslintrc CHANGED
@@ -12,6 +12,7 @@
12
12
  "rules": {
13
13
  "no-var": "error",
14
14
  "no-console": 0,
15
+ "vue/no-deprecated-v-on-native-modifier": 0,
15
16
  "multiline-ternary": "off",
16
17
  "no-unused-vars": [
17
18
  "error",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.52.0 (2023-07-06)
4
+
5
+ ### Changes
6
+ * Foreign widget UI no longer uses inverted theme styles.
7
+
8
+ ### Adds
9
+ * Allows users to double-click a nested widget's breadcrumb entry and open its editor.
10
+ * Adds support for a new `conditions` property in `addContextOperation` and validation of `addContextOperation` configuration.
11
+
12
+ ### Fixes
13
+
14
+ * The API now allows the user to create a page without defining the page target ID. By default it takes the Home page.
15
+ * Users are no longer blocked from saving documents when a field is hidden
16
+ by an `if` condition fails to satisfy a condition such as `min` or `max`
17
+ or is otherwise invalid. Instead the invalid value is discarded for safety.
18
+ Note that `required` has always been ignored when an `if` condition is not
19
+ satisfied.
20
+
21
+ ## 3.51.1 (2023-06-23)
22
+
23
+ ## Fixes
24
+
25
+ * Fix a regression introduced in 3.51.0 - conditional fields work again in the array editor dialog box.
26
+
3
27
  ## 3.51.0 (2023-06-21)
4
28
 
5
29
  ### Adds
@@ -41,13 +41,13 @@
41
41
  <li class="apos-area-widget__breadcrumb" data-apos-widget-breadcrumb="0">
42
42
  <AposButton
43
43
  type="quiet"
44
- @click="foreign ? $emit('edit', i) : false"
44
+ @click="foreign ? $emit('edit', i) : null"
45
+ @dblclick.native="(!foreign && !isContextual) ? $emit('edit', i) : null"
45
46
  :label="foreign ? {
46
47
  key: 'apostrophe:editWidgetType',
47
48
  label: $t(widgetLabel)
48
49
  } : widgetLabel"
49
- tooltip="apostrophe:editWidgetForeignTooltip"
50
- :icon="foreign ? 'earth-icon' : null"
50
+ :tooltip="!isContextual && 'apostrophe:editWidgetForeignTooltip'"
51
51
  :icon-size="11"
52
52
  :modifiers="['no-motion']"
53
53
  />
@@ -76,6 +76,7 @@
76
76
  :class="ui.controls"
77
77
  >
78
78
  <AposWidgetControls
79
+ v-if="!foreign"
79
80
  :first="i === 0"
80
81
  :last="i === next.length - 1"
81
82
  :options="{ contextual: isContextual }"
@@ -272,7 +273,8 @@ export default {
272
273
  };
273
274
  },
274
275
  widgetIcon() {
275
- return this.contextMenuOptions.menu.filter(item => item.name === this.widget.type)[0]?.icon || 'shape-icon';
276
+ const natural = this.contextMenuOptions.menu.filter(item => item.name === this.widget.type)[0]?.icon || 'shape-icon';
277
+ return this.foreign ? 'earth-icon' : natural;
276
278
  },
277
279
  widgetLabel() {
278
280
  const moduleName = `${this.widget.type}-widget`;
@@ -694,7 +696,7 @@ export default {
694
696
 
695
697
  .apos-area-widget__label {
696
698
  position: absolute;
697
- top: -8px;
699
+ top: 0;
698
700
  right: 0;
699
701
  display: flex;
700
702
  transform: translateY(-100%);
@@ -709,20 +711,13 @@ export default {
709
711
  @include apos-list-reset();
710
712
  display: flex;
711
713
  align-items: center;
712
- margin: 0;
714
+ margin: 0 0 8px;
713
715
  padding: 4px 6px;
714
716
  background-color: var(--a-background-primary);
715
717
  border: 1px solid var(--a-primary-transparent-50);
716
718
  border-radius: 8px;
717
719
  }
718
720
 
719
- .apos-area-widget-wrapper--foreign .apos-area-widget-inner .apos-area-widget__breadcrumbs {
720
- background-color: var(--a-background-inverted);
721
- & ::v-deep .apos-button__content {
722
- color: var(--a-text-inverted);
723
- }
724
- }
725
-
726
721
  .apos-area-widget__breadcrumb,
727
722
  .apos-area-widget__breadcrumb ::v-deep .apos-button__content {
728
723
  @include type-help;
@@ -730,9 +725,6 @@ export default {
730
725
  white-space: nowrap;
731
726
  color: var(--a-base-1);
732
727
  transition: background-color 0.3s var(--a-transition-timing-bounce);
733
- &:hover {
734
- cursor: pointer;
735
- }
736
728
  }
737
729
 
738
730
  .apos-area-widget__breadcrumbs:hover .apos-area-widget__breadcrumb,
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="apos-area-modify-controls">
3
3
  <AposButtonGroup
4
- :modifiers="groupModifiers"
4
+ :modifiers="[ 'vertical' ]"
5
5
  >
6
6
  <AposButton
7
7
  v-if="!foreign"
@@ -124,15 +124,6 @@ export default {
124
124
  };
125
125
  },
126
126
  computed: {
127
- groupModifiers() {
128
- const mods = [ 'vertical' ];
129
-
130
- if (this.foreign) {
131
- mods.push('invert');
132
- }
133
-
134
- return mods;
135
- },
136
127
  upButton() {
137
128
  return {
138
129
  ...this.buttonDefaults,
@@ -1153,9 +1153,10 @@ module.exports = {
1153
1153
  // context: 'update',
1154
1154
  // action: 'someAction',
1155
1155
  // modal: 'ModalComponent',
1156
- // label: 'Context Menu Label'
1156
+ // label: 'Context Menu Label',
1157
+ // conditions: ['canEdit']
1157
1158
  // }
1158
- // All properties are required.
1159
+ // All properties are required except conditions.
1159
1160
  // The only supported `context` for now is `update`.
1160
1161
  // `action` is the operation identifier and should be globally unique.
1161
1162
  // Overriding existing custom actions is possible (the last wins).
@@ -1166,7 +1167,13 @@ module.exports = {
1166
1167
  // An optional `manuallyPublished` boolean property is supported - if true
1167
1168
  // the menu will be shown only for docs which have `autopublish: false` and
1168
1169
  // `localized: true` options.
1170
+ // `conditions` defines the needed permission actions to be run on the current doc
1171
+ // in order to display the operation.
1172
+ // It must be an array containing one or multiple of these available values:
1173
+ // 'canPublish', 'canEdit', 'canDismissSubmission', 'canDiscardDraft',
1174
+ // 'canLocalize', 'canArchive', 'canUnpublish', 'canCopy', 'canRestore'.
1169
1175
  addContextOperation(moduleName, operation) {
1176
+ validate(operation);
1170
1177
  self.contextOperations = [
1171
1178
  ...self.contextOperations
1172
1179
  .filter(op => op.action !== operation.action),
@@ -1175,6 +1182,39 @@ module.exports = {
1175
1182
  moduleName
1176
1183
  }
1177
1184
  ];
1185
+
1186
+ function validate ({
1187
+ action, context, label, modal, conditions
1188
+ }) {
1189
+ const allowedConditions = [
1190
+ 'canPublish',
1191
+ 'canEdit',
1192
+ 'canDismissSubmission',
1193
+ 'canDiscardDraft',
1194
+ 'canLocalize',
1195
+ 'canArchive',
1196
+ 'canUnpublish',
1197
+ 'canCopy',
1198
+ 'canRestore'
1199
+ ];
1200
+
1201
+ if (!action || !context || !label || !modal) {
1202
+ throw self.apos.error('invalid', 'addContextOperation requires action, context, label and modal properties');
1203
+ }
1204
+
1205
+ if (!conditions) {
1206
+ return;
1207
+ }
1208
+
1209
+ if (
1210
+ !Array.isArray(conditions) ||
1211
+ conditions.some((perm) => !allowedConditions.includes(perm))
1212
+ ) {
1213
+ throw self.apos.error(
1214
+ 'invalid', `The conditions property in addContextOperation must be an array containing one or multiple of these values:\n\t${allowedConditions.join('\n\t')}.`
1215
+ );
1216
+ }
1217
+ }
1178
1218
  },
1179
1219
  getBrowserData(req) {
1180
1220
  return {
@@ -195,15 +195,26 @@ export default {
195
195
  return menus;
196
196
  },
197
197
  customOperationsByContext() {
198
- return this.customOperations.filter(op => {
199
- if (typeof op.manuallyPublished === 'boolean' && op.manuallyPublished !== this.manuallyPublished) {
198
+ return this.customOperations.filter(({
199
+ manuallyPublished, hasUrl, conditions, context
200
+ }) => {
201
+ if (typeof manuallyPublished === 'boolean' && manuallyPublished !== this.manuallyPublished) {
200
202
  return false;
201
203
  }
202
- if (typeof op.hasUrl === 'boolean' && op.hasUrl !== this.hasUrl) {
204
+
205
+ if (typeof hasUrl === 'boolean' && hasUrl !== this.hasUrl) {
203
206
  return false;
204
207
  }
205
208
 
206
- return op.context === 'update' && this.isUpdateOperation;
209
+ if (conditions) {
210
+ const notAllowed = conditions.some((action) => !this[action]);
211
+
212
+ if (notAllowed) {
213
+ return false;
214
+ }
215
+ }
216
+
217
+ return context === 'update' && this.isUpdateOperation;
207
218
  });
208
219
  },
209
220
  moduleName() {
@@ -133,7 +133,8 @@ export default {
133
133
  return conditionalFields(
134
134
  this.schema,
135
135
  this.getFieldsByCategory(followedByCategory),
136
- this.docFields.data,
136
+ // currentDoc for arrays, docFields for all other editors
137
+ this.currentDoc ? this.currentDoc.data : this.docFields.data,
137
138
  this.externalConditionsResults
138
139
  );
139
140
  },
@@ -254,14 +254,13 @@ module.exports = {
254
254
  // This call is atomic with respect to other REST write operations on pages.
255
255
  async post(req) {
256
256
  self.publicApiCheck(req);
257
- req.body._position = req.body._position || 'lastChild';
258
257
  let targetId = self.apos.launder.string(req.body._targetId);
259
- let position = self.apos.launder.string(req.body._position);
258
+ let position = self.apos.launder.string(req.body._position || 'lastChild');
260
259
  // Here we have to normalize before calling insert because we
261
260
  // need the parent page to call newChild(). insert calls again but
262
261
  // sees there's no work to be done, so no performance hit
263
262
  const normalized = await self.getTargetIdAndPosition(req, null, targetId, position);
264
- targetId = normalized.targetId;
263
+ targetId = normalized.targetId || '_home';
265
264
  position = normalized.position;
266
265
  const copyingId = self.apos.launder.id(req.body._copyingId);
267
266
  const input = _.omit(req.body, '_targetId', '_position', '_copyingId');
@@ -273,7 +272,7 @@ module.exports = {
273
272
  if (req.body._newInstance) {
274
273
  // If we're looking for a fresh page instance and aren't saving yet,
275
274
  // simply get a new page doc and return it
276
- const parentPage = await self.findForEditing(req, { _id: targetId })
275
+ const parentPage = await self.findForEditing(req, self.getIdCriteria(targetId))
277
276
  .permission('edit', '@apostrophecms/any-page-type').toObject();
278
277
  const newChild = self.newChild(parentPage);
279
278
  newChild._previewable = true;
@@ -281,7 +280,12 @@ module.exports = {
281
280
  }
282
281
 
283
282
  return self.withLock(req, async () => {
284
- const targetPage = await self.findForEditing(req, targetId ? self.getIdCriteria(targetId) : { level: 0 }).ancestors(true).permission('edit').toObject();
283
+ const targetPage = await self
284
+ .findForEditing(req, self.getIdCriteria(targetId))
285
+ .ancestors(true)
286
+ .permission('edit')
287
+ .toObject();
288
+
285
289
  if (!targetPage) {
286
290
  throw self.apos.error('notfound');
287
291
  }
@@ -523,15 +523,29 @@ module.exports = {
523
523
  const errorsList = [];
524
524
 
525
525
  for (const error of errors) {
526
- if ((error.name === 'required' || error.name === 'mandatory')) {
526
+ if (error.path) {
527
527
  // `self.isVisible` will only throw for required fields that have
528
528
  // an external condition containing an unknown module or method:
529
529
  const isVisible = await self.isVisible(req, schema, destination, error.path);
530
530
 
531
531
  if (!isVisible) {
532
- // It is not reasonable to enforce required for
533
- // fields hidden via conditional fields
534
- continue;
532
+ // It is not reasonable to enforce required,
533
+ // min, max or anything else for fields
534
+ // hidden via "if" as the user cannot correct it
535
+ // and it will not be used. If the user changes
536
+ // the conditional field later then they won't
537
+ // be able to save until the erroneous field
538
+ // is corrected
539
+ const name = error.path;
540
+ const field = schema.find(field => field.name === name);
541
+ if (field) {
542
+ // To protect against security issues, an invalid value
543
+ // for a field that is not visible should be quietly discarded.
544
+ // We only worry about this if the value is not valid, as otherwise
545
+ // it's a kindness to save the work so the user can toggle back to it
546
+ destination[field.name] = klona((field.def !== undefined) ? field.def : self.fieldTypes[field.type]?.def);
547
+ continue;
548
+ }
535
549
  }
536
550
  }
537
551
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.51.0",
3
+ "version": "3.52.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {