-
Notifications
You must be signed in to change notification settings - Fork 626
Expand file tree
/
Copy pathAposEditorMixin.js
More file actions
229 lines (216 loc) · 7.81 KB
/
AposEditorMixin.js
File metadata and controls
229 lines (216 loc) · 7.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
/*
* Provides:
*
* 1. A scaffold for modeling the doc or doc-like object in the editor,
* in the form of the docFields data attribute
* 2. A scaffold for managing server side errors, in the form of the
* serverErrors data attribute and the handleSaveError method
* 3. A scaffold for handling `following` in field definitions, via
* the `followingValues` method
* 4. A scaffold for handling conditional fields, via the
* `conditionalFields` method
*
* This mixin is designed to accommodate extension by components like
* AposDocEditor that split the UI into several AposSchemas.
*/
import { klona } from 'klona';
import {
evaluateExternalConditions, getConditionalFields, getConditionTypesObject
} from 'Modules/@apostrophecms/schema/lib/conditionalFields.js';
export default {
props: {
parentFollowingValues: {
type: Object,
default: null
}
},
data() {
return {
docFields: {
data: {}
},
serverErrors: null,
restoreOnly: false,
readOnly: false,
changed: [],
externalConditionsResults: getConditionTypesObject(),
conditionalFields: getConditionTypesObject()
};
},
computed: {
schema() {
let schema = (this.moduleOptions.schema || [])
.filter(field => apos.schema.components.fields[field.type]);
if (this.restoreOnly || this.readOnly) {
schema = klona(schema);
for (const field of schema) {
field.readOnly = true;
}
}
// Archive UI is handled via action buttons
schema = schema.filter(field => field.name !== 'archived');
return schema;
},
docMeta() {
return this.docFields.data?.aposMeta || {};
}
},
watch: {
docType: {
// Evaluate external conditions found in current page-type's schema
async handler() {
if (this.moduleName === '@apostrophecms/page') {
await this.evaluateExternalConditions();
this.evaluateConditions();
}
}
}
},
methods: {
// Evaluate the external conditions found in each field
// via API calls -made in parallel for performance-
// and store their result for reusability.
async evaluateExternalConditions() {
this.externalConditionsResults = await evaluateExternalConditions(
this.schema,
this.docId || this.docFields?.data?._docId || this.docFields?.data?._id,
this.$t
);
},
// followedByCategory may be falsy (all fields), "other" or "utility". The
// returned object contains properties named for each field in that category
// that follows other fields. For instance if followedBy is "utility" then
// in our default configuration `followingValues` will be:
//
// `{ slug: { title: 'latest title here' } }`
followingValues(followedByCategory, parentOnly = false) {
const fields = this.getFieldsByCategory(followedByCategory);
const followingValues = {};
const parentFollowing = {};
for (const [ key, val ] of Object.entries(this.parentFollowingValues || {})) {
parentFollowing[`<${key}`] = val;
}
if (parentOnly) {
// If we are only interested in the parent following values, return them
return parentFollowing;
}
for (const field of fields) {
if (field.following) {
const following = Array.isArray(field.following)
? field.following
: [ field.following ];
followingValues[field.name] = {};
for (const name of following) {
if (name.startsWith('<')) {
followingValues[field.name][name] = parentFollowing[name];
} else {
followingValues[field.name][name] = this.getFieldValue(name);
}
}
}
}
return followingValues;
},
// Fetch the subset of the schema in the given category, either
// 'utility' or 'other', or the entire schema if followedByCategory
// is falsy
getFieldsByCategory(followedByCategory) {
if (followedByCategory && this.utilityFields) {
return (followedByCategory === 'other')
? this.schema.filter(field => !this.utilityFields.includes(field.name))
: this.schema.filter(field => this.utilityFields.includes(field.name));
} else {
return this.schema;
}
},
// The returned object contains a property for each field that is
// conditional on other fields, `true` if that field's conditions are
// satisfied and `false` if they are not. There will be no properties for
// fields that are not conditional.
//
// Any condition on a field that is itself conditional fails if the second
// field's conditions fail.
//
// If present, followedByCategory must be either "other" or "utility", and
// the returned object will contain properties only for conditional fields
// in that category, although they may be conditional upon fields in either
// category.
getConditionalFields(followedByCategory) {
const values = {
// Append the parent following values without the current doc
// values, so that the parent can be used in conditions
...this.followingValues(followedByCategory, true),
// currentDoc for arrays, docFields for all other editors
...(this.currentDoc ? this.currentDoc.data : this.docFields.data)
};
return getConditionalFields(
this.getFieldsByCategory(followedByCategory),
values,
this.externalConditionsResults
);
},
evaluateConditions() {
this.conditionalFields = this.getConditionalFields();
},
// Overridden by components that split the fields into several AposSchemas
getFieldValue(name) {
return this.docFields.data[name];
},
// Simple parents only have one AposSchema object.
// Complex parents like ApocDocEditor can override
// to return the appropriate ref
getAposSchema(field) {
return this.$refs.schema;
},
// Handle a server-side save error, attaching it to the right field
// in the schema. fallback is a fallback error message, if none is provided
// by the server.
async handleSaveError(e, { fallback }) {
// eslint-disable-next-line no-console
console.error(e);
if (e.body?.data?.errors) {
const serverErrors = {};
let first;
e.body.data.errors.forEach(e => {
first = first || e;
serverErrors[e.path] = e;
});
this.serverErrors = serverErrors;
if (first) {
const field = this.schema.find(field => field.name === first.path);
if (field) {
if ((field.group.name !== 'utility') && (this.switchPane)) {
this.switchPane(field.group.name);
}
// Let pane switching effects settle first
this.$nextTick(() => {
this.getAposSchema(field).scrollFieldIntoView(field.name);
});
}
}
} else {
// As per the new standard, any message in `data.detail` is considered
// a human readable error message. If it is not present, we fall back to
// the message in `body.message` or the fallback.
const bodyMessage = e.body?.data?.detail || e.body?.message;
await apos.notify(bodyMessage || fallback, {
type: 'danger',
icon: 'alert-circle-icon',
dismiss: true
});
}
},
triggerValidate() {
this.triggerValidation = true;
this.$nextTick(() => {
this.triggerValidation = false;
});
},
async postprocess() {
// eslint-disable-next-line no-console
console.warn(
'The function postprocess from AposEditorMixin does not do anything anymore.\nRelationship postprocessing is made at input level in AposInputRelationship and in some cases globally like in AposImageWidget.'
);
}
}
};