apostrophe 3.60.0 → 3.61.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 (35) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/index.js +3 -3
  3. package/modules/@apostrophecms/area/index.js +22 -0
  4. package/modules/@apostrophecms/cache/index.js +9 -0
  5. package/modules/@apostrophecms/doc-type/index.js +32 -6
  6. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +1 -1
  7. package/modules/@apostrophecms/login/index.js +1 -4
  8. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +1 -0
  9. package/modules/@apostrophecms/module/index.js +2 -2
  10. package/modules/@apostrophecms/notification/index.js +9 -9
  11. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +44 -17
  12. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +2 -2
  13. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +33 -6
  14. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +15 -8
  15. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Image.js +2 -1
  16. package/modules/@apostrophecms/schema/index.js +3 -2
  17. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +26 -4
  18. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +8 -4
  19. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +2 -0
  20. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +4 -2
  21. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +1 -0
  22. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +4 -2
  23. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +2 -0
  24. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +10 -0
  25. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -0
  26. package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +3 -0
  27. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js +3 -0
  28. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +3 -0
  29. package/modules/@apostrophecms/template/index.js +8 -4
  30. package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +14 -8
  31. package/modules/@apostrophecms/ui/ui/apos/components/AposLabel.vue +16 -1
  32. package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +5 -0
  33. package/modules/@apostrophecms/ui/ui/apos/mixins/AposAdvisoryLockMixin.js +1 -1
  34. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +4 -0
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.61.0 (2023-12-21)
4
+
5
+ ### Adds
6
+
7
+ * Add a `validate` method to the `url` field type to allow the use of the `pattern` property.
8
+ * Add `autocomplete` attribute to schema fields that implement it (cf. [HTML attribute: autocomplete](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete)).
9
+ * Add the `delete` method to the `@apostrophecms/cache` module so we don't have to rely on direct MongoDB manipulation to remove a cache item.
10
+ * Adds tag property to fields in order to show a tag next to the field title (used in advanced permission for the admin field). Adds new sensitive label color.
11
+ * Pass on the module name and the full, namespaced template name to external front ends, e.g. Astro.
12
+ Also make this information available to other related methods for future and project-level use.
13
+ * Fixes the AposCheckbox component to be used more easily standalone, accepts a single model value instead of an array.
14
+
15
+ ### Fixes
16
+
17
+ * Fix `date` schema field query builder to work with arrays.
18
+ * Fix `if` on pages. When you open the `AposDocEditor` modal on pages, you now see an up to date view of the visible fields.
19
+ * Pass on complete annotation information for nested areas when adding or editing a nested widget using an external front, like Astro.
20
+ * We can now close the image modal in rich-text widgets when we click outside of the modal.
21
+ The click on the cancel button now works too.
22
+ * Fixes the `clearLoginAttempts` method to work with the new `@apostrophecms/cache` module `delete` method.
23
+
24
+ ## 3.60.1 (2023-12-06)
25
+
26
+ ### Fixes
27
+
28
+ * corrected an issue where the use of the doc template library can result in errors at startup when
29
+ replicating certain content to new locales. This was not a bug in the doc template library.
30
+ Apostrophe was not invoking `findForEditing` where it should have.
31
+
3
32
  ## 3.60.0 (2023-11-29)
4
33
 
5
34
  ### Adds
@@ -510,6 +539,7 @@ those writing mocha tests of Apostrophe modules.
510
539
  * Hide the suggestion help from the relationship input list when the user starts typing a search term.
511
540
  * Hide the suggestion hint from the relationship input list when the user starts typing a search term except when there are no matches to display.
512
541
  * Disable context menu for related items when their `relationship` field has no sub-[`fields`](https://v3.docs.apostrophecms.org/guide/relationships.html#providing-context-with-fields) configured.
542
+ * Logic for checking whether we are running a unit test of an external module under mocha now uses `includes` for a simpler, safer test that should be more cross-platform.
513
543
 
514
544
  ## 3.42.0 (2023-03-16)
515
545
 
package/index.js CHANGED
@@ -525,12 +525,12 @@ async function apostrophe(options, telemetry, rootSpan) {
525
525
  // and throws an exception if we don't
526
526
  function findTestModule() {
527
527
  let m = module;
528
- const nodeModuleRegex = new RegExp(`node_modules${path.sep}mocha`);
529
- if (!require.main.filename.match(nodeModuleRegex)) {
528
+ const testFor = `node_modules${path.sep}mocha`;
529
+ if (!require.main.filename.includes(testFor)) {
530
530
  throw new Error('mocha does not seem to be running, is this really a test?');
531
531
  }
532
532
  while (m) {
533
- if (m.parent && m.parent.filename.match(nodeModuleRegex)) {
533
+ if (m.parent && m.parent.filename.includes(testFor)) {
534
534
  return m;
535
535
  } else if (!m.parent) {
536
536
  // Mocha v10 doesn't inject mocha paths inside `module`, therefore, we only detect the parent until the last parent. But we can get Mocha running using `require.main` - Amin
@@ -69,6 +69,28 @@ module.exports = {
69
69
  }
70
70
  async function render() {
71
71
  if (req.aposExternalFront) {
72
+ // Simulate an area and annotate it so that the
73
+ // widget's sub-areas wind up with the right metadata
74
+ const area = {
75
+ metaType: 'area',
76
+ items: [ widget ],
77
+ _docId
78
+ };
79
+ self.apos.template.annotateAreaForExternalFront(field, area);
80
+ // Annotate sub-areas. It's like annotating a doc, but not quite,
81
+ // so this logic is reproduced partially
82
+ self.apos.doc.walk(area, (o, k, v) => {
83
+ if (v && v.metaType === 'area') {
84
+ const manager = self.apos.util.getManagerOf(o);
85
+ if (!manager) {
86
+ self.apos.util.warnDevOnce('noManagerForDocInExternalFront', `No manager for: ${o.metaType} ${o.type || ''}`);
87
+ return;
88
+ }
89
+ const field = manager.schema.find(f => f.name === k);
90
+ v._docId = _docId;
91
+ self.apos.template.annotateAreaForExternalFront(field, v);
92
+ }
93
+ });
72
94
  const result = {
73
95
  ...req.data,
74
96
  options,
@@ -79,6 +79,15 @@ module.exports = {
79
79
  return self.cacheCollection.removeMany({ namespace });
80
80
  },
81
81
 
82
+ // Delete a single key and value from the cache.
83
+
84
+ async delete(namespace, username) {
85
+ await self.cacheCollection.deleteOne({
86
+ namespace,
87
+ key: username
88
+ });
89
+ },
90
+
82
91
  // Establish the collection that will store all of the caches and
83
92
  // set up its indexes.
84
93
 
@@ -225,8 +225,8 @@ module.exports = {
225
225
  handlers(self) {
226
226
  return {
227
227
  beforeSave: {
228
- prepareForStorage(req, doc) {
229
- self.apos.schema.prepareForStorage(req, doc);
228
+ prepareForStorage(req, doc, options) {
229
+ self.apos.schema.prepareForStorage(req, doc, options);
230
230
  },
231
231
  async updateCacheField(req, doc) {
232
232
  await self.updateCacheField(req, doc);
@@ -1028,9 +1028,16 @@ module.exports = {
1028
1028
  });
1029
1029
  const toId = draft._id.replace(`:${draft.aposLocale}`, `:${toLocale}:draft`);
1030
1030
  const actionModule = self.apos.page.isPage(draft) ? self.apos.page : self;
1031
+ // Use findForEditing so that we are successful even for edge cases
1032
+ // like doc templates that don't appear in public renderings, but
1033
+ // also use permission('view') so that we are not actually restricted
1034
+ // to what we can edit, avoiding any confusion about whether there
1035
+ // is really an existing localized doc or not and preventing the
1036
+ // possibility of inserting an unwanted duplicate. The update() call will
1037
+ // still stop us if edit permissions are an issue
1031
1038
  const existing = await actionModule.findForEditing(toReq, {
1032
1039
  _id: toId
1033
- }).toObject();
1040
+ }).permission('view').toObject();
1034
1041
  // We only want to copy schema properties, leave non-schema
1035
1042
  // properties of the source document alone
1036
1043
  const data = Object.fromEntries(Object.entries(draft).filter(([ key, value ]) => self.schema.find(field => field.name === key)));
@@ -1056,8 +1063,15 @@ module.exports = {
1056
1063
  // A page that is not the home page, being replicated for the first time
1057
1064
  let { lastTargetId, lastPosition } = await self.apos.page.inferLastTargetIdAndPosition(draft);
1058
1065
  let localizedTargetId = lastTargetId.replace(`:${draft.aposLocale}`, `:${toLocale}:draft`);
1066
+ // When fetching the target (parent or peer), always use findForEditing
1067
+ // so we don't miss doc templates and other edge cases, but also use
1068
+ // .permission('view') because we are not actually editing the target
1069
+ // and should not be blocked over edit permissions. Later change this check
1070
+ // to 'create' ("can create a child of this doc"), but not until we're ready
1071
+ // to do it for all creation attempts
1059
1072
  const localizedTarget = await actionModule
1060
- .find(toReq, self.apos.page.getIdCriteria(localizedTargetId))
1073
+ .findForEditing(toReq, self.apos.page.getIdCriteria(localizedTargetId))
1074
+ .permission('view')
1061
1075
  .archived(null)
1062
1076
  .areas(false)
1063
1077
  .relationships(false)
@@ -1070,7 +1084,13 @@ module.exports = {
1070
1084
  parentNotLocalized: true
1071
1085
  });
1072
1086
  } else {
1073
- const originalTarget = await actionModule.find(req, self.apos.page.getIdCriteria(lastTargetId)).archived(null).areas(false).relationships(false).toObject();
1087
+ const originalTarget = await actionModule
1088
+ .findForEditing(req, self.apos.page.getIdCriteria(lastTargetId))
1089
+ .permission('view')
1090
+ .archived(null)
1091
+ .areas(false)
1092
+ .relationships(false)
1093
+ .toObject();
1074
1094
  if (!originalTarget) {
1075
1095
  // Almost impossible (race conditions like someone removing it while we're in the modal)
1076
1096
  throw self.apos.error('notfound');
@@ -1078,7 +1098,13 @@ module.exports = {
1078
1098
  const criteria = {
1079
1099
  path: self.apos.page.getParentPath(originalTarget)
1080
1100
  };
1081
- const localizedTarget = await actionModule.find(toReq, criteria).archived(null).areas(false).relationships(false).toObject();
1101
+ const localizedTarget = await actionModule
1102
+ .findForEditing(toReq, criteria)
1103
+ .permission('view')
1104
+ .archived(null)
1105
+ .areas(false)
1106
+ .relationships(false)
1107
+ .toObject();
1082
1108
  if (!localizedTarget) {
1083
1109
  throw self.apos.error('notfound', req.t('apostrophe:parentNotLocalized'), {
1084
1110
  // Also provide as data for code that prefers to localize client side
@@ -350,8 +350,8 @@ export default {
350
350
  type: this.$t(this.moduleOptions.label)
351
351
  };
352
352
  if (this.docId) {
353
- this.evaluateConditions();
354
353
  await this.loadDoc();
354
+ this.evaluateConditions();
355
355
  try {
356
356
  if (this.manuallyPublished) {
357
357
  this.published = await apos.http.get(this.getOnePath, {
@@ -890,10 +890,7 @@ module.exports = {
890
890
  },
891
891
 
892
892
  async clearLoginAttempts (username, namespace = loginAttemptsNamespace) {
893
- await self.apos.cache.cacheCollection.deleteOne({
894
- namespace,
895
- key: username
896
- });
893
+ await self.apos.cache.delete(namespace, username);
897
894
  },
898
895
 
899
896
  addToAdminBar() {
@@ -55,6 +55,7 @@ export default {
55
55
  async handler() {
56
56
  if (this.moduleName === '@apostrophecms/page') {
57
57
  await this.evaluateExternalConditions();
58
+ this.evaluateConditions();
58
59
  }
59
60
  }
60
61
  }
@@ -432,8 +432,8 @@ module.exports = {
432
432
  await self.apos.area.loadDeferredWidgets(req);
433
433
  if (req.aposExternalFront) {
434
434
  data = self.apos.template.getRenderDataArgs(req, data, self);
435
- await self.apos.template.annotateDataForExternalFront(req, template, data);
436
- self.apos.template.pruneDataForExternalFront(req, template, data);
435
+ await self.apos.template.annotateDataForExternalFront(req, template, data, self.__meta.name);
436
+ self.apos.template.pruneDataForExternalFront(req, template, data, self.__meta.name);
437
437
  // Reply with JSON
438
438
  return data;
439
439
  }
@@ -1,6 +1,6 @@
1
1
  // This module provides a framework for triggering notifications
2
2
  // within the Apostrophe admin UI. Notifications may be triggered
3
- // either on the browser or the server side, via `apos.notice`.
3
+ // either on the browser or the server side, via `apos.notify`.
4
4
  //
5
5
  // ## Options
6
6
  //
@@ -209,17 +209,17 @@ module.exports = {
209
209
  action: self.action
210
210
  };
211
211
  },
212
- // Call with `req`, then a message key as found in the localization files,
213
- // followed by an `options` object if desired.
214
- //
215
- // If you do not have a `req` it is acceptable to pass a user `_id` string
212
+ // When used server-side, call with `req` as the first argument,
213
+ // or if you do not have a `req` it is acceptable to pass a user `_id` string
216
214
  // in place of `req`. Someone must be the recipient.
217
215
  //
218
- // `message` should be a key that exists in a localization file. If it does not
219
- // it will be displayed directly as a fallback.
216
+ // When called client-side, there is no req argument because the recipient is always the current user.
217
+ //
218
+ // The `message` argument should be a key that exists in a localization file.
219
+ // If it does not, it will be displayed directly as a fallback.
220
220
  //
221
- // `options.type` styles the notification and may be set to `error`,
222
- // `warning` or `success`. If not set, a "plain" default style is used.
221
+ // The `options.type` styles the notification and may be set to `error`, `danger`,
222
+ // `warning`, `info` or `success`. If not set, a "plain" default style is used.
223
223
  //
224
224
  // If `options.dismiss` is set to `true`, the message will auto-dismiss after 5 seconds.
225
225
  // If it is set to a number of seconds, it will dismiss after that number of seconds.
@@ -1,12 +1,12 @@
1
1
  <template>
2
2
  <div
3
3
  v-if="active"
4
- v-click-outside-element="cancel"
4
+ v-click-outside-element="close"
5
5
  class="apos-popover apos-image-control__dialog"
6
6
  x-placement="bottom"
7
7
  :class="{
8
8
  'apos-is-triggered': active,
9
- 'apos-has-selection': true
9
+ 'apos-has-selection': hasSelection
10
10
  }"
11
11
  >
12
12
  <AposContextMenuDialog
@@ -26,7 +26,7 @@
26
26
  <footer class="apos-image-control__footer">
27
27
  <AposButton
28
28
  type="default" label="apostrophe:cancel"
29
- @click="cancel"
29
+ @click="close"
30
30
  :modifiers="formModifiers"
31
31
  />
32
32
  <AposButton
@@ -55,9 +55,14 @@ export default {
55
55
  active: {
56
56
  type: Boolean,
57
57
  required: true
58
+ },
59
+ hasSelection: {
60
+ type: Boolean,
61
+ required: true,
62
+ default: false
58
63
  }
59
64
  },
60
- emits: [ 'before-commands', 'done', 'cancel' ],
65
+ emits: [ 'before-commands', 'close' ],
61
66
  data() {
62
67
  return {
63
68
  generation: 1,
@@ -92,27 +97,52 @@ export default {
92
97
  {
93
98
  name: 'caption',
94
99
  label: this.$t('apostrophe:caption'),
95
- type: 'string'
100
+ type: 'string',
101
+ def: ''
96
102
  }
97
103
  ]
98
104
  };
99
105
  },
100
106
  computed: {
107
+ attributes() {
108
+ return this.editor.getAttributes('image');
109
+ },
101
110
  lastSelectionTime() {
102
- return this.editor.view.lastSelectionTime;
111
+ return this.editor.view.input.lastSelectionTime;
103
112
  },
104
113
  schema() {
105
114
  return this.originalSchema;
106
115
  }
107
116
  },
108
117
  watch: {
109
- active(newVal) {
118
+ 'attributes.imageId': {
119
+ handler(newVal, oldVal) {
120
+ if (newVal === oldVal) {
121
+ return;
122
+ }
123
+
124
+ this.close();
125
+ }
126
+ },
127
+ active(newVal, oldVal) {
110
128
  if (newVal) {
111
129
  window.addEventListener('keydown', this.keyboardHandler);
112
- this.populateFields();
113
130
  } else {
114
131
  window.removeEventListener('keydown', this.keyboardHandler);
115
132
  }
133
+
134
+ if (newVal !== oldVal && this.hasSelection) {
135
+ this.populateFields();
136
+ this.evaluateConditions();
137
+ }
138
+ },
139
+ lastSelectionTime(newVal, oldVal) {
140
+ if (newVal === oldVal) {
141
+ return;
142
+ }
143
+
144
+ this.populateFields();
145
+ this.evaluateConditions();
116
146
  }
117
147
  },
118
148
  async mounted() {
@@ -120,11 +150,8 @@ export default {
120
150
  this.evaluateConditions();
121
151
  },
122
152
  methods: {
123
- cancel() {
124
- this.$emit('cancel');
125
- },
126
- done() {
127
- this.$emit('done');
153
+ close() {
154
+ this.$emit('close');
128
155
  },
129
156
  save() {
130
157
  this.triggerValidation = true;
@@ -142,24 +169,24 @@ export default {
142
169
  style: this.docFields.data.style,
143
170
  alt: this.docFields.data.alt
144
171
  });
145
- this.done();
172
+ this.close();
146
173
  });
147
174
  },
148
175
  keyboardHandler(e) {
149
176
  if (e.keyCode === 27) {
150
- this.cancel();
177
+ this.close();
151
178
  }
152
179
  if (e.keyCode === 13) {
153
180
  if (this.docFields.data.href || e.metaKey) {
154
181
  this.save();
155
- this.done();
182
+ this.close();
156
183
  }
157
184
  e.preventDefault();
158
185
  }
159
186
  },
160
187
  async populateFields() {
161
188
  try {
162
- const attrs = this.editor.getAttributes('image');
189
+ const attrs = this.attributes;
163
190
  this.docFields.data = {};
164
191
  this.schema.forEach((item) => {
165
192
  this.docFields.data[item.name] = attrs[item.name] || '';
@@ -107,7 +107,7 @@ export default {
107
107
  return this.editor.isActive('anchor') || this.active;
108
108
  },
109
109
  lastSelectionTime() {
110
- return this.editor.view.lastSelectionTime;
110
+ return this.editor.view.input.lastSelectionTime;
111
111
  },
112
112
  hasSelection() {
113
113
  const { state } = this.editor;
@@ -129,7 +129,7 @@ export default {
129
129
  window.removeEventListener('keydown', this.keyboardHandler);
130
130
  }
131
131
  },
132
- 'editor.view.lastSelectionTime': {
132
+ 'editor.view.input.lastSelectionTime': {
133
133
  handler(newVal, oldVal) {
134
134
  this.populateFields();
135
135
  }
@@ -7,19 +7,19 @@
7
7
  :label="tool.label"
8
8
  :icon-only="!!tool.icon"
9
9
  :icon="tool.icon || false"
10
+ :icon-size="tool.iconSize || 16"
10
11
  :modifiers="['no-border', 'no-motion']"
11
12
  :tooltip="{
12
13
  content: tool.label,
13
14
  placement: 'top',
14
15
  delay: 650
15
16
  }"
16
- @close="close"
17
17
  />
18
18
  <AposImageControlDialog
19
19
  :active="active"
20
20
  :editor="editor"
21
+ :has-selection="hasSelection"
21
22
  @close="close"
22
- @click.stop="$event => null"
23
23
  />
24
24
  </div>
25
25
  </template>
@@ -42,18 +42,45 @@ export default {
42
42
  active: false
43
43
  };
44
44
  },
45
+ watch: {
46
+ hasSelection(newVal, oldVal) {
47
+ if (!newVal) {
48
+ this.close();
49
+ }
50
+ }
51
+ },
45
52
  computed: {
53
+ attributes() {
54
+ return this.editor.getAttributes('image');
55
+ },
46
56
  buttonActive() {
47
- return this.editor.isActive('image');
57
+ return this.attributes.imageId || this.active;
58
+ },
59
+ hasSelection() {
60
+ const { state } = this.editor;
61
+ const { selection } = this.editor.state;
62
+
63
+ // Text is selected
64
+ const { from, to } = selection;
65
+ const text = state.doc.textBetween(from, to, '');
66
+
67
+ // Image node is selected
68
+ const { content = [] } = selection.content().content;
69
+ const [ { type } = {} ] = content;
70
+
71
+ return text !== '' || type?.name === 'image';
48
72
  }
49
73
  },
50
74
  methods: {
51
75
  click() {
52
- this.active = !this.active;
76
+ if (this.hasSelection) {
77
+ this.active = !this.active;
78
+ }
53
79
  },
54
80
  close() {
55
- this.active = false;
56
- this.editor.chain().focus();
81
+ if (this.active) {
82
+ this.active = false;
83
+ }
57
84
  }
58
85
  }
59
86
  };
@@ -168,11 +168,14 @@ export default {
168
168
  };
169
169
  },
170
170
  computed: {
171
+ attributes() {
172
+ return this.editor.getAttributes('link');
173
+ },
171
174
  buttonActive() {
172
- return this.editor.getAttributes('link').href || this.active;
175
+ return this.attributes.href || this.active;
173
176
  },
174
177
  lastSelectionTime() {
175
- return this.editor.view.lastSelectionTime;
178
+ return this.editor.view.input.lastSelectionTime;
176
179
  },
177
180
  hasSelection() {
178
181
  const { state } = this.editor;
@@ -186,6 +189,15 @@ export default {
186
189
  }
187
190
  },
188
191
  watch: {
192
+ 'attributes.href': {
193
+ handler(newVal, oldVal) {
194
+ if (newVal === oldVal) {
195
+ return;
196
+ }
197
+
198
+ this.close();
199
+ }
200
+ },
189
201
  active(newVal) {
190
202
  if (newVal) {
191
203
  this.hasLinkOnOpen = !!(this.docFields.data.href);
@@ -194,11 +206,6 @@ export default {
194
206
  window.removeEventListener('keydown', this.keyboardHandler);
195
207
  }
196
208
  },
197
- 'editor.view.lastSelectionTime': {
198
- handler(newVal, oldVal) {
199
- this.populateFields();
200
- }
201
- },
202
209
  hasSelection(newVal, oldVal) {
203
210
  if (!newVal) {
204
211
  this.close();
@@ -265,7 +272,7 @@ export default {
265
272
  },
266
273
  async populateFields() {
267
274
  try {
268
- const attrs = this.editor.getAttributes('link');
275
+ const attrs = this.attributes;
269
276
  if (attrs.target) {
270
277
  // checkboxes field expects an array
271
278
  attrs.target = [ attrs.target ];
@@ -24,7 +24,7 @@ export default options => {
24
24
 
25
25
  draggable: true,
26
26
 
27
- isolating: false,
27
+ isolating: true,
28
28
 
29
29
  addAttributes() {
30
30
  return {
@@ -109,6 +109,7 @@ export default options => {
109
109
  return {
110
110
  setImage: (attrs) => ({ chain }) => {
111
111
  return chain()
112
+ .focus()
112
113
  .insertContent({
113
114
  type: this.name,
114
115
  attrs,
@@ -999,9 +999,10 @@ module.exports = {
999
999
  //
1000
1000
  // Currently `req` does not impact this, but that may change.
1001
1001
 
1002
- prepareForStorage(req, doc) {
1002
+ prepareForStorage(req, doc, options = {}) {
1003
1003
  const can = (field) => {
1004
- return (!field.withType && !field.editPermission && !field.viewPermission) ||
1004
+ return options.permissions === false ||
1005
+ (!field.withType && !field.editPermission && !field.viewPermission) ||
1005
1006
  (field.withType && self.apos.permission.can(req, 'view', field.withType)) ||
1006
1007
  (field.editPermission && self.apos.permission.can(req, field.editPermission.action, field.editPermission.type)) ||
1007
1008
  (field.viewPermission && self.apos.permission.can(req, field.viewPermission.action, field.viewPermission.type)) ||
@@ -544,6 +544,18 @@ module.exports = (self) => {
544
544
  vueComponent: 'AposInputString',
545
545
  async convert(req, field, data, destination) {
546
546
  destination[field.name] = self.apos.launder.url(data[field.name], field.def, true);
547
+
548
+ if (field.required && (data[field.name] == null || !data[field.name].toString().length)) {
549
+ throw self.apos.error('required');
550
+ }
551
+
552
+ if (field.pattern) {
553
+ const regex = new RegExp(field.pattern);
554
+
555
+ if (!regex.test(destination[field.name])) {
556
+ throw self.apos.error('invalid');
557
+ }
558
+ }
547
559
  },
548
560
  diffable: function (value) {
549
561
  // URLs are fine to diff and display
@@ -553,6 +565,18 @@ module.exports = (self) => {
553
565
  // always return a valid string
554
566
  return '';
555
567
  },
568
+ validate(field, options, warn, fail) {
569
+ if (!field.pattern) {
570
+ return;
571
+ }
572
+
573
+ const isRegexInstance = field.pattern instanceof RegExp;
574
+ if (!isRegexInstance && typeof field.pattern !== 'string') {
575
+ fail('The pattern property must be a RegExp or a String');
576
+ }
577
+
578
+ field.pattern = isRegexInstance ? field.pattern.source : field.pattern;
579
+ },
556
580
  addQueryBuilder(field, query) {
557
581
  query.addBuilder(field.name, {
558
582
  finalize: function () {
@@ -613,18 +637,16 @@ module.exports = (self) => {
613
637
  finalize: function () {
614
638
  if (self.queryBuilderInterested(query, field.name)) {
615
639
  const value = query.get(field.name);
616
- let criteria;
640
+ const criteria = {};
617
641
  if (Array.isArray(value)) {
618
- criteria = {};
619
642
  criteria[field.name] = {
620
643
  $gte: value[0],
621
644
  $lte: value[1]
622
645
  };
623
646
  } else {
624
- criteria = {};
625
647
  criteria[field.name] = self.apos.launder.date(value);
626
- query.and(criteria);
627
648
  }
649
+ query.and(criteria);
628
650
  }
629
651
  },
630
652
  launder: function (value) {
@@ -1,9 +1,12 @@
1
1
  <template>
2
2
  <AposModal
3
- class="apos-array-editor" :modal="modal"
3
+ class="apos-array-editor"
4
+ :modal="modal"
4
5
  :modal-title="modalTitle"
5
- @inactive="modal.active = false" @show-modal="modal.showModal = true"
6
- @esc="confirmAndCancel" @no-modal="$emit('safe-close')"
6
+ @inactive="modal.active = false"
7
+ @show-modal="modal.showModal = true"
8
+ @esc="confirmAndCancel"
9
+ @no-modal="emitSafeClose"
7
10
  >
8
11
  <template #secondaryControls>
9
12
  <AposButton
@@ -88,7 +91,8 @@ import AposArrayEditorLogic from '../logic/AposArrayEditor';
88
91
 
89
92
  export default {
90
93
  name: 'AposArrayEditor',
91
- mixins: [ AposArrayEditorLogic ]
94
+ mixins: [ AposArrayEditorLogic ],
95
+ emits: [ 'safe-close' ]
92
96
  };
93
97
  </script>
94
98
 
@@ -42,6 +42,7 @@
42
42
  <th
43
43
  v-for="subfield in visibleSchema()"
44
44
  :key="subfield._id"
45
+ :style="subfield.columnStyle || {}"
45
46
  >
46
47
  {{ $t(subfield.label) }}
47
48
  </th>
@@ -76,6 +77,7 @@
76
77
  <component
77
78
  :is="field.style === 'table' ? 'td' : 'div'"
78
79
  class="apos-input-array-inline-item-controls"
80
+ :style="(field.style === 'table' && field.columnStyle) || {}"
79
81
  >
80
82
  <AposIndicator
81
83
  v-if="field.draggable"
@@ -15,7 +15,8 @@
15
15
  :required="field.required"
16
16
  :id="uid"
17
17
  :tabindex="tabindex"
18
- @keydown.enter="$emit('return')"
18
+ :autocomplete="field.autocomplete"
19
+ @keydown.enter="emitReturn"
19
20
  >
20
21
  </div>
21
22
  </template>
@@ -26,6 +27,7 @@
26
27
  import AposInputPasswordLogic from '../logic/AposInputPassword';
27
28
  export default {
28
29
  name: 'AposInputPassword',
29
- mixins: [ AposInputPasswordLogic ]
30
+ mixins: [ AposInputPasswordLogic ],
31
+ emits: [ 'return' ]
30
32
  };
31
33
  </script>
@@ -13,6 +13,7 @@
13
13
  :classes="classes"
14
14
  :disabled="field.readOnly"
15
15
  :selected="value.data"
16
+ :autocomplete="field.autocomplete"
16
17
  @change="change"
17
18
  />
18
19
  </template>
@@ -18,10 +18,11 @@
18
18
  :class="classes"
19
19
  v-model="next" :type="type"
20
20
  :placeholder="$t(field.placeholder)"
21
- @keydown.enter="$emit('return')"
21
+ @keydown.enter="emitReturn"
22
22
  :disabled="field.readOnly" :required="field.required"
23
23
  :id="uid" :tabindex="tabindex"
24
24
  ref="input"
25
+ :autocomplete="field.autocomplete"
25
26
  >
26
27
  <component
27
28
  v-if="icon"
@@ -38,7 +39,8 @@
38
39
  import AposInputSlugLogic from '../logic/AposInputSlug';
39
40
  export default {
40
41
  name: 'AposInputSlug',
41
- mixins: [ AposInputSlugLogic ]
42
+ mixins: [ AposInputSlugLogic ],
43
+ emits: [ 'return' ]
42
44
  };
43
45
  </script>
44
46
 
@@ -14,6 +14,7 @@
14
14
  :disabled="field.readOnly"
15
15
  :required="field.required"
16
16
  :id="uid" :tabindex="tabindex"
17
+ :autocomplete="field.autocomplete"
17
18
  />
18
19
  <input
19
20
  v-else :class="classes"
@@ -24,6 +25,7 @@
24
25
  :required="field.required"
25
26
  :id="uid" :tabindex="tabindex"
26
27
  :step="step"
28
+ :autocomplete="field.autocomplete"
27
29
  >
28
30
  <component
29
31
  v-if="icon"
@@ -17,6 +17,12 @@
17
17
  <span v-if="field.required" class="apos-field__required">
18
18
  *
19
19
  </span>
20
+ <AposLabel
21
+ class="apos-field__tag"
22
+ v-if="field.tag"
23
+ :label="field.tag.value || field.tag"
24
+ :modifiers="[ `apos-is-${field.tag.type || 'success'}`, 'apos-is-filled' ]"
25
+ />
20
26
  <span
21
27
  v-if="(field.help || field.htmlHelp) && displayOptions.helpTooltip"
22
28
  class="apos-field__help-tooltip"
@@ -119,6 +125,10 @@ export default {
119
125
  color: var(--a-danger);
120
126
  }
121
127
 
128
+ .apos-field__tag {
129
+ margin-left: 5px;
130
+ }
131
+
122
132
  .apos-field__help-tooltip {
123
133
  position: relative;
124
134
  top: 2px;
@@ -32,6 +32,7 @@
32
32
  v-for="field in schema" :key="field.name.concat(field._id ?? '')"
33
33
  :data-apos-field="field.name"
34
34
  :is="fieldStyle === 'table' ? 'td' : 'div'"
35
+ :style="(fieldStyle === 'table' && field.columnStyle) || {}"
35
36
  v-show="displayComponent(field)"
36
37
  >
37
38
  <component
@@ -364,6 +364,9 @@ export default {
364
364
  }
365
365
  }
366
366
  return choices;
367
+ },
368
+ emitSafeClose() {
369
+ this.$emit('safe-close');
367
370
  }
368
371
  }
369
372
  };
@@ -36,6 +36,9 @@ export default {
36
36
  }
37
37
  }
38
38
  return false;
39
+ },
40
+ emitReturn() {
41
+ this.$emit('return');
39
42
  }
40
43
  }
41
44
  };
@@ -273,6 +273,9 @@ export default {
273
273
  },
274
274
  passFocus() {
275
275
  this.$refs.input.focus();
276
+ },
277
+ emitReturn() {
278
+ this.$emit('return');
276
279
  }
277
280
  }
278
281
  };
@@ -890,20 +890,24 @@ module.exports = {
890
890
  self.insertions[key].push(componentName);
891
891
  },
892
892
 
893
- async annotateDataForExternalFront(req, template, data) {
894
- const docs = self.getDocsForExternalFront(req, template, data);
893
+ async annotateDataForExternalFront(req, template, data, moduleName) {
894
+ const docs = self.getDocsForExternalFront(req, template, data, moduleName);
895
895
  for (const doc of docs) {
896
896
  self.annotateDocForExternalFront(doc);
897
897
  }
898
898
  data.aposBodyData = await self.getBodyData(req);
899
+ // Already contains module name too
900
+ data.template = template;
901
+ // For simple cases (not piece pages and the like)
902
+ data.module = moduleName;
899
903
  return data;
900
904
  },
901
905
 
902
- pruneDataForExternalFront(req, data, template) {
906
+ pruneDataForExternalFront(req, template, data, moduleName) {
903
907
  return data;
904
908
  },
905
909
 
906
- getDocsForExternalFront(req, template, data) {
910
+ getDocsForExternalFront(req, template, data, moduleName) {
907
911
  return [ data.home, ...(data.page?._ancestors || []), ...(data.page?._children || []), data.page, data.piece, ...(data.pieces || []) ].filter(doc => !!doc);
908
912
  },
909
913
 
@@ -5,19 +5,21 @@
5
5
  :tabindex="{'-1' : field.hideLabel}"
6
6
  >
7
7
  <input
8
+ v-model="checkProxy"
8
9
  type="checkbox" class="apos-sr-only apos-input--choice apos-input--checkbox"
9
- :value="choice.value" :name="field.name"
10
+ :value="choice.value"
11
+ :name="field.name"
10
12
  :id="id" :aria-label="choice.label || field.label"
11
13
  :tabindex="tabindex" :disabled="field.readOnly || choice.readOnly"
12
- v-model="checkProxy"
13
- @change="updateThis"
14
+ @change="update"
14
15
  >
15
16
  <span class="apos-input-indicator" aria-hidden="true">
16
17
  <component
18
+ v-if="isChecked(checked)"
17
19
  :is="`${
18
20
  choice.indeterminate ? 'minus-icon' : 'check-bold-icon'
19
21
  }`"
20
- :size="10" v-if="checked && checked.includes(choice.value)"
22
+ :size="10"
21
23
  />
22
24
  </span>
23
25
  <span
@@ -39,7 +41,7 @@ export default {
39
41
  },
40
42
  props: {
41
43
  checked: {
42
- type: [ Array, Boolean, String ],
44
+ type: [ Array, Boolean ],
43
45
  default: false
44
46
  },
45
47
  choice: {
@@ -74,8 +76,7 @@ export default {
74
76
  return this.checked;
75
77
  },
76
78
  set(val) {
77
- // TODO: Move indeterminate to `status`
78
- if (!this.choice.indeterminate) {
79
+ if (!this.choice.indeterminate || this.choice.triggerIndeterminateEvent) {
79
80
  // Only update the model if the box was *not* indeterminate.
80
81
  this.$emit('change', val);
81
82
  }
@@ -83,10 +84,15 @@ export default {
83
84
  }
84
85
  },
85
86
  methods: {
87
+ isChecked(checked) {
88
+ return Array.isArray(checked)
89
+ ? checked.includes(this.choice.value)
90
+ : checked;
91
+ },
86
92
  // This event is only necessary if the parent needs to do *more* than simply
87
93
  // keep track of an array of checkbox values. For example, AposTagApply
88
94
  // does extra work with indeterminate values.
89
- updateThis($event) {
95
+ update($event) {
90
96
  this.$emit('updated', $event);
91
97
  }
92
98
  }
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <span
3
- class="apos-label" :class="modifiers"
3
+ class="apos-label"
4
+ :class="modifiers"
4
5
  v-apos-tooltip="tooltip"
5
6
  >
6
7
  {{ $t(label) }}
@@ -49,6 +50,11 @@ export default {
49
50
  border-color: var(--a-danger);
50
51
  }
51
52
 
53
+ .apos-is-sensitive {
54
+ border-color: var(--a-sensitive-medium);
55
+ color: var(--a-sensitive);
56
+ }
57
+
52
58
  .apos-is-success {
53
59
  border-color: var(--a-success);
54
60
  }
@@ -58,6 +64,11 @@ export default {
58
64
  color: var(--a-base-1);
59
65
  }
60
66
 
67
+ .apos-is-sensitive {
68
+ border-color: var(--a-sensitive-medium);
69
+ color: var(--a-sensitive);
70
+ }
71
+
61
72
  .apos-is-warning.apos-is-filled {
62
73
  background-color: var(--a-warning-fade);
63
74
  }
@@ -66,6 +77,10 @@ export default {
66
77
  background-color: var(--a-danger-fade);
67
78
  }
68
79
 
80
+ .apos-is-sensitive.apos-is-filled {
81
+ background-color: var(--a-sensitive-light);
82
+ }
83
+
69
84
  .apos-is-success.apos-is-filled {
70
85
  background-color: var(--a-success-fade);
71
86
  }
@@ -5,6 +5,7 @@
5
5
  :class="classes"
6
6
  :uid="uid"
7
7
  :disabled="disabled"
8
+ :autocomplete="autocomplete"
8
9
  @change="change($event.target.value)"
9
10
  >
10
11
  <option
@@ -61,6 +62,10 @@ export default {
61
62
  disabled: {
62
63
  type: Boolean,
63
64
  default: false
65
+ },
66
+ autocomplete: {
67
+ type: String,
68
+ default: null
64
69
  }
65
70
  },
66
71
  emits: [ 'change' ],
@@ -69,7 +69,7 @@ export default {
69
69
  return true;
70
70
  } catch (e) {
71
71
  await apos.notify(e.message, {
72
- type: 'error',
72
+ type: 'danger',
73
73
  localize: false
74
74
  });
75
75
  return false;
@@ -13,6 +13,10 @@
13
13
  --a-danger-button-active: #a10000;
14
14
  --a-danger-button-disabled: #ff9d98;
15
15
 
16
+ --a-sensitive: #643810;
17
+ --a-sensitive-medium: #E2C7B9;
18
+ --a-sensitive-light: #FFF3E7;
19
+
16
20
  --a-base-1: #5d5d5d;
17
21
  --a-base-2: #6f6f6f;
18
22
  --a-base-3: #818181;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.60.0",
3
+ "version": "3.61.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {