apostrophe 3.58.1 → 3.59.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 +32 -0
- package/modules/@apostrophecms/area/index.js +7 -0
- package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +3 -0
- package/modules/@apostrophecms/doc-type/index.js +1 -0
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +11 -4
- package/modules/@apostrophecms/express/index.js +28 -0
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocErrorsMixin.js +5 -4
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +11 -8
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposModalTabsMixin.js +4 -3
- package/modules/@apostrophecms/module/index.js +6 -0
- package/modules/@apostrophecms/piece-page-type/index.js +9 -3
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +10 -3
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +6 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +12 -3
- package/modules/@apostrophecms/schema/index.js +105 -51
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +3 -3
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +10 -6
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +5 -3
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +16 -11
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +2 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +3 -3
- package/modules/@apostrophecms/schema/ui/apos/lib/conditionalFields.js +76 -71
- package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +9 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +48 -20
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js +10 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +8 -3
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +3 -2
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +45 -25
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +4 -1
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputConditionalFieldsMixin.js +11 -4
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputFollowingMixin.js +1 -1
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +4 -6
- package/modules/@apostrophecms/template/index.js +99 -31
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +5 -2
- package/package.json +1 -1
- package/test/schemas.js +1700 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.59.0 (2023-11-03)
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
* Webpack warnings about package size during the admin UI build process have been turned off by default. Warnings are still enabled for the public build, where a large bundle can be problematic for SEO.
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
|
|
11
|
+
* Apostrophe warns you if you have more than one piece page for the same piece type and you have not overridden `chooseParentPage`
|
|
12
|
+
to help Apostrophe decide which page is suitable as the `_url` of each piece. Beginning with this release, Apostrophe can recognize
|
|
13
|
+
when you have chosen to do this via `extendMethods`, so that you can call `_super()` to fall back to the default implementation without
|
|
14
|
+
receiving this warning. The default implementation still just returns the first page found, but always following the
|
|
15
|
+
`_super()` pattern here opens the door to npm modules that `improve` `@apostrophecms/piece-page` to do something more
|
|
16
|
+
sophisticated by default.
|
|
17
|
+
* `newInstance` always returns a reasonable non-null empty value for area and
|
|
18
|
+
object fields in case the document is inserted without being passed through
|
|
19
|
+
the editor, e.g. in a parked page like the home page. This simplifies
|
|
20
|
+
the new external front feature.
|
|
21
|
+
|
|
22
|
+
### Adds
|
|
23
|
+
|
|
24
|
+
* An adapter for Astro is under development with support from Michelin.
|
|
25
|
+
Starting with this release, adapters for external fronts, i.e. "back for front"
|
|
26
|
+
frameworks such as Astro, may now be implemented more easily. Apostrophe recognizes the
|
|
27
|
+
`x-requested-with: AposExternalFront` header and the `apos-external-front-key` header.
|
|
28
|
+
If both are present and `apos-external-front-key` matches the `APOS_EXTERNAL_FRONT_KEY`
|
|
29
|
+
environment variable, then Apostrophe returns JSON in place of a normal page response.
|
|
30
|
+
This mechanism is also available for the `render-widget` route.
|
|
31
|
+
* Like `type`, `metaType` is always included in projections. This helps
|
|
32
|
+
ensure that `apos.util.getManagerOf()` can be used on any object returned
|
|
33
|
+
by the Apostrophe APIs.
|
|
34
|
+
|
|
3
35
|
## 3.58.1 (2023-10-18)
|
|
4
36
|
|
|
5
37
|
### Security
|
|
@@ -68,6 +68,13 @@ module.exports = {
|
|
|
68
68
|
return manager.loadIfSuitable(req, [ widget ]);
|
|
69
69
|
}
|
|
70
70
|
async function render() {
|
|
71
|
+
if (req.aposExternalFront) {
|
|
72
|
+
const result = {
|
|
73
|
+
...req.data,
|
|
74
|
+
widget
|
|
75
|
+
};
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
71
78
|
return self.renderWidget(req, type, widget, options);
|
|
72
79
|
}
|
|
73
80
|
}
|
|
@@ -24,6 +24,9 @@ module.exports = ({
|
|
|
24
24
|
|
|
25
25
|
const pnpmModulePath = apos.isPnpm ? [ path.join(apos.selfDir, '../') ] : [];
|
|
26
26
|
const config = {
|
|
27
|
+
performance: {
|
|
28
|
+
hints: false
|
|
29
|
+
},
|
|
27
30
|
entry: importFile,
|
|
28
31
|
// Ensure that the correct version of vue-loader is found
|
|
29
32
|
context: __dirname,
|
|
@@ -1629,6 +1629,7 @@ module.exports = {
|
|
|
1629
1629
|
const hasExclusion = Object.values(projection).some(value => !value);
|
|
1630
1630
|
if (!_.isEmpty(projection) && !hasExclusion) {
|
|
1631
1631
|
add.push('type');
|
|
1632
|
+
add.push('metaType');
|
|
1632
1633
|
}
|
|
1633
1634
|
|
|
1634
1635
|
for (const [ key, val ] of Object.entries(projection)) {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
:trigger-validation="triggerValidation"
|
|
65
65
|
:utility-rail="false"
|
|
66
66
|
:following-values="followingValues('other')"
|
|
67
|
-
:conditional-fields="conditionalFields
|
|
67
|
+
:conditional-fields="conditionalFields"
|
|
68
68
|
:doc-id="docId"
|
|
69
69
|
:value="docFields"
|
|
70
70
|
:server-errors="serverErrors"
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
:trigger-validation="triggerValidation"
|
|
90
90
|
:utility-rail="true"
|
|
91
91
|
:following-values="followingUtils"
|
|
92
|
-
:conditional-fields="conditionalFields
|
|
92
|
+
:conditional-fields="conditionalFields"
|
|
93
93
|
:doc-id="docId"
|
|
94
94
|
:value="docFields"
|
|
95
95
|
@input="updateDocFields"
|
|
@@ -321,7 +321,7 @@ export default {
|
|
|
321
321
|
},
|
|
322
322
|
watch: {
|
|
323
323
|
'docFields.data.type': {
|
|
324
|
-
handler(newVal
|
|
324
|
+
handler(newVal) {
|
|
325
325
|
if (this.moduleName !== '@apostrophecms/page') {
|
|
326
326
|
return;
|
|
327
327
|
}
|
|
@@ -342,6 +342,7 @@ export default {
|
|
|
342
342
|
},
|
|
343
343
|
async mounted() {
|
|
344
344
|
this.modal.active = true;
|
|
345
|
+
await this.evaluateExternalConditions();
|
|
345
346
|
// After computed properties become available
|
|
346
347
|
this.saveMenu = this.computeSaveMenu();
|
|
347
348
|
this.cancelDescription = {
|
|
@@ -349,6 +350,7 @@ export default {
|
|
|
349
350
|
type: this.$t(this.moduleOptions.label)
|
|
350
351
|
};
|
|
351
352
|
if (this.docId) {
|
|
353
|
+
this.evaluateConditions();
|
|
352
354
|
await this.loadDoc();
|
|
353
355
|
try {
|
|
354
356
|
if (this.manuallyPublished) {
|
|
@@ -372,6 +374,8 @@ export default {
|
|
|
372
374
|
}
|
|
373
375
|
this.modal.triggerFocusRefresh++;
|
|
374
376
|
} else if (this.copyOfId) {
|
|
377
|
+
this.evaluateConditions();
|
|
378
|
+
|
|
375
379
|
// Because the page or piece manager might give us just a projected,
|
|
376
380
|
// minimum number of properties otherwise, and because we need to
|
|
377
381
|
// make sure we use our preferred module to fetch the content
|
|
@@ -405,6 +409,7 @@ export default {
|
|
|
405
409
|
} else {
|
|
406
410
|
this.$nextTick(async () => {
|
|
407
411
|
await this.loadNewInstance();
|
|
412
|
+
this.evaluateConditions();
|
|
408
413
|
this.modal.triggerFocusRefresh++;
|
|
409
414
|
});
|
|
410
415
|
}
|
|
@@ -452,7 +457,7 @@ export default {
|
|
|
452
457
|
const canEdit = docData._edit || this.moduleOptions.canEdit;
|
|
453
458
|
this.readOnly = canEdit === false;
|
|
454
459
|
if (canEdit && !await this.lock(this.getOnePath, this.docId)) {
|
|
455
|
-
|
|
460
|
+
this.lockNotAvailable();
|
|
456
461
|
return;
|
|
457
462
|
}
|
|
458
463
|
} catch {
|
|
@@ -679,6 +684,8 @@ export default {
|
|
|
679
684
|
...this.docFields.data,
|
|
680
685
|
...value.data
|
|
681
686
|
};
|
|
687
|
+
|
|
688
|
+
this.evaluateConditions();
|
|
682
689
|
},
|
|
683
690
|
getAposSchema(field) {
|
|
684
691
|
if (field.group.name === 'utility') {
|
|
@@ -162,6 +162,7 @@ module.exports = {
|
|
|
162
162
|
self.createApp();
|
|
163
163
|
self.prefix();
|
|
164
164
|
self.trustProxy();
|
|
165
|
+
self.options.externalFrontKey = process.env.APOS_EXTERNAL_FRONT_KEY || self.options.externalFrontKey;
|
|
165
166
|
if (self.options.baseUrl && !self.apos.baseUrl) {
|
|
166
167
|
self.apos.util.error('WARNING: you have baseUrl set as an option to the `@apostrophecms/express` module.');
|
|
167
168
|
self.apos.util.error('Set it as a global option (a property of the main object passed to apostrophe).');
|
|
@@ -249,6 +250,33 @@ module.exports = {
|
|
|
249
250
|
url: '/api/v1',
|
|
250
251
|
middleware: cors()
|
|
251
252
|
},
|
|
253
|
+
externalFront(req, res, next) {
|
|
254
|
+
if (req.headers['x-requested-with'] !== 'AposExternalFront') {
|
|
255
|
+
return next();
|
|
256
|
+
}
|
|
257
|
+
if ((!self.options.externalFrontKey) || (req.headers['apos-external-front-key'] !== self.options.externalFrontKey)) {
|
|
258
|
+
if (!self.options.externalFrontKey) {
|
|
259
|
+
self.logError('externalFrontNotEnabled', 'An attempt was made to integrate an external front but the externalFrontKey option has not been set on the @apostrophecms/express module');
|
|
260
|
+
} else {
|
|
261
|
+
self.logError('externalFrontKeyInvalid', 'An attempt was made to integrate an external front but the apos-external-front-key header was missing or did not match the externalFrontKey option set on the @apostrophecms/express module');
|
|
262
|
+
}
|
|
263
|
+
return res.status(403).send('forbidden');
|
|
264
|
+
}
|
|
265
|
+
req.aposExternalFront = true;
|
|
266
|
+
res.redirect = function(...args) {
|
|
267
|
+
// The external front end needs to issue the actual redirect,
|
|
268
|
+
// not us
|
|
269
|
+
// Per Express handling of 1 arg versus 2
|
|
270
|
+
const status = args.length > 1 ? args[0] : 302;
|
|
271
|
+
const url = args[args.length - 1];
|
|
272
|
+
return res.send({
|
|
273
|
+
redirect: true,
|
|
274
|
+
url,
|
|
275
|
+
status
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
return next();
|
|
279
|
+
},
|
|
252
280
|
attachUtilityMethods(req, res, next) {
|
|
253
281
|
// We apply the super pattern variously to res.redirect,
|
|
254
282
|
// make sure the original version is always available
|
|
@@ -4,7 +4,6 @@ export default {
|
|
|
4
4
|
data: () => ({
|
|
5
5
|
fieldErrors: {},
|
|
6
6
|
errorCount: 0
|
|
7
|
-
|
|
8
7
|
}),
|
|
9
8
|
mounted () {
|
|
10
9
|
this.prepErrors();
|
|
@@ -28,17 +27,19 @@ export default {
|
|
|
28
27
|
}
|
|
29
28
|
});
|
|
30
29
|
}
|
|
30
|
+
|
|
31
31
|
this.updateErrorCount();
|
|
32
32
|
},
|
|
33
33
|
updateErrorCount() {
|
|
34
34
|
let count = 0;
|
|
35
|
-
for (const
|
|
36
|
-
for (const
|
|
37
|
-
if (this.fieldErrors[
|
|
35
|
+
for (const group in this.fieldErrors) {
|
|
36
|
+
for (const field in this.fieldErrors[group]) {
|
|
37
|
+
if (this.fieldErrors[group][field]) {
|
|
38
38
|
count++;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
|
|
42
43
|
this.errorCount = count;
|
|
43
44
|
},
|
|
44
45
|
prepErrors() {
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { klona } from 'klona';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
evaluateExternalConditions, getConditionalFields, getConditionTypesObject
|
|
20
|
+
} from 'Modules/@apostrophecms/schema/lib/conditionalFields.js';
|
|
19
21
|
|
|
20
22
|
export default {
|
|
21
23
|
data() {
|
|
@@ -27,7 +29,8 @@ export default {
|
|
|
27
29
|
restoreOnly: false,
|
|
28
30
|
readOnly: false,
|
|
29
31
|
changed: [],
|
|
30
|
-
externalConditionsResults:
|
|
32
|
+
externalConditionsResults: getConditionTypesObject(),
|
|
33
|
+
conditionalFields: getConditionTypesObject()
|
|
31
34
|
};
|
|
32
35
|
},
|
|
33
36
|
|
|
@@ -57,10 +60,6 @@ export default {
|
|
|
57
60
|
}
|
|
58
61
|
},
|
|
59
62
|
|
|
60
|
-
async created() {
|
|
61
|
-
await this.evaluateExternalConditions();
|
|
62
|
-
},
|
|
63
|
-
|
|
64
63
|
methods: {
|
|
65
64
|
// Evaluate the external conditions found in each field
|
|
66
65
|
// via API calls -made in parallel for performance-
|
|
@@ -129,8 +128,8 @@ export default {
|
|
|
129
128
|
// the returned object will contain properties only for conditional fields
|
|
130
129
|
// in that category, although they may be conditional upon fields in either
|
|
131
130
|
// category.
|
|
132
|
-
|
|
133
|
-
return
|
|
131
|
+
getConditionalFields(followedByCategory) {
|
|
132
|
+
return getConditionalFields(
|
|
134
133
|
this.schema,
|
|
135
134
|
this.getFieldsByCategory(followedByCategory),
|
|
136
135
|
// currentDoc for arrays, docFields for all other editors
|
|
@@ -139,6 +138,10 @@ export default {
|
|
|
139
138
|
);
|
|
140
139
|
},
|
|
141
140
|
|
|
141
|
+
evaluateConditions() {
|
|
142
|
+
this.conditionalFields = this.getConditionalFields();
|
|
143
|
+
},
|
|
144
|
+
|
|
142
145
|
// Overridden by components that split the fields into several AposSchemas
|
|
143
146
|
getFieldValue(name) {
|
|
144
147
|
return this.docFields.data[name];
|
|
@@ -46,14 +46,15 @@ export default {
|
|
|
46
46
|
const tabs = [];
|
|
47
47
|
for (const key in this.groups) {
|
|
48
48
|
if (key !== 'utility') {
|
|
49
|
-
// AposRelationshipEditor does not implement AposEditorMixin with the function
|
|
50
|
-
const conditionalFields = this.conditionalFields?.('other') || [];
|
|
49
|
+
// AposRelationshipEditor does not implement AposEditorMixin with the function getConditionalFields
|
|
51
50
|
const fields = this.groups[key].fields;
|
|
52
51
|
tabs.push({
|
|
53
52
|
name: key,
|
|
54
53
|
label: this.groups[key].label,
|
|
55
54
|
fields,
|
|
56
|
-
isVisible:
|
|
55
|
+
isVisible: this.conditionalFields?.if
|
|
56
|
+
? fields.some(field => this.conditionalFields.if[field] !== false)
|
|
57
|
+
: true
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -423,6 +423,12 @@ module.exports = {
|
|
|
423
423
|
// for this part of the behavior of sendPage.
|
|
424
424
|
|
|
425
425
|
async renderPage(req, template, data) {
|
|
426
|
+
if (req.aposExternalFront) {
|
|
427
|
+
await self.apos.template.annotateDataForExternalFront(req, template, data);
|
|
428
|
+
self.apos.template.pruneDataForExternalFront(req, template, data);
|
|
429
|
+
// Reply with JSON
|
|
430
|
+
return data;
|
|
431
|
+
}
|
|
426
432
|
return self.apos.template.renderPageForModule(req, template, data, self);
|
|
427
433
|
},
|
|
428
434
|
|
|
@@ -40,7 +40,7 @@ module.exports = {
|
|
|
40
40
|
self.enableAddUrlsToPieces();
|
|
41
41
|
},
|
|
42
42
|
methods(self) {
|
|
43
|
-
|
|
43
|
+
const methods = {
|
|
44
44
|
|
|
45
45
|
// Extend this method for your piece type to call additional
|
|
46
46
|
// query builders by default.
|
|
@@ -212,8 +212,11 @@ module.exports = {
|
|
|
212
212
|
// for their design.
|
|
213
213
|
|
|
214
214
|
chooseParentPage(pages, piece) {
|
|
215
|
-
if
|
|
216
|
-
|
|
215
|
+
// Complain if this method is called with more than one page without an
|
|
216
|
+
// extension to make it smart enough to presumably do something intelligent
|
|
217
|
+
// in that situation. Don't complain though if this is just a call to _super
|
|
218
|
+
if ((self.originalChooseParentPage === self.chooseParentPage) && (pages.length > 1)) {
|
|
219
|
+
self.apos.util.warnDevOnce(`${self.__meta.name}/chooseParentPage`, `Your site has more than one ${self.name} page, but does not extend the chooseParentPage\nmethod in ${self.__meta.name} to choose the right one for individual ${self.pieces.name}. You should also extend filterByIndexPage.\nOtherwise URLs for each ${self.pieces.name} will point to an arbitrarily chosen page.`);
|
|
217
220
|
}
|
|
218
221
|
return pages[0];
|
|
219
222
|
},
|
|
@@ -309,6 +312,9 @@ module.exports = {
|
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
};
|
|
315
|
+
|
|
316
|
+
self.originalChooseParentPage = methods.chooseParentPage;
|
|
317
|
+
return methods;
|
|
312
318
|
},
|
|
313
319
|
extendMethods(self) {
|
|
314
320
|
return {
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
:key="lastSelectionTime"
|
|
21
21
|
:generation="generation"
|
|
22
22
|
:following-values="followingValues()"
|
|
23
|
-
:conditional-fields="conditionalFields
|
|
23
|
+
:conditional-fields="conditionalFields"
|
|
24
|
+
@input="evaluateConditions()"
|
|
24
25
|
/>
|
|
25
26
|
<footer class="apos-image-control__footer">
|
|
26
27
|
<AposButton
|
|
@@ -29,9 +30,11 @@
|
|
|
29
30
|
:modifiers="formModifiers"
|
|
30
31
|
/>
|
|
31
32
|
<AposButton
|
|
32
|
-
type="primary"
|
|
33
|
-
|
|
33
|
+
type="primary"
|
|
34
|
+
label="apostrophe:save"
|
|
34
35
|
:modifiers="formModifiers"
|
|
36
|
+
:disabled="docFields.hasErrors"
|
|
37
|
+
@click="save"
|
|
35
38
|
/>
|
|
36
39
|
</footer>
|
|
37
40
|
</AposContextMenuDialog>
|
|
@@ -112,6 +115,10 @@ export default {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
},
|
|
118
|
+
async mounted() {
|
|
119
|
+
await this.evaluateExternalConditions();
|
|
120
|
+
this.evaluateConditions();
|
|
121
|
+
},
|
|
115
122
|
methods: {
|
|
116
123
|
cancel() {
|
|
117
124
|
this.$emit('cancel');
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
:key="lastSelectionTime"
|
|
43
43
|
:generation="generation"
|
|
44
44
|
:following-values="followingValues()"
|
|
45
|
-
:conditional-fields="conditionalFields
|
|
45
|
+
:conditional-fields="conditionalFields"
|
|
46
|
+
@input="evaluateConditions()"
|
|
46
47
|
/>
|
|
47
48
|
<footer class="apos-anchor-control__footer">
|
|
48
49
|
<AposButton
|
|
@@ -139,6 +140,10 @@ export default {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
},
|
|
143
|
+
async mounted() {
|
|
144
|
+
await this.evaluateExternalConditions();
|
|
145
|
+
this.evaluateConditions();
|
|
146
|
+
},
|
|
142
147
|
methods: {
|
|
143
148
|
removeAnchor() {
|
|
144
149
|
this.docFields.data = {};
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
:key="lastSelectionTime"
|
|
44
44
|
:generation="generation"
|
|
45
45
|
:following-values="followingValues()"
|
|
46
|
-
:conditional-fields="conditionalFields
|
|
46
|
+
:conditional-fields="conditionalFields"
|
|
47
|
+
@input="evaluateConditions()"
|
|
47
48
|
/>
|
|
48
49
|
<footer class="apos-link-control__footer">
|
|
49
50
|
<AposButton
|
|
@@ -52,9 +53,11 @@
|
|
|
52
53
|
:modifiers="formModifiers"
|
|
53
54
|
/>
|
|
54
55
|
<AposButton
|
|
55
|
-
type="primary"
|
|
56
|
-
|
|
56
|
+
type="primary"
|
|
57
|
+
label="apostrophe:save"
|
|
57
58
|
:modifiers="formModifiers"
|
|
59
|
+
:disabled="docFields.hasErrors"
|
|
60
|
+
@click="save"
|
|
58
61
|
/>
|
|
59
62
|
</footer>
|
|
60
63
|
</AposContextMenuDialog>
|
|
@@ -145,6 +148,7 @@ export default {
|
|
|
145
148
|
name: 'href',
|
|
146
149
|
label: this.$t('apostrophe:url'),
|
|
147
150
|
type: 'string',
|
|
151
|
+
required: true,
|
|
148
152
|
if: {
|
|
149
153
|
linkTo: '_url'
|
|
150
154
|
}
|
|
@@ -201,6 +205,10 @@ export default {
|
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
},
|
|
208
|
+
async mounted() {
|
|
209
|
+
await this.evaluateExternalConditions();
|
|
210
|
+
this.evaluateConditions();
|
|
211
|
+
},
|
|
204
212
|
methods: {
|
|
205
213
|
removeLink() {
|
|
206
214
|
this.docFields.data = {};
|
|
@@ -211,6 +219,7 @@ export default {
|
|
|
211
219
|
if (this.hasSelection) {
|
|
212
220
|
this.active = !this.active;
|
|
213
221
|
this.populateFields();
|
|
222
|
+
this.evaluateConditions();
|
|
214
223
|
}
|
|
215
224
|
},
|
|
216
225
|
close() {
|
|
@@ -374,6 +374,25 @@ module.exports = {
|
|
|
374
374
|
// All fields should have an initial value in the database
|
|
375
375
|
instance[field.name] = null;
|
|
376
376
|
}
|
|
377
|
+
// A workaround specifically for areas. They must have a
|
|
378
|
+
// unique `_id` which makes `klona` a poor way to establish
|
|
379
|
+
// a default, and we don't pass functions in schema
|
|
380
|
+
// definitions, but top-level areas should always exist
|
|
381
|
+
// for reasonable results if the output of `newInstance`
|
|
382
|
+
// is saved without further editing on the front end
|
|
383
|
+
if ((field.type === 'area') && (!instance[field.name])) {
|
|
384
|
+
instance[field.name] = {
|
|
385
|
+
metaType: 'area',
|
|
386
|
+
items: [],
|
|
387
|
+
_id: self.apos.util.generateId()
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// A workaround specifically for objects. These too need
|
|
391
|
+
// to have reasonable values in parked pages and any other
|
|
392
|
+
// situation where the data never passes through the UI
|
|
393
|
+
if ((field.type === 'object') && ((!instance[field.name]) || _.isEmpty(instance[field.name]))) {
|
|
394
|
+
instance[field.name] = self.newInstance(field.schema);
|
|
395
|
+
}
|
|
377
396
|
}
|
|
378
397
|
return instance;
|
|
379
398
|
},
|
|
@@ -465,6 +484,78 @@ module.exports = {
|
|
|
465
484
|
});
|
|
466
485
|
},
|
|
467
486
|
|
|
487
|
+
async evaluateCondition(req, field, clause, destination, conditionalFields) {
|
|
488
|
+
for (const [ key, val ] of Object.entries(clause)) {
|
|
489
|
+
const destinationKey = _.get(destination, key);
|
|
490
|
+
|
|
491
|
+
if (key === '$or') {
|
|
492
|
+
const results = await Promise.all(val.map(clause => self.evaluateCondition(req, field, clause, destination, conditionalFields)));
|
|
493
|
+
const testResults = _.isPlainObject(results?.[0])
|
|
494
|
+
? results.some(({ value }) => value)
|
|
495
|
+
: results.some((value) => value);
|
|
496
|
+
if (!testResults) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
continue;
|
|
500
|
+
} else if (val.$ne) {
|
|
501
|
+
// eslint-disable-next-line eqeqeq
|
|
502
|
+
if (val.$ne == destinationKey) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Handle external conditions:
|
|
508
|
+
// - `if: { 'methodName()': true }`
|
|
509
|
+
// - `if: { 'moduleName:methodName()': 'expected value' }`
|
|
510
|
+
// Checking if key ends with a closing parenthesis here to throw later if any argument is passed.
|
|
511
|
+
if (key.endsWith(')')) {
|
|
512
|
+
let externalConditionResult;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
externalConditionResult = await self.evaluateMethod(req, key, field.name, field.moduleName, destination._id);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
throw self.apos.error('invalid', error.message);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (externalConditionResult !== val) {
|
|
521
|
+
return false;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Stop there, this is an external condition thus
|
|
525
|
+
// does not need to be checked against doc fields.
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (val.min && destinationKey < val.min) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
if (val.max && destinationKey > val.max) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (conditionalFields?.[key] === false) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (typeof val === 'boolean' && !destinationKey) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// eslint-disable-next-line eqeqeq
|
|
545
|
+
if ((typeof val === 'string' || typeof val === 'number') && destinationKey != val) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return true;
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
async isFieldRequired(req, field, destination) {
|
|
554
|
+
return field.requiredIf
|
|
555
|
+
? await self.evaluateCondition(req, field, field.requiredIf, destination)
|
|
556
|
+
: field.required;
|
|
557
|
+
},
|
|
558
|
+
|
|
468
559
|
// Convert submitted `data` object according to `schema`, sanitizing it
|
|
469
560
|
// and populating the appropriate properties of `destination` with it.
|
|
470
561
|
//
|
|
@@ -504,7 +595,16 @@ module.exports = {
|
|
|
504
595
|
|
|
505
596
|
if (convert) {
|
|
506
597
|
try {
|
|
507
|
-
await
|
|
598
|
+
const isRequired = await self.isFieldRequired(req, field, destination);
|
|
599
|
+
await convert(
|
|
600
|
+
req,
|
|
601
|
+
{
|
|
602
|
+
...field,
|
|
603
|
+
required: isRequired
|
|
604
|
+
},
|
|
605
|
+
data,
|
|
606
|
+
destination
|
|
607
|
+
);
|
|
508
608
|
} catch (error) {
|
|
509
609
|
if (Array.isArray(error)) {
|
|
510
610
|
const invalid = self.apos.error('invalid', {
|
|
@@ -572,7 +672,7 @@ module.exports = {
|
|
|
572
672
|
for (const field of schema) {
|
|
573
673
|
if (field.if) {
|
|
574
674
|
try {
|
|
575
|
-
const result = await
|
|
675
|
+
const result = await self.evaluateCondition(req, field, field.if, object, conditionalFields);
|
|
576
676
|
const previous = conditionalFields[field.name];
|
|
577
677
|
if (previous !== result) {
|
|
578
678
|
change = true;
|
|
@@ -598,55 +698,6 @@ module.exports = {
|
|
|
598
698
|
} else {
|
|
599
699
|
return true;
|
|
600
700
|
}
|
|
601
|
-
async function evaluate(clause, fieldName, fieldModuleName) {
|
|
602
|
-
let result = true;
|
|
603
|
-
for (const [ key, val ] of Object.entries(clause)) {
|
|
604
|
-
if (key === '$or') {
|
|
605
|
-
const results = await Promise.all(val.map(clause => evaluate(clause, fieldName, fieldModuleName)));
|
|
606
|
-
|
|
607
|
-
if (!results.some(({ value }) => value)) {
|
|
608
|
-
result = false;
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// No need to go further here, the key is an "$or" condition...
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Handle external conditions:
|
|
617
|
-
// - `if: { 'methodName()': true }`
|
|
618
|
-
// - `if: { 'moduleName:methodName()': 'expected value' }`
|
|
619
|
-
// Checking if key ends with a closing parenthesis here to throw later if any argument is passed.
|
|
620
|
-
if (key.endsWith(')')) {
|
|
621
|
-
let externalConditionResult;
|
|
622
|
-
|
|
623
|
-
try {
|
|
624
|
-
externalConditionResult = await self.evaluateMethod(req, key, fieldName, fieldModuleName, object._id);
|
|
625
|
-
} catch (error) {
|
|
626
|
-
throw self.apos.error('invalid', error.message);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (externalConditionResult !== val) {
|
|
630
|
-
result = false;
|
|
631
|
-
break;
|
|
632
|
-
};
|
|
633
|
-
|
|
634
|
-
// Stop there, this is an external condition thus
|
|
635
|
-
// does not need to be checked against doc fields.
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (conditionalFields[key] === false) {
|
|
640
|
-
result = false;
|
|
641
|
-
break;
|
|
642
|
-
}
|
|
643
|
-
if (val !== object[key]) {
|
|
644
|
-
result = false;
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return result;
|
|
649
|
-
}
|
|
650
701
|
},
|
|
651
702
|
|
|
652
703
|
async evaluateMethod(req, methodKey, fieldName, fieldModuleName, docId = null, optionalParenthesis = false) {
|
|
@@ -1246,6 +1297,9 @@ module.exports = {
|
|
|
1246
1297
|
if (field.if && field.if.$or && !Array.isArray(field.if.$or)) {
|
|
1247
1298
|
fail(`$or conditional must be an array of conditions. Current $or configuration: ${JSON.stringify(field.if.$or)}`);
|
|
1248
1299
|
}
|
|
1300
|
+
if (field.requiredIf && field.requiredIf.$or && !Array.isArray(field.requiredIf.$or)) {
|
|
1301
|
+
fail(`$or conditional must be an array of conditions. Current $or configuration: ${JSON.stringify(field.requiredIf.$or)}`);
|
|
1302
|
+
}
|
|
1249
1303
|
if (!field.editPermission && field.permission) {
|
|
1250
1304
|
field.editPermission = field.permission;
|
|
1251
1305
|
}
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
:schema="schema"
|
|
66
66
|
:trigger-validation="triggerValidation"
|
|
67
67
|
:following-values="followingValues()"
|
|
68
|
-
:conditional-fields="conditionalFields
|
|
68
|
+
:conditional-fields="conditionalFields"
|
|
69
69
|
:value="currentDoc"
|
|
70
|
-
@input="currentDocUpdate"
|
|
71
|
-
@validate="triggerValidate"
|
|
72
70
|
:server-errors="currentDocServerErrors"
|
|
73
71
|
ref="schema"
|
|
74
72
|
:doc-id="docId"
|
|
73
|
+
@input="currentDocUpdate"
|
|
74
|
+
@validate="triggerValidate"
|
|
75
75
|
/>
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|