UNPKG

apostrophe

Version:
230 lines (216 loc) • 8 kB
/* * 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.' ); } } };