apostrophe 3.17.0 → 3.18.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/.editorconfig +3 -0
- package/.eslintrc +4 -3
- package/.github/workflows/main.yml +2 -2
- package/.stylelintrc +12 -2
- package/CHANGELOG.md +34 -2
- package/defaults.js +2 -2
- package/index.js +124 -33
- package/lib/escape-host.js +8 -0
- package/lib/mongodb-connect.js +55 -0
- package/lib/opentelemetry.js +144 -0
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +2 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +20 -8
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +10 -0
- package/modules/@apostrophecms/asset/lib/globalIcons.js +1 -0
- package/modules/@apostrophecms/attachment/index.js +81 -29
- package/modules/@apostrophecms/db/index.js +7 -10
- package/modules/@apostrophecms/doc/index.js +138 -23
- package/modules/@apostrophecms/doc-type/index.js +162 -63
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +39 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +11 -1
- package/modules/@apostrophecms/email/index.js +1 -1
- package/modules/@apostrophecms/express/index.js +2 -2
- package/modules/@apostrophecms/http/index.js +2 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +10 -0
- package/modules/@apostrophecms/i18n/i18n/es.json +7 -0
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +7 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +7 -0
- package/modules/@apostrophecms/image/index.js +182 -1
- package/modules/@apostrophecms/image/ui/apos/apps/AposImageRelationshipQueryFilter.js +13 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposImageCropper.vue +460 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +510 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +5 -1
- package/modules/@apostrophecms/image/ui/apos/lib/aspectRatios.js +26 -0
- package/modules/@apostrophecms/image-widget/views/widget.html +5 -2
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +45 -1
- package/modules/@apostrophecms/module/index.js +98 -17
- package/modules/@apostrophecms/module/lib/events.js +46 -11
- package/modules/@apostrophecms/page/index.js +55 -22
- package/modules/@apostrophecms/piece-page-type/index.js +1 -0
- package/modules/@apostrophecms/piece-type/index.js +13 -4
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposRelationshipEditor.vue +2 -2
- package/modules/@apostrophecms/rich-text-widget/index.js +1 -3
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +4 -0
- package/modules/@apostrophecms/schema/index.js +79 -73
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +10 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +22 -3
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +72 -36
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +7 -26
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +8 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +45 -15
- package/modules/@apostrophecms/task/index.js +106 -52
- package/modules/@apostrophecms/template/index.js +111 -76
- package/modules/@apostrophecms/template/lib/custom-tags/component.js +42 -22
- package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +61 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +46 -11
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +10 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -22
- package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
- package/modules/@apostrophecms/widget-type/index.js +2 -23
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidget.vue +1 -1
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +20 -1
- package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +0 -9
- package/package.json +16 -12
- package/scripts/lint-i18n.js +2 -2
- package/test/assets.js +2 -1
- package/test/attachments.js +119 -26
- package/test/bundle.js +1 -1
- package/test/content-i18n.js +6 -6
- package/test/docs.js +244 -4
- package/test/draft-published.js +41 -41
- package/test/express.js +1 -1
- package/test/http.js +2 -2
- package/test/images.js +94 -4
- package/test/job.js +1 -1
- package/test/locks.js +1 -1
- package/test/middleware-and-route-order.js +3 -3
- package/test/pages-public-api.js +48 -4
- package/test/pages-rest.js +20 -20
- package/test/pages.js +377 -11
- package/test/parked-pages.js +1 -1
- package/test/permissions.js +10 -10
- package/test/pieces-public-api.js +130 -6
- package/test/pieces.js +247 -60
- package/test/recursionGuard.js +6 -6
- package/test/restApiRoutes.js +6 -6
- package/test/schemaBuilders.js +7 -7
- package/test/schemas.js +59 -59
- package/test/search.js +3 -3
- package/test/soft-redirects.js +13 -13
- package/test/static-i18n.js +1 -1
- package/test/templates.js +10 -10
- package/test/urls.js +2 -2
- package/test/users.js +21 -21
- package/test/utils.js +13 -13
- package/test/widgets.js +2 -2
- package/test-lib/util.js +2 -5
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidget.vue +0 -26
|
@@ -1012,12 +1012,12 @@ module.exports = {
|
|
|
1012
1012
|
// so consider that too
|
|
1013
1013
|
const withType = field.name.replace(/^_/, '').replace(/s$/, '');
|
|
1014
1014
|
if (!_.find(self.apos.doc.managers, { name: withType })) {
|
|
1015
|
-
fail('withType property is missing. Hint: it must match the
|
|
1015
|
+
fail('withType property is missing. Hint: it must match the name of a doc type module. Or omit it and give your relationship the same name as the other type, with a leading _ and optional trailing s.');
|
|
1016
1016
|
}
|
|
1017
1017
|
field.withType = withType;
|
|
1018
1018
|
}
|
|
1019
1019
|
if (!field.withType) {
|
|
1020
|
-
fail('withType property is missing. Hint: it must match the
|
|
1020
|
+
fail('withType property is missing. Hint: it must match the name of a doc type module.');
|
|
1021
1021
|
}
|
|
1022
1022
|
if (Array.isArray(field.withType)) {
|
|
1023
1023
|
_.each(field.withType, function (type) {
|
|
@@ -1025,6 +1025,18 @@ module.exports = {
|
|
|
1025
1025
|
});
|
|
1026
1026
|
} else {
|
|
1027
1027
|
lintType(field.withType);
|
|
1028
|
+
const withTypeManager = self.apos.doc.getManager(field.withType);
|
|
1029
|
+
field.editor = field.editor || withTypeManager.options.relationshipEditor;
|
|
1030
|
+
field.postprocessor = field.postprocessor || withTypeManager.options.relationshipPostprocessor;
|
|
1031
|
+
field.editorLabel = field.editorLabel || withTypeManager.options.relationshipEditorLabel;
|
|
1032
|
+
field.editorIcon = field.editorIcon || withTypeManager.options.relationshipEditorIcon;
|
|
1033
|
+
|
|
1034
|
+
if (!field.schema && !Array.isArray(field.withType)) {
|
|
1035
|
+
const fieldsOption = withTypeManager.options.relationshipFields;
|
|
1036
|
+
const fields = fieldsOption && fieldsOption.add;
|
|
1037
|
+
field.fields = fields && klona(fields);
|
|
1038
|
+
field.schema = self.fieldsToArray(`Relationship field ${field.name}`, field.fields);
|
|
1039
|
+
}
|
|
1028
1040
|
}
|
|
1029
1041
|
if (field.schema && !field.fieldsStorage) {
|
|
1030
1042
|
field.fieldsStorage = field.name.replace(/^_/, '') + 'Fields';
|
|
@@ -1204,52 +1216,7 @@ module.exports = {
|
|
|
1204
1216
|
options.alterFields(schema);
|
|
1205
1217
|
}
|
|
1206
1218
|
|
|
1207
|
-
|
|
1208
|
-
let groups = [ {
|
|
1209
|
-
name: defaultGroup.name,
|
|
1210
|
-
label: defaultGroup.label,
|
|
1211
|
-
fields: _.map(schema, 'name')
|
|
1212
|
-
} ];
|
|
1213
|
-
|
|
1214
|
-
// if we are getting arrangeFields and it's not empty
|
|
1215
|
-
if (options.arrangeFields && options.arrangeFields.length > 0) {
|
|
1216
|
-
// if it's full of strings, use them for the default group
|
|
1217
|
-
if (_.isString(options.arrangeFields[0])) {
|
|
1218
|
-
groups[0].fields = options.arrangeFields; // if it's full of objects, those are groups, so use them
|
|
1219
|
-
} else if (_.isPlainObject(options.arrangeFields[0])) {
|
|
1220
|
-
// reset the default group's fields, but keep it around,
|
|
1221
|
-
// in case they have fields they forgot to put in a group
|
|
1222
|
-
groups[0].fields = [];
|
|
1223
|
-
groups = groups.concat(options.arrangeFields);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// If there is a later group with the same name, the later
|
|
1228
|
-
// one wins and the earlier is forgotten. Otherwise you can't
|
|
1229
|
-
// ever toss a field out of a group without putting it into
|
|
1230
|
-
// another one, which makes it impossible to un-group a
|
|
1231
|
-
// field and have it appear outside of tabs in the interface.
|
|
1232
|
-
//
|
|
1233
|
-
// A reconfigured group is ordered to the bottom of the list
|
|
1234
|
-
// of groups again, which has the intended effect if you
|
|
1235
|
-
// arrange all of the groups in your module config. However
|
|
1236
|
-
// it comes before any groups with the `last: true` flag that
|
|
1237
|
-
// were not reconfigured. Reconfiguring a group without that
|
|
1238
|
-
// flag clears it.
|
|
1239
|
-
|
|
1240
|
-
const newGroups = [];
|
|
1241
|
-
_.each(groups, function (group) {
|
|
1242
|
-
const index = _.findIndex(newGroups, { name: group.name });
|
|
1243
|
-
if (index !== -1) {
|
|
1244
|
-
newGroups.splice(index, 1);
|
|
1245
|
-
}
|
|
1246
|
-
let i = _.findIndex(newGroups, { last: true });
|
|
1247
|
-
if (i === -1) {
|
|
1248
|
-
i = groups.length;
|
|
1249
|
-
}
|
|
1250
|
-
newGroups.splice(i, 0, group);
|
|
1251
|
-
});
|
|
1252
|
-
groups = newGroups;
|
|
1219
|
+
const groups = self.composeGroups(schema, options.arrangeFields);
|
|
1253
1220
|
|
|
1254
1221
|
// all fields in the schema will end up in this variable
|
|
1255
1222
|
let newSchema = [];
|
|
@@ -1349,6 +1316,56 @@ module.exports = {
|
|
|
1349
1316
|
return schema;
|
|
1350
1317
|
},
|
|
1351
1318
|
|
|
1319
|
+
composeGroups (schema, arrangeFields) {
|
|
1320
|
+
// always make sure there is a default group
|
|
1321
|
+
let groups = [ {
|
|
1322
|
+
name: defaultGroup.name,
|
|
1323
|
+
label: defaultGroup.label,
|
|
1324
|
+
fields: _.map(schema, 'name')
|
|
1325
|
+
} ];
|
|
1326
|
+
|
|
1327
|
+
// if we are getting arrangeFields and it's not empty
|
|
1328
|
+
if (arrangeFields && arrangeFields.length > 0) {
|
|
1329
|
+
// if it's full of strings, use them for the default group
|
|
1330
|
+
if (_.isString(arrangeFields[0])) {
|
|
1331
|
+
groups[0].fields = arrangeFields; // if it's full of objects, those are groups, so use them
|
|
1332
|
+
} else if (_.isPlainObject(arrangeFields[0])) {
|
|
1333
|
+
// reset the default group's fields, but keep it around,
|
|
1334
|
+
// in case they have fields they forgot to put in a group
|
|
1335
|
+
groups[0].fields = [];
|
|
1336
|
+
groups = groups.concat(arrangeFields);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// If there is a later group with the same name, the later
|
|
1341
|
+
// one wins and the earlier is forgotten. Otherwise you can't
|
|
1342
|
+
// ever toss a field out of a group without putting it into
|
|
1343
|
+
// another one, which makes it impossible to un-group a
|
|
1344
|
+
// field and have it appear outside of tabs in the interface.
|
|
1345
|
+
//
|
|
1346
|
+
// A reconfigured group is ordered to the bottom of the list
|
|
1347
|
+
// of groups again, which has the intended effect if you
|
|
1348
|
+
// arrange all of the groups in your module config. However
|
|
1349
|
+
// it comes before any groups with the `last: true` flag that
|
|
1350
|
+
// were not reconfigured. Reconfiguring a group without that
|
|
1351
|
+
// flag clears it.
|
|
1352
|
+
|
|
1353
|
+
const newGroups = [];
|
|
1354
|
+
_.each(groups, function (group) {
|
|
1355
|
+
const index = _.findIndex(newGroups, { name: group.name });
|
|
1356
|
+
if (index !== -1) {
|
|
1357
|
+
newGroups.splice(index, 1);
|
|
1358
|
+
}
|
|
1359
|
+
let i = _.findIndex(newGroups, { last: true });
|
|
1360
|
+
if (i === -1) {
|
|
1361
|
+
i = groups.length;
|
|
1362
|
+
}
|
|
1363
|
+
newGroups.splice(i, 0, group);
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
return newGroups;
|
|
1367
|
+
},
|
|
1368
|
+
|
|
1352
1369
|
// Recursively set moduleName property of the field and any subfields,
|
|
1353
1370
|
// as might be found in array or object fields. `module` is an actual module
|
|
1354
1371
|
setModuleName(field, module) {
|
|
@@ -1693,7 +1710,6 @@ module.exports = {
|
|
|
1693
1710
|
}
|
|
1694
1711
|
const find = options.find;
|
|
1695
1712
|
const builders = options.builders || {};
|
|
1696
|
-
const hints = options.hints || {};
|
|
1697
1713
|
const getCriteria = options.getCriteria || {};
|
|
1698
1714
|
await method(items, idsStorage, fieldsStorage, objectField, ids => {
|
|
1699
1715
|
const idsCriteria = {};
|
|
@@ -1712,8 +1728,6 @@ module.exports = {
|
|
|
1712
1728
|
// Builders hardcoded as part of this relationship's options don't
|
|
1713
1729
|
// require any sanitization
|
|
1714
1730
|
query.applyBuilders(builders);
|
|
1715
|
-
// Hints, on the other hand, must be sanitized
|
|
1716
|
-
query.applyBuildersSafely(hints);
|
|
1717
1731
|
return query.toArray();
|
|
1718
1732
|
}, self.apos.doc.toAposDocId);
|
|
1719
1733
|
},
|
|
@@ -1854,8 +1868,7 @@ module.exports = {
|
|
|
1854
1868
|
|
|
1855
1869
|
const options = {
|
|
1856
1870
|
find: find,
|
|
1857
|
-
builders: { relationships: withRelationshipsNext[relationship._dotPath] || false }
|
|
1858
|
-
hints: {}
|
|
1871
|
+
builders: { relationships: withRelationshipsNext[relationship._dotPath] || false }
|
|
1859
1872
|
};
|
|
1860
1873
|
const subname = relationship.name + ':' + type;
|
|
1861
1874
|
const _relationship = _.assign({}, relationship, {
|
|
@@ -1871,13 +1884,6 @@ module.exports = {
|
|
|
1871
1884
|
if (_relationship.buildersByType && _relationship.buildersByType[type]) {
|
|
1872
1885
|
_.extend(options.builders, _relationship.buildersByType[type]);
|
|
1873
1886
|
}
|
|
1874
|
-
if (_relationship.hints) {
|
|
1875
|
-
_.extend(options.hints, _relationship.hints);
|
|
1876
|
-
}
|
|
1877
|
-
if (_relationship.hintsByType && _relationship.hintsByType[type]) {
|
|
1878
|
-
_.extend(options.hints, _relationship.hints);
|
|
1879
|
-
_.extend(options.hints, _relationship.hintsByType[type]);
|
|
1880
|
-
}
|
|
1881
1887
|
await self.apos.util.recursionGuard(req, `${_relationship.type}:${_relationship.withType}`, () => {
|
|
1882
1888
|
// Allow options to the getter to be specified in the schema,
|
|
1883
1889
|
return self.fieldTypes[_relationship.type].relate(req, _relationship, _objects, options);
|
|
@@ -1912,8 +1918,7 @@ module.exports = {
|
|
|
1912
1918
|
|
|
1913
1919
|
const options = {
|
|
1914
1920
|
find: find,
|
|
1915
|
-
builders: { relationships: withRelationshipsNext[relationship._dotPath] || false }
|
|
1916
|
-
hints: {}
|
|
1921
|
+
builders: { relationships: withRelationshipsNext[relationship._dotPath] || false }
|
|
1917
1922
|
};
|
|
1918
1923
|
|
|
1919
1924
|
// Allow options to the get() method to be
|
|
@@ -1921,9 +1926,6 @@ module.exports = {
|
|
|
1921
1926
|
if (relationship.builders) {
|
|
1922
1927
|
_.extend(options.builders, relationship.builders);
|
|
1923
1928
|
}
|
|
1924
|
-
if (relationship.hints) {
|
|
1925
|
-
_.extend(options.hints, relationship.hints);
|
|
1926
|
-
}
|
|
1927
1929
|
|
|
1928
1930
|
// Allow options to the getter to be specified in the schema
|
|
1929
1931
|
await self.apos.util.recursionGuard(req, `${relationship.type}:${relationship.withType}`, () => {
|
|
@@ -2479,25 +2481,24 @@ module.exports = {
|
|
|
2479
2481
|
return idsStorageFields[name] || name;
|
|
2480
2482
|
}
|
|
2481
2483
|
},
|
|
2482
|
-
groupsToArray(groups) {
|
|
2484
|
+
groupsToArray(groups = {}) {
|
|
2483
2485
|
return Object.keys(groups).map(name => ({
|
|
2484
2486
|
name,
|
|
2485
2487
|
...groups[name]
|
|
2486
2488
|
}));
|
|
2487
2489
|
},
|
|
2488
|
-
fieldsToArray(context, fields) {
|
|
2490
|
+
fieldsToArray(context, fields = {}) {
|
|
2489
2491
|
const result = [];
|
|
2490
2492
|
for (const name of Object.keys(fields)) {
|
|
2491
2493
|
const field = {
|
|
2492
2494
|
name,
|
|
2493
2495
|
...fields[name]
|
|
2494
2496
|
};
|
|
2497
|
+
const fieldTypesWithSchemas = [ 'object', 'array', 'relationship' ];
|
|
2495
2498
|
// TODO same for relationship schemas but they are being refactored in another PR
|
|
2496
|
-
if (
|
|
2497
|
-
if (field.type !== 'relationship') {
|
|
2498
|
-
|
|
2499
|
-
throw new Error(`${context}: the subfield ${name} requires a 'fields' property, with an 'add' subproperty containing its own fields.`);
|
|
2500
|
-
}
|
|
2499
|
+
if (fieldTypesWithSchemas.includes(field.type)) {
|
|
2500
|
+
if (field.type !== 'relationship' && !field.fields) {
|
|
2501
|
+
throw new Error(`${context}: the subfield ${name} requires a 'fields' property, with an 'add' subproperty containing its own fields.`);
|
|
2501
2502
|
}
|
|
2502
2503
|
if (field.fields) {
|
|
2503
2504
|
if (!field.fields.add) {
|
|
@@ -2507,11 +2508,16 @@ module.exports = {
|
|
|
2507
2508
|
throw new Error(`${context}: the subfield ${name} must have a 'fields' property with an 'add' subproperty containing its own fields.`);
|
|
2508
2509
|
}
|
|
2509
2510
|
}
|
|
2510
|
-
|
|
2511
|
+
|
|
2512
|
+
field.schema = self.compose({
|
|
2513
|
+
addFields: self.fieldsToArray(context, field.fields.add),
|
|
2514
|
+
arrangeFields: self.groupsToArray(field.fields.group)
|
|
2515
|
+
});
|
|
2511
2516
|
}
|
|
2512
2517
|
}
|
|
2513
2518
|
result.push(field);
|
|
2514
2519
|
}
|
|
2520
|
+
|
|
2515
2521
|
return result;
|
|
2516
2522
|
},
|
|
2517
2523
|
// Array "managers" currently offer just a schema property, for parallelism
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
:id="next._id"
|
|
23
23
|
:field-id="field._id"
|
|
24
24
|
:field="field"
|
|
25
|
+
:generation="generation"
|
|
25
26
|
@changed="changed"
|
|
26
27
|
/>
|
|
27
28
|
</div>
|
|
@@ -37,6 +38,15 @@ import cuid from 'cuid';
|
|
|
37
38
|
export default {
|
|
38
39
|
name: 'AposInputArea',
|
|
39
40
|
mixins: [ AposInputMixin ],
|
|
41
|
+
props: {
|
|
42
|
+
generation: {
|
|
43
|
+
type: Number,
|
|
44
|
+
required: false,
|
|
45
|
+
default() {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
40
50
|
data () {
|
|
41
51
|
return {
|
|
42
52
|
next: this.value.data || this.getEmptyValue(),
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
:schema="field.schema"
|
|
15
15
|
:trigger-validation="triggerValidation"
|
|
16
16
|
:utility-rail="false"
|
|
17
|
+
:generation="generation"
|
|
17
18
|
v-model="schemaInput"
|
|
18
19
|
ref="schema"
|
|
19
20
|
/>
|
|
@@ -25,13 +26,21 @@
|
|
|
25
26
|
|
|
26
27
|
<script>
|
|
27
28
|
import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
|
|
28
|
-
import { klona } from 'klona';
|
|
29
29
|
|
|
30
30
|
export default {
|
|
31
31
|
name: 'AposInputObject',
|
|
32
32
|
mixins: [ AposInputMixin ],
|
|
33
|
+
props: {
|
|
34
|
+
generation: {
|
|
35
|
+
type: Number,
|
|
36
|
+
required: false,
|
|
37
|
+
default() {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
33
42
|
data () {
|
|
34
|
-
const next = this.
|
|
43
|
+
const next = this.getNext();
|
|
35
44
|
return {
|
|
36
45
|
schemaInput: {
|
|
37
46
|
data: next
|
|
@@ -42,6 +51,12 @@ export default {
|
|
|
42
51
|
watch: {
|
|
43
52
|
schemaInput() {
|
|
44
53
|
this.next = this.schemaInput.data;
|
|
54
|
+
},
|
|
55
|
+
generation() {
|
|
56
|
+
this.next = this.getNext();
|
|
57
|
+
this.schemaInput = {
|
|
58
|
+
data: this.next
|
|
59
|
+
};
|
|
45
60
|
}
|
|
46
61
|
},
|
|
47
62
|
methods: {
|
|
@@ -49,6 +64,10 @@ export default {
|
|
|
49
64
|
if (this.schemaInput.hasErrors) {
|
|
50
65
|
return 'invalid';
|
|
51
66
|
}
|
|
67
|
+
},
|
|
68
|
+
// Return next at mount or when generation changes
|
|
69
|
+
getNext() {
|
|
70
|
+
return this.value ? this.value.data : (this.field.def || {});
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
73
|
};
|
|
@@ -64,4 +83,4 @@ export default {
|
|
|
64
83
|
.apos-input-object ::v-deep .apos-schema .apos-field {
|
|
65
84
|
margin-bottom: 30px;
|
|
66
85
|
}
|
|
67
|
-
</style>
|
|
86
|
+
</style>
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
:value="next"
|
|
44
44
|
:disabled="field.readOnly"
|
|
45
45
|
:has-relationship-schema="!!field.schema"
|
|
46
|
+
:editor-label="field.editorLabel"
|
|
47
|
+
:editor-icon="field.editorIcon"
|
|
46
48
|
/>
|
|
47
49
|
<AposSearchList
|
|
48
50
|
:list="searchList"
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
|
|
58
60
|
<script>
|
|
59
61
|
import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
|
|
62
|
+
import { klona } from 'klona';
|
|
60
63
|
|
|
61
64
|
export default {
|
|
62
65
|
name: 'AposInputRelationship',
|
|
@@ -64,18 +67,21 @@ export default {
|
|
|
64
67
|
emits: [ 'input' ],
|
|
65
68
|
data () {
|
|
66
69
|
const next = (this.value && Array.isArray(this.value.data))
|
|
67
|
-
? this.value.data : (this.field.def || []);
|
|
70
|
+
? klona(this.value.data) : (klona(this.field.def) || []);
|
|
71
|
+
|
|
72
|
+
// Remember relationship subfield values even if a document
|
|
73
|
+
// is temporarily deselected, easing the user's pain if they
|
|
74
|
+
// inadvertently deselect something for a moment
|
|
75
|
+
const subfields = Object.fromEntries(
|
|
76
|
+
(next || []).filter(doc => doc._fields)
|
|
77
|
+
.map(doc => [ doc._id, doc._fields ])
|
|
78
|
+
);
|
|
79
|
+
|
|
68
80
|
return {
|
|
69
81
|
searchTerm: '',
|
|
70
82
|
searchList: [],
|
|
71
83
|
next,
|
|
72
|
-
|
|
73
|
-
// is temporarily deselected, easing the user's pain if they
|
|
74
|
-
// inadvertently deselect something for a moment
|
|
75
|
-
subfields: Object.fromEntries((this.next || [])
|
|
76
|
-
.filter(doc => doc._fields)
|
|
77
|
-
.map(doc => [ doc._id, doc._fields ])
|
|
78
|
-
),
|
|
84
|
+
subfields,
|
|
79
85
|
disabled: false,
|
|
80
86
|
searching: false,
|
|
81
87
|
choosing: false,
|
|
@@ -129,18 +135,16 @@ export default {
|
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
},
|
|
138
|
+
mounted () {
|
|
139
|
+
this.checkLimit();
|
|
140
|
+
},
|
|
132
141
|
methods: {
|
|
133
142
|
validate(value) {
|
|
143
|
+
this.checkLimit();
|
|
144
|
+
|
|
134
145
|
if (this.field.required && !value.length) {
|
|
135
146
|
return { message: 'required' };
|
|
136
147
|
}
|
|
137
|
-
if (this.limitReached) {
|
|
138
|
-
this.searchTerm = 'Limit reached!';
|
|
139
|
-
this.disabled = true;
|
|
140
|
-
} else {
|
|
141
|
-
this.searchTerm = '';
|
|
142
|
-
this.disabled = false;
|
|
143
|
-
}
|
|
144
148
|
|
|
145
149
|
if (this.field.min && this.field.min > value.length) {
|
|
146
150
|
return { message: `minimum of ${this.field.min} required` };
|
|
@@ -148,31 +152,54 @@ export default {
|
|
|
148
152
|
|
|
149
153
|
return false;
|
|
150
154
|
},
|
|
155
|
+
checkLimit() {
|
|
156
|
+
if (this.limitReached) {
|
|
157
|
+
this.searchTerm = 'Limit reached!';
|
|
158
|
+
} else if (this.searchTerm === 'Limit reached!') {
|
|
159
|
+
this.searchTerm = '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.disabled = !!this.limitReached;
|
|
163
|
+
},
|
|
151
164
|
updateSelected(items) {
|
|
152
165
|
this.next = items;
|
|
153
166
|
},
|
|
154
167
|
async input () {
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
});
|
|
171
|
-
this.searching = false;
|
|
172
|
-
} else {
|
|
173
|
-
this.searchList = [];
|
|
174
|
-
}
|
|
168
|
+
if (this.searching) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!this.searchTerm.length) {
|
|
173
|
+
this.searchList = [];
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const qs = {
|
|
178
|
+
autocomplete: this.searchTerm
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (this.field.withType === '@apostrophecms/image') {
|
|
182
|
+
apos.bus.$emit('piece-relationship-query', qs);
|
|
175
183
|
}
|
|
184
|
+
|
|
185
|
+
this.searching = true;
|
|
186
|
+
const list = await apos.http.get(
|
|
187
|
+
apos.modules[this.field.withType].action,
|
|
188
|
+
{
|
|
189
|
+
busy: false,
|
|
190
|
+
draft: true,
|
|
191
|
+
qs
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
// filter items already selected
|
|
195
|
+
this.searchList = list.results
|
|
196
|
+
.filter(item => !this.next.map(i => i._id).includes(item._id))
|
|
197
|
+
.map(item => ({
|
|
198
|
+
...item,
|
|
199
|
+
disabled: this.disableUnpublished && !item.lastPublishedAt
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
this.searching = false;
|
|
176
203
|
},
|
|
177
204
|
handleFocusOut() {
|
|
178
205
|
// hide search list when click outside the input
|
|
@@ -198,11 +225,15 @@ export default {
|
|
|
198
225
|
}
|
|
199
226
|
},
|
|
200
227
|
async editRelationship (item) {
|
|
201
|
-
const
|
|
228
|
+
const editor = this.field.editor || 'AposRelationshipEditor';
|
|
229
|
+
|
|
230
|
+
const result = await apos.modal.execute(editor, {
|
|
202
231
|
schema: this.field.schema,
|
|
232
|
+
item,
|
|
203
233
|
title: item.title,
|
|
204
234
|
value: item._fields
|
|
205
235
|
});
|
|
236
|
+
|
|
206
237
|
if (result) {
|
|
207
238
|
const index = this.next.findIndex(_item => _item._id === item._id);
|
|
208
239
|
this.$set(this.next, index, {
|
|
@@ -210,6 +241,11 @@ export default {
|
|
|
210
241
|
_fields: result
|
|
211
242
|
});
|
|
212
243
|
}
|
|
244
|
+
},
|
|
245
|
+
getEditRelationshipLabel () {
|
|
246
|
+
if (this.field.editor === 'AposImageRelationshipEditor') {
|
|
247
|
+
return 'apostrophe:editImageAdjustments';
|
|
248
|
+
}
|
|
213
249
|
}
|
|
214
250
|
}
|
|
215
251
|
};
|
|
@@ -7,26 +7,13 @@
|
|
|
7
7
|
:display-options="displayOptions"
|
|
8
8
|
>
|
|
9
9
|
<template #body>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
v-for="choice in choices" :key="JSON.stringify(choice.value)"
|
|
18
|
-
:value="JSON.stringify(choice.value)"
|
|
19
|
-
:selected="choice.value === value.data"
|
|
20
|
-
>
|
|
21
|
-
{{ $t(choice.label) }}
|
|
22
|
-
</option>
|
|
23
|
-
</select>
|
|
24
|
-
<AposIndicator
|
|
25
|
-
icon="menu-down-icon"
|
|
26
|
-
class="apos-input-icon"
|
|
27
|
-
:icon-size="20"
|
|
28
|
-
/>
|
|
29
|
-
</div>
|
|
10
|
+
<AposSelect
|
|
11
|
+
:icon="icon"
|
|
12
|
+
:choices="choices"
|
|
13
|
+
:disabled="field.readOnly"
|
|
14
|
+
:selected="value.data"
|
|
15
|
+
@change="change"
|
|
16
|
+
/>
|
|
30
17
|
</template>
|
|
31
18
|
</AposInputWrapper>
|
|
32
19
|
</template>
|
|
@@ -105,9 +92,3 @@ export default {
|
|
|
105
92
|
}
|
|
106
93
|
};
|
|
107
94
|
</script>
|
|
108
|
-
|
|
109
|
-
<style lang="scss" scoped>
|
|
110
|
-
.apos-input-icon {
|
|
111
|
-
@include apos-transition();
|
|
112
|
-
}
|
|
113
|
-
</style>
|
|
@@ -172,6 +172,14 @@ export default {
|
|
|
172
172
|
if ((s == null) || (s === '')) {
|
|
173
173
|
return s;
|
|
174
174
|
} else {
|
|
175
|
+
// The native parse float converts 3.0 to 3 and makes
|
|
176
|
+
// next to become integer. In theory we don't need parseFloat
|
|
177
|
+
// as the value is natively guarded by the browser 'number' type.
|
|
178
|
+
// However we need a float value sent to the backend
|
|
179
|
+
// and we force that when focus is lost.
|
|
180
|
+
if (this.focus && `${s}`.match(/\.[0]*$/)) {
|
|
181
|
+
return s;
|
|
182
|
+
}
|
|
175
183
|
return parseFloat(s);
|
|
176
184
|
}
|
|
177
185
|
} else {
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
AposSchema takes an array of fields
|
|
2
|
+
AposSchema takes an array of fields (`schema`), renders their inputs,
|
|
3
|
+
and emits a new object with a `value` subproperty and a `hasErrors`
|
|
4
|
+
subproperty via the input event whenever the value of a field
|
|
5
|
+
or subfield changes.
|
|
6
|
+
|
|
7
|
+
At mount time the fields are initialized from the subproperties of the
|
|
8
|
+
`value.data` prop.
|
|
9
|
+
|
|
10
|
+
For performance reasons, this component is not strictly v-model compliant.
|
|
11
|
+
While all changes will emit an outgoing `input` event, the
|
|
12
|
+
incoming `value` prop only updates the fields in three situations:
|
|
13
|
+
|
|
14
|
+
1. At mount time, to set the initial values of the fields.
|
|
15
|
+
|
|
16
|
+
2. When `value.data._id` changes (an entirely different document is in play).
|
|
17
|
+
|
|
18
|
+
3. When the optional prop `generation` changes to a new number. This
|
|
19
|
+
prop is also passed on to the individual input field components.
|
|
20
|
+
|
|
21
|
+
If you need to force an update from the calling component, increment the
|
|
22
|
+
`generation` prop. This should be done only if the value has changed for
|
|
23
|
+
an external reason.
|
|
3
24
|
-->
|
|
4
25
|
<template>
|
|
5
26
|
<div class="apos-schema">
|
|
@@ -20,6 +41,7 @@
|
|
|
20
41
|
:server-error="fields[field.name].serverError"
|
|
21
42
|
:doc-id="docId"
|
|
22
43
|
:ref="field.name"
|
|
44
|
+
:generation="generation"
|
|
23
45
|
/>
|
|
24
46
|
</div>
|
|
25
47
|
</div>
|
|
@@ -35,6 +57,13 @@ export default {
|
|
|
35
57
|
type: Object,
|
|
36
58
|
required: true
|
|
37
59
|
},
|
|
60
|
+
generation: {
|
|
61
|
+
type: Number,
|
|
62
|
+
required: false,
|
|
63
|
+
default() {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
38
67
|
schema: {
|
|
39
68
|
type: Array,
|
|
40
69
|
required: true
|
|
@@ -135,22 +164,23 @@ export default {
|
|
|
135
164
|
schema() {
|
|
136
165
|
this.populateDocData();
|
|
137
166
|
},
|
|
138
|
-
value
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
// repopulate the schema.
|
|
150
|
-
this.populateDocData();
|
|
151
|
-
}
|
|
167
|
+
'value.data._id'(_id) {
|
|
168
|
+
// The doc might be swapped out completely in cases such as the media
|
|
169
|
+
// library editor. Repopulate the fields if that happens.
|
|
170
|
+
if (
|
|
171
|
+
// If the fieldState had been cleared and there's new populated data
|
|
172
|
+
(!this.fieldState._id && _id) ||
|
|
173
|
+
// or if there *is* active fieldState, but the new data is a new doc
|
|
174
|
+
(this.fieldState._id && _id !== this.fieldState._id.data)
|
|
175
|
+
) {
|
|
176
|
+
// repopulate the schema.
|
|
177
|
+
this.populateDocData();
|
|
152
178
|
}
|
|
153
179
|
},
|
|
180
|
+
generation() {
|
|
181
|
+
// repopulate the schema.
|
|
182
|
+
this.populateDocData();
|
|
183
|
+
},
|
|
154
184
|
conditionalFields(newVal, oldVal) {
|
|
155
185
|
for (const field in oldVal) {
|
|
156
186
|
if (!this.fieldState[field] || (newVal[field] === oldVal[field]) || !this.fieldState[field].ranValidation) {
|