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.
- package/CHANGELOG.md +58 -1
- package/defaults.js +1 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +5 -2
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +28 -19
- package/modules/@apostrophecms/any-doc-type/index.js +2 -2
- package/modules/@apostrophecms/any-page-type/index.js +2 -2
- package/modules/@apostrophecms/doc/index.js +55 -29
- package/modules/@apostrophecms/doc-type/index.js +11 -6
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +4 -440
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +445 -0
- package/modules/@apostrophecms/i18n/i18n/de.json +113 -105
- package/modules/@apostrophecms/i18n/i18n/es.json +10 -0
- package/modules/@apostrophecms/i18n/i18n/fr.json +8 -0
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +10 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +8 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +1 -0
- package/modules/@apostrophecms/log/index.js +429 -0
- package/modules/@apostrophecms/login/index.js +47 -4
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +14 -1
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +1 -1
- package/modules/@apostrophecms/module/index.js +32 -6
- package/modules/@apostrophecms/module/lib/log.js +68 -0
- package/modules/@apostrophecms/page/index.js +71 -19
- package/modules/@apostrophecms/page/lib/legacy-migrations.js +0 -57
- package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +8 -285
- package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +291 -0
- package/modules/@apostrophecms/page-type/index.js +39 -26
- package/modules/@apostrophecms/piece-type/index.js +19 -11
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +2 -357
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +2 -86
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +2 -254
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +2 -77
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputBoolean.vue +2 -44
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +2 -64
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +2 -94
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +3 -47
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -82
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +2 -37
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +2 -26
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -57
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +2 -259
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +2 -38
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +2 -275
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +2 -167
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +2 -115
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +3 -279
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +2 -83
- package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +10 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +361 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +89 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +257 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputAttachment.js +81 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputBoolean.js +48 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +68 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputColor.js +98 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js +49 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +86 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js +41 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +29 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRange.js +60 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +262 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSelect.js +41 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +278 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +170 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +118 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +281 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSearchList.js +85 -0
- package/modules/@apostrophecms/template/index.js +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -2
- package/modules/@apostrophecms/util/index.js +83 -13
- package/modules/@apostrophecms/util/lib/logger.js +19 -17
- package/package.json +1 -1
- package/test/docs.js +35 -2
- package/test/log.js +1765 -0
- package/test/pages.js +57 -0
- 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
|
+
}
|