apostrophe 3.53.0 → 3.55.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 (77) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/defaults.js +1 -0
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +5 -2
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +28 -19
  5. package/modules/@apostrophecms/any-doc-type/index.js +2 -2
  6. package/modules/@apostrophecms/any-page-type/index.js +2 -2
  7. package/modules/@apostrophecms/doc/index.js +55 -29
  8. package/modules/@apostrophecms/doc-type/index.js +11 -6
  9. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +4 -440
  10. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +445 -0
  11. package/modules/@apostrophecms/i18n/i18n/de.json +113 -105
  12. package/modules/@apostrophecms/i18n/i18n/es.json +10 -0
  13. package/modules/@apostrophecms/i18n/i18n/fr.json +8 -0
  14. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +10 -0
  15. package/modules/@apostrophecms/i18n/i18n/sk.json +8 -0
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +1 -0
  17. package/modules/@apostrophecms/log/index.js +429 -0
  18. package/modules/@apostrophecms/login/index.js +47 -4
  19. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +14 -1
  20. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +1 -1
  21. package/modules/@apostrophecms/module/index.js +32 -6
  22. package/modules/@apostrophecms/module/lib/log.js +68 -0
  23. package/modules/@apostrophecms/page/index.js +71 -19
  24. package/modules/@apostrophecms/page/lib/legacy-migrations.js +0 -57
  25. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +8 -285
  26. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +291 -0
  27. package/modules/@apostrophecms/page-type/index.js +39 -26
  28. package/modules/@apostrophecms/piece-type/index.js +19 -11
  29. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
  30. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +2 -357
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +2 -86
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +2 -254
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +2 -77
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputBoolean.vue +2 -44
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +2 -64
  36. package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +2 -94
  37. package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +3 -47
  38. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -82
  39. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +2 -37
  40. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +2 -26
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -57
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +2 -259
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +2 -38
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +2 -275
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +2 -167
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +2 -115
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +3 -279
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +2 -83
  49. package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +10 -1
  50. package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +361 -0
  51. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +89 -0
  52. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +257 -0
  53. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputAttachment.js +81 -0
  54. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputBoolean.js +48 -0
  55. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +68 -0
  56. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputColor.js +98 -0
  57. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js +49 -0
  58. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +86 -0
  59. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js +41 -0
  60. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +29 -0
  61. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRange.js +60 -0
  62. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +262 -0
  63. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSelect.js +41 -0
  64. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +278 -0
  65. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +170 -0
  66. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +118 -0
  67. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +281 -0
  68. package/modules/@apostrophecms/schema/ui/apos/logic/AposSearchList.js +85 -0
  69. package/modules/@apostrophecms/template/index.js +1 -1
  70. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -2
  71. package/modules/@apostrophecms/util/index.js +83 -13
  72. package/modules/@apostrophecms/util/lib/logger.js +19 -17
  73. package/package.json +1 -1
  74. package/test/docs.js +35 -2
  75. package/test/log.js +1765 -0
  76. package/test/pages.js +57 -0
  77. package/test-lib/util.js +1 -1
@@ -0,0 +1,361 @@
1
+ import AposModifiedMixin from 'Modules/@apostrophecms/ui/mixins/AposModifiedMixin';
2
+ import AposEditorMixin from 'Modules/@apostrophecms/modal/mixins/AposEditorMixin';
3
+ import cuid from 'cuid';
4
+ import { klona } from 'klona';
5
+ import { get } from 'lodash';
6
+ import { detectDocChange } from 'Modules/@apostrophecms/schema/lib/detectChange';
7
+
8
+ export default {
9
+ name: 'AposArrayEditor',
10
+ mixins: [
11
+ AposModifiedMixin,
12
+ AposEditorMixin
13
+ ],
14
+ props: {
15
+ items: {
16
+ required: true,
17
+ type: Array
18
+ },
19
+ field: {
20
+ required: true,
21
+ type: Object
22
+ },
23
+ serverError: {
24
+ type: Object,
25
+ default: null
26
+ },
27
+ docId: {
28
+ type: String,
29
+ default: null
30
+ },
31
+ parentFollowingValues: {
32
+ type: Object,
33
+ default: null
34
+ }
35
+ },
36
+ emits: [ 'modal-result', 'safe-close' ],
37
+ data() {
38
+ // Automatically add `_id` to default items
39
+ const items = this.items.map(item => ({
40
+ ...item,
41
+ _id: item._id || cuid()
42
+ }));
43
+
44
+ return {
45
+ currentId: null,
46
+ currentDoc: null,
47
+ modal: {
48
+ active: false,
49
+ type: 'overlay',
50
+ showModal: false
51
+ },
52
+ modalTitle: {
53
+ key: 'apostrophe:editType',
54
+ type: this.$t(this.field.label)
55
+ },
56
+ titleFieldChoices: null,
57
+ // If we don't clone, then we're making
58
+ // permanent modifications whether the user
59
+ // clicks save or not
60
+ next: klona(items),
61
+ original: klona(items),
62
+ triggerValidation: false,
63
+ minError: false,
64
+ maxError: false,
65
+ cancelDescription: 'apostrophe:arrayCancelDescription'
66
+ };
67
+ },
68
+ computed: {
69
+ moduleOptions() {
70
+ return window.apos.schema || {};
71
+ },
72
+ itemError() {
73
+ return this.currentDoc && this.currentDoc.hasErrors;
74
+ },
75
+ valid() {
76
+ return !(this.minError || this.maxError || this.itemError);
77
+ },
78
+ maxed() {
79
+ return (this.field.max !== undefined) && (this.next.length >= this.field.max);
80
+ },
81
+ schema() {
82
+ // For AposDocEditorMixin
83
+ return (this.field.schema || []).filter(field => apos.schema.components.fields[field.type]);
84
+ },
85
+ countLabel() {
86
+ return this.$t('apostrophe:numberAdded', {
87
+ count: this.next.length
88
+ });
89
+ },
90
+ // Here in the array editor we use effectiveMin to factor in the
91
+ // required property because there is no other good place to do that,
92
+ // unlike the input field wrapper which has a separate visual
93
+ // representation of "required".
94
+ minLabel() {
95
+ if (this.effectiveMin) {
96
+ return this.$t('apostrophe:minUi', {
97
+ number: this.effectiveMin
98
+ });
99
+ } else {
100
+ return false;
101
+ }
102
+ },
103
+ maxLabel() {
104
+ if ((typeof this.field.max) === 'number') {
105
+ return this.$t('apostrophe:maxUi', {
106
+ number: this.field.max
107
+ });
108
+ } else {
109
+ return false;
110
+ }
111
+ },
112
+ effectiveMin() {
113
+ if (this.field.min) {
114
+ return this.field.min;
115
+ } else if (this.field.required) {
116
+ return 1;
117
+ } else {
118
+ return 0;
119
+ }
120
+ },
121
+ currentDocServerErrors() {
122
+ let serverErrors = null;
123
+ ((this.serverError && this.serverError.data && this.serverError.data.errors) || []).forEach(error => {
124
+ const [ _id, fieldName ] = error.path.split('.');
125
+ if (_id === this.currentId) {
126
+ serverErrors = serverErrors || {};
127
+ serverErrors[fieldName] = error;
128
+ }
129
+ });
130
+ return serverErrors;
131
+ }
132
+ },
133
+ async mounted() {
134
+ this.modal.active = true;
135
+ if (this.next.length) {
136
+ this.select(this.next[0]._id);
137
+ }
138
+ if (this.serverError && this.serverError.data && this.serverError.data.errors) {
139
+ const first = this.serverError.data.errors[0];
140
+ const [ _id, name ] = first.path.split('.');
141
+ await this.select(_id);
142
+ const aposSchema = this.$refs.schema;
143
+ await this.nextTick();
144
+ aposSchema.scrollFieldIntoView(name);
145
+ }
146
+ this.titleFieldChoices = await this.getTitleFieldChoices();
147
+ },
148
+ methods: {
149
+ async select(_id) {
150
+ if (this.currentId === _id) {
151
+ return;
152
+ }
153
+ if (await this.validate(true, false)) {
154
+ // Force the array editor to totally reset to avoid in-schema
155
+ // animations when switching (e.g., the relationship input).
156
+ this.currentDocToCurrentItem();
157
+ this.currentId = null;
158
+ await this.nextTick();
159
+ this.currentId = _id;
160
+ this.currentDoc = {
161
+ hasErrors: false,
162
+ data: this.next.find(item => item._id === _id)
163
+ };
164
+ this.triggerValidation = false;
165
+ }
166
+ },
167
+ update(items) {
168
+ // Take care to use the same items in order to avoid
169
+ // losing too much state inside draggable, otherwise
170
+ // drags fail
171
+ this.next = items.map(item => this.next.find(_item => item._id === _item._id));
172
+ if (this.currentId) {
173
+ if (!this.next.find(item => item._id === this.currentId)) {
174
+ this.currentId = null;
175
+ this.currentDoc = null;
176
+ }
177
+ }
178
+ this.updateMinMax();
179
+ },
180
+ currentDocUpdate(currentDoc) {
181
+ this.currentDoc = currentDoc;
182
+ },
183
+ async add() {
184
+ if (await this.validate(true, false)) {
185
+ const item = this.newInstance();
186
+ item._id = cuid();
187
+ this.next.push(item);
188
+ this.select(item._id);
189
+ this.updateMinMax();
190
+ }
191
+ },
192
+ updateMinMax() {
193
+ let minError = false;
194
+ let maxError = false;
195
+ if (this.effectiveMin) {
196
+ if (this.next.length < this.effectiveMin) {
197
+ minError = true;
198
+ }
199
+ }
200
+ if (this.field.max !== undefined) {
201
+ if (this.next.length > this.field.max) {
202
+ maxError = true;
203
+ }
204
+ }
205
+ this.minError = minError;
206
+ this.maxError = maxError;
207
+ },
208
+ async submit() {
209
+ if (await this.validate(true, true)) {
210
+ this.currentDocToCurrentItem();
211
+ for (const item of this.next) {
212
+ item.metaType = 'arrayItem';
213
+ item.scopedArrayName = this.field.scopedArrayName;
214
+ }
215
+ this.$emit('modal-result', this.next);
216
+ this.modal.showModal = false;
217
+ }
218
+ },
219
+ currentDocToCurrentItem() {
220
+ if (!this.currentId) {
221
+ return;
222
+ }
223
+ const currentIndex = this.next.findIndex(item => item._id === this.currentId);
224
+ this.next[currentIndex] = this.currentDoc.data;
225
+ },
226
+ getFieldValue(name) {
227
+ return this.currentDoc.data[name];
228
+ },
229
+ isModified() {
230
+ if (this.currentId) {
231
+ const currentIndex = this.next.findIndex(item => item._id === this.currentId);
232
+ if (detectDocChange(this.schema, this.next[currentIndex], this.currentDoc.data)) {
233
+ return true;
234
+ }
235
+ }
236
+ if (this.next.length !== this.original.length) {
237
+ return true;
238
+ }
239
+ for (let i = 0; (i < this.next.length); i++) {
240
+ if (this.next[i]._id !== this.original[i]._id) {
241
+ return true;
242
+ }
243
+ if (detectDocChange(this.schema, this.next[i], this.original[i])) {
244
+ return true;
245
+ }
246
+ }
247
+ return false;
248
+ },
249
+ async validate(validateItem, validateLength) {
250
+ if (validateItem) {
251
+ this.triggerValidation = true;
252
+ }
253
+ await this.nextTick();
254
+ if (validateLength) {
255
+ this.updateMinMax();
256
+ }
257
+ if (
258
+ (validateLength && (this.minError || this.maxError)) ||
259
+ (validateItem && (this.currentDoc && this.currentDoc.hasErrors))
260
+ ) {
261
+ await apos.notify('apostrophe:resolveErrorsFirst', {
262
+ type: 'warning',
263
+ icon: 'alert-circle-icon',
264
+ dismiss: true
265
+ });
266
+ return false;
267
+ } else {
268
+ return true;
269
+ }
270
+ },
271
+ // Awaitable nextTick
272
+ nextTick() {
273
+ return new Promise((resolve, reject) => {
274
+ this.$nextTick(() => {
275
+ return resolve();
276
+ });
277
+ });
278
+ },
279
+ newInstance() {
280
+ const instance = {};
281
+ for (const field of this.schema) {
282
+ if (field.def !== undefined) {
283
+ instance[field.name] = klona(field.def);
284
+ }
285
+ }
286
+ return instance;
287
+ },
288
+ label(item) {
289
+ let candidate;
290
+ if (this.field.titleField) {
291
+
292
+ // Initial field value
293
+ candidate = get(item, this.field.titleField);
294
+
295
+ // If the titleField references a select input, use the
296
+ // select label as the slat label, rather than the value.
297
+ if (this.titleFieldChoices) {
298
+ const choice = this.titleFieldChoices.find(choice => choice.value === candidate);
299
+ if (choice && choice.label) {
300
+ candidate = choice.label;
301
+ }
302
+ }
303
+
304
+ } else if (this.schema.find(field => field.name === 'title') && (item.title !== undefined)) {
305
+ candidate = item.title;
306
+ }
307
+ if ((candidate == null) || candidate === '') {
308
+ for (let i = 0; (i < this.next.length); i++) {
309
+ if (this.next[i]._id === item._id) {
310
+ candidate = `#${i + 1}`;
311
+ break;
312
+ }
313
+ }
314
+ }
315
+ return candidate;
316
+ },
317
+ withLabels(items) {
318
+ const result = items.map(item => ({
319
+ ...item,
320
+ title: this.label(item)
321
+ }));
322
+ return result;
323
+ },
324
+ async getTitleFieldChoices() {
325
+ // If the titleField references a select input, get it's choices
326
+ // to use as labels for the slat UI
327
+
328
+ let choices = null;
329
+ const titleField = this.schema.find(field => field.name === this.field.titleField);
330
+
331
+ // The titleField is a select
332
+ if (titleField?.choices) {
333
+
334
+ // Choices are provided by a method
335
+ if (typeof titleField.choices === 'string') {
336
+ const action = `${this.moduleOptions.action}/choices`;
337
+ try {
338
+ const result = await apos.http.get(
339
+ action,
340
+ {
341
+ qs: {
342
+ fieldId: titleField._id
343
+ }
344
+ }
345
+ );
346
+ if (result && result.choices) {
347
+ choices = result.choices;
348
+ }
349
+ } catch (e) {
350
+ console.error(this.$t('apostrophe:errorFetchingTitleFieldChoicesByMethod', { name: titleField.name }));
351
+ }
352
+
353
+ // Choices are a normal, hardcoded array
354
+ } else if (Array.isArray(titleField.choices)) {
355
+ choices = titleField.choices;
356
+ }
357
+ }
358
+ return choices;
359
+ }
360
+ }
361
+ };
@@ -0,0 +1,89 @@
1
+
2
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
3
+ import cuid from 'cuid';
4
+
5
+ export default {
6
+ name: 'AposInputArea',
7
+ mixins: [ AposInputMixin ],
8
+ props: {
9
+ generation: {
10
+ type: Number,
11
+ required: false,
12
+ default() {
13
+ return null;
14
+ }
15
+ }
16
+ },
17
+ data () {
18
+ return {
19
+ next: this.value.data || this.getEmptyValue(),
20
+ error: false,
21
+ // This is just meant to be sufficient to prevent unintended collisions
22
+ // in the UI between id attributes
23
+ uid: Math.random()
24
+ };
25
+ },
26
+ computed: {
27
+ editorComponent() {
28
+ return window.apos.area.components.editor;
29
+ },
30
+ choices() {
31
+ const result = [];
32
+
33
+ let widgets = this.field.options.widgets || {};
34
+ if (this.field.options.groups) {
35
+ for (const group of Object.entries(this.field.options.groups)) {
36
+ widgets = {
37
+ ...widgets,
38
+ ...group.widgets
39
+ };
40
+ }
41
+ }
42
+
43
+ for (const [ name, options ] of Object.entries(widgets)) {
44
+ result.push({
45
+ name,
46
+ label: options.addLabel || apos.modules[`${name}-widget`].label
47
+ });
48
+ }
49
+ return result;
50
+ }
51
+ },
52
+ methods: {
53
+ getEmptyValue() {
54
+ return {
55
+ metaType: 'area',
56
+ _id: cuid(),
57
+ items: []
58
+ };
59
+ },
60
+ watchValue () {
61
+ this.error = this.value.error;
62
+ this.next = this.value.data || this.getEmptyValue();
63
+ },
64
+ validate(value) {
65
+ if (this.field.required) {
66
+ if (!value.items.length) {
67
+ return 'required';
68
+ }
69
+ }
70
+ if (this.field.min) {
71
+ if (value.items.length && (value.items.length < this.field.min)) {
72
+ return 'min';
73
+ }
74
+ }
75
+ if (this.field.max) {
76
+ if (value.items.length && (value.items.length > this.field.max)) {
77
+ return 'max';
78
+ }
79
+ }
80
+ return false;
81
+ },
82
+ changed($event) {
83
+ this.next = {
84
+ ...this.next,
85
+ items: $event.items
86
+ };
87
+ }
88
+ }
89
+ };
@@ -0,0 +1,257 @@
1
+ import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
2
+ import AposInputFollowingMixin from 'Modules/@apostrophecms/schema/mixins/AposInputFollowingMixin.js';
3
+ import AposInputConditionalFieldsMixin from 'Modules/@apostrophecms/schema/mixins/AposInputConditionalFieldsMixin.js';
4
+
5
+ import cuid from 'cuid';
6
+ import { klona } from 'klona';
7
+ import { get } from 'lodash';
8
+ import draggable from 'vuedraggable';
9
+
10
+ export default {
11
+ name: 'AposInputArray',
12
+ components: { draggable },
13
+ mixins: [
14
+ AposInputMixin,
15
+ AposInputFollowingMixin,
16
+ AposInputConditionalFieldsMixin
17
+ ],
18
+ props: {
19
+ generation: {
20
+ type: Number,
21
+ required: false,
22
+ default: null
23
+ }
24
+ },
25
+ data() {
26
+ const next = this.getNext();
27
+ const data = {
28
+ next,
29
+ items: modelItems(next, this.field)
30
+ };
31
+ return data;
32
+ },
33
+ computed: {
34
+ // required by the conditional fields mixin
35
+ schema() {
36
+ return this.field.schema;
37
+ },
38
+ alwaysExpand() {
39
+ return alwaysExpand(this.field);
40
+ },
41
+ listId() {
42
+ return `sortableList-${cuid()}`;
43
+ },
44
+ dragOptions() {
45
+ return {
46
+ disabled: !this.field.draggable || this.field.readOnly || this.next.length <= 1,
47
+ ghostClass: 'apos-is-dragging',
48
+ handle: '.apos-drag-handle'
49
+ };
50
+ },
51
+ itemLabel() {
52
+ return this.field.itemLabel
53
+ ? {
54
+ key: 'apostrophe:addType',
55
+ type: this.$t(this.field.itemLabel)
56
+ }
57
+ : 'apostrophe:addItem';
58
+ },
59
+ editLabel() {
60
+ return {
61
+ key: 'apostrophe:editType',
62
+ type: this.$t(this.field.label)
63
+ };
64
+ },
65
+ effectiveError() {
66
+ const error = this.error || this.serverError;
67
+ // Server-side errors behave differently
68
+ const name = error?.name || error;
69
+ if (name === 'invalid') {
70
+ // Always due to a subproperty which will display its own error,
71
+ // don't confuse the user
72
+ return false;
73
+ }
74
+ return error;
75
+ }
76
+ },
77
+ watch: {
78
+ generation() {
79
+ this.next = this.getNext();
80
+ this.items = modelItems(this.next, this.field);
81
+ },
82
+ items: {
83
+ deep: true,
84
+ handler() {
85
+ const erroneous = this.items.filter(item => item.schemaInput.hasErrors);
86
+ if (erroneous.length) {
87
+ erroneous.forEach(item => {
88
+ if (!item.open) {
89
+ // Make errors visible
90
+ item.open = true;
91
+ }
92
+ });
93
+ } else {
94
+ const next = this.items.map(item => ({
95
+ ...item.schemaInput.data,
96
+ _id: item._id,
97
+ metaType: 'arrayItem',
98
+ scopedArrayName: this.field.scopedArrayName
99
+ }));
100
+ this.next = next;
101
+ }
102
+ // Our validate method was called first before that of
103
+ // the subfields, so remedy that by calling again on any
104
+ // change to the subfield state during validation
105
+ if (this.triggerValidation) {
106
+ this.validateAndEmit();
107
+ }
108
+ }
109
+ }
110
+ },
111
+ async created() {
112
+ if (this.field.inline) {
113
+ await this.evaluateExternalConditions();
114
+ }
115
+ },
116
+ methods: {
117
+ validate(value) {
118
+ if (this.items.find(item => item.schemaInput.hasErrors)) {
119
+ return 'invalid';
120
+ }
121
+ if (this.field.required && !value.length) {
122
+ return 'required';
123
+ }
124
+ if (this.field.min && value.length < this.field.min) {
125
+ return 'min';
126
+ }
127
+ if (this.field.max && value.length > this.field.max) {
128
+ return 'max';
129
+ }
130
+ if (value.length && this.field.fields && this.field.fields.add) {
131
+ const [ uniqueFieldName, uniqueFieldSchema ] = Object.entries(this.field.fields.add).find(([ , subfield ]) => subfield.unique) || [];
132
+ if (uniqueFieldName) {
133
+ const duplicates = this.next
134
+ .map(item =>
135
+ Array.isArray(item[uniqueFieldName])
136
+ ? item[uniqueFieldName].map(i => i._id).sort().join('|')
137
+ : item[uniqueFieldName])
138
+ .filter((item, index, array) => array.indexOf(item) !== index);
139
+
140
+ if (duplicates.length) {
141
+ duplicates.forEach(duplicate => {
142
+ this.items.forEach(item => {
143
+ uniqueFieldSchema.type === 'relationship'
144
+ ? item.schemaInput.data[uniqueFieldName] && item.schemaInput.data[uniqueFieldName].forEach(datum => {
145
+ item.schemaInput.fieldState[uniqueFieldName].duplicate = duplicate.split('|').find(i => i === datum._id);
146
+ })
147
+ : item.schemaInput.fieldState[uniqueFieldName].duplicate = item.schemaInput.data[uniqueFieldName] === duplicate;
148
+ });
149
+ });
150
+
151
+ return {
152
+ name: 'duplicate',
153
+ message: `${this.$t('apostrophe:duplicateError')} ${this.$t(uniqueFieldSchema.label) || uniqueFieldName}`
154
+ };
155
+ }
156
+ }
157
+ }
158
+ return false;
159
+ },
160
+ async edit() {
161
+ const result = await apos.modal.execute('AposArrayEditor', {
162
+ field: this.field,
163
+ items: this.next,
164
+ serverError: this.serverError,
165
+ docId: this.docId,
166
+ parentFollowingValues: this.followingValues
167
+ });
168
+ if (result) {
169
+ this.next = result;
170
+ }
171
+ },
172
+ getNext() {
173
+ // Next should consistently be an array.
174
+ return (this.value && Array.isArray(this.value.data))
175
+ ? this.value.data : (this.field.def || []);
176
+ },
177
+ disableAdd() {
178
+ return this.field.max && (this.items.length >= this.field.max);
179
+ },
180
+ remove(_id) {
181
+ this.items = this.items.filter(item => item._id !== _id);
182
+ },
183
+ add() {
184
+ const _id = cuid();
185
+ this.items.push({
186
+ _id,
187
+ schemaInput: {
188
+ data: this.newInstance()
189
+ },
190
+ open: alwaysExpand(this.field)
191
+ });
192
+ this.openInlineItem(_id);
193
+ },
194
+ newInstance() {
195
+ const instance = {};
196
+ for (const field of this.field.schema) {
197
+ if (field.def !== undefined) {
198
+ instance[field.name] = klona(field.def);
199
+ }
200
+ }
201
+ return instance;
202
+ },
203
+ getLabel(id, index) {
204
+ const titleField = this.field.titleField || null;
205
+ const item = this.items.find(item => item._id === id);
206
+ return get(item.schemaInput.data, titleField) || `Item ${index + 1}`;
207
+ },
208
+ openInlineItem(id) {
209
+ this.items.forEach(item => {
210
+ item.open = (item._id === id) || this.alwaysExpand;
211
+ });
212
+ },
213
+ closeInlineItem(id) {
214
+ this.items.forEach(item => {
215
+ item.open = this.alwaysExpand;
216
+ });
217
+ },
218
+ getFollowingValues(item) {
219
+ return this.computeFollowingValues(item.schemaInput.data);
220
+ },
221
+ // Retrieve table heading fields from the schema, based on the currently
222
+ // opened item. Available only when the field style is `table`.
223
+ visibleSchema() {
224
+ if (this.field.style !== 'table') {
225
+ return this.schema;
226
+ }
227
+ const currentItem = this.items.find(item => item.open) || this.items[this.items.length - 1];
228
+ const conditions = this.conditionalFields(currentItem?.schemaInput?.data || {});
229
+ return this.schema.filter(
230
+ field => conditions[field.name] !== false
231
+ );
232
+ }
233
+ }
234
+ };
235
+
236
+ function modelItems(items, field) {
237
+ return items.map(item => {
238
+ const open = alwaysExpand(field);
239
+ return {
240
+ _id: item._id || cuid(),
241
+ schemaInput: {
242
+ data: item
243
+ },
244
+ open
245
+ };
246
+ });
247
+ }
248
+
249
+ function alwaysExpand(field) {
250
+ if (!field.inline) {
251
+ return false;
252
+ }
253
+ if (field.inline.alwaysExpand === undefined) {
254
+ return field.schema.length < 3;
255
+ }
256
+ return field.inline.alwaysExpand;
257
+ }