apostrophe
Version:
The Apostrophe Content Management System.
340 lines (322 loc) • 8.59 kB
JavaScript
// HAS to be module import, because it's universal CJS code.
import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';
import { getConditionTypesObject } from '../lib/conditionalFields';
export default {
name: 'AposSchema',
props: {
modelValue: {
type: Object,
required: true,
default() {
return {
data: {}
};
}
},
meta: {
type: Object,
default() {
return {};
}
},
generation: {
type: Number,
required: false,
default() {
return null;
}
},
schema: {
type: Array,
required: true
},
fieldStyle: {
type: String,
required: false,
default: ''
},
currentFields: {
type: Array,
default() {
return null;
}
},
followingValues: {
type: Object,
default() {
return {};
}
},
conditionalFields: {
type: Object,
default() {
return getConditionTypesObject();
}
},
// Modifiers applied to all fields
modifiers: {
type: Array,
default() {
return [];
}
},
// Modifiers applied to specified field types
fieldModifiers: {
type: Object,
default: () => ({})
},
triggerValidation: Boolean,
utilityRail: {
type: Boolean,
default() {
return false;
}
},
docId: {
type: String,
default() {
return null;
}
},
serverErrors: {
type: Object,
default() {
return null;
}
},
displayOptions: {
type: Object,
default() {
return {};
}
},
changed: {
type: Array,
default() {
return [];
}
}
},
emits: [
'update:model-value',
'reset',
'validate',
'update-doc-data'
],
data() {
return {
schemaReady: false,
next: {
hasErrors: false,
data: {},
fieldErrors: {}
},
fieldState: {},
fieldComponentMap: window.apos.schema.components.fields || {},
changedFields: new Set()
};
},
computed: {
fields() {
return this.schema.reduce((acc, item) => {
const { requiredIf } = this.conditionalFields;
const required = Object.hasOwn(requiredIf, item.name)
? requiredIf[item.name]
: item.required;
return {
...acc,
[item.name]: {
field: {
...item,
required
},
serverError: this.serverErrors && this.serverErrors[item.name],
modifiers: this.computeModifiers(item)
}
};
}, {});
},
hasCompareMeta() {
return this.schema.some(field => this.meta[field.name]?.['@apostrophecms/schema:compare']);
},
classes() {
const classes = [];
if (this.hasCompareMeta) {
classes.push('apos-schema--compare');
}
return classes;
},
compareMetaState() {
if (!this.hasCompareMeta) {
return {};
}
const compareMetaState = {};
this.schema.forEach(field => {
compareMetaState[field.name] = {
error: false,
data: this.meta[field.name]?.['@apostrophecms/schema:compare']
};
});
return compareMetaState;
}
},
watch: {
fieldState: {
deep: 2,
handler() {
this.updateNextAndEmit();
},
flush: 'post'
},
schema() {
this.populateDocData();
},
'modelValue.data._id'(_id) {
// The doc might be swapped out completely in cases such as the media
// library editor. Repopulate the fields if that happens.
if (
// If the fieldState had been cleared and there's new populated data
(!this.fieldState._id && _id) ||
// or if there *is* active fieldState, but the new data is a new doc
(this.fieldState._id && _id !== this.fieldState._id.data)
) {
// repopulate the schema.
this.populateDocData();
}
},
generation() {
// repopulate the schema.
this.populateDocData();
},
conditionalFields: {
handler(newVal, oldVal) {
for (const [ conditionType, conditions ] of Object.entries(oldVal)) {
for (const [ field, value ] of Object.entries(conditions)) {
if (
(value === newVal[conditionType][field]) ||
!this.fieldState[field] ||
!this.fieldState[field].ranValidation
) {
continue;
}
this.emitValidate();
return;
}
}
}
}
},
created() {
this.populateDocData();
},
methods: {
emitValidate() {
this.$emit('validate');
},
getDisplayOptions(fieldName) {
let options = {};
if (this.displayOptions) {
options = { ...this.displayOptions };
}
if (this.changed && this.changed.includes(fieldName)) {
options.changed = true;
}
return options;
},
populateDocData() {
this.schemaReady = false;
const next = {
hasErrors: false,
data: {}
};
const fieldState = {};
const instance = newInstance(this.schema);
// Though not in the schema, keep track of the _id field.
if (this.modelValue.data._id) {
next.data._id = this.modelValue.data._id;
fieldState._id = { data: this.modelValue.data._id };
}
// Though not *always* in the schema, keep track of the archived status.
if (this.modelValue.data.archived !== undefined) {
next.data.archived = this.modelValue.data.archived;
fieldState.archived = { data: this.modelValue.data.archived };
}
this.schema.forEach(field => {
const value = this.modelValue.data[field.name];
fieldState[field.name] = {
error: false,
data: (value === undefined) ? instance[field.name] : value
};
next.data[field.name] = fieldState[field.name].data;
});
this.next = next;
this.fieldState = fieldState;
// Wait until the next tick so the parent editor component is done
// updating. This is only really a concern in editors that can swap
// the active doc/object without unmounting AposSchema.
this.$nextTick(() => {
this.schemaReady = true;
// Signal that the schema data is ready to be tracked.
this.$emit('reset');
});
},
async updateNextAndEmit() {
if (!this.schemaReady) {
return;
}
this.next.hasErrors = false;
this.schema
.filter(field => this.displayComponent(field))
.forEach(field => {
if (this.fieldState[field.name].error) {
this.next.hasErrors = true;
}
this.next.data[field.name] = this.fieldState[field.name].data;
});
this.next.fieldState = { ...this.fieldState };
this.$emit('update:model-value', {
...this.next,
changed: [ ...this.changedFields ]
});
this.changedFields = new Set();
},
handleFieldUpdate(name, val) {
this.fieldState[name] = val;
this.changedFields.add(name);
},
displayComponent({ name, hidden = false }) {
if (hidden === true) {
return false;
}
if (this.currentFields && !this.currentFields.includes(name)) {
return false;
}
// Might not be a conditional field at all, so test explicitly for false
if (this.conditionalFields.if[name] === false) {
return false;
}
return true;
},
scrollFieldIntoView(fieldName) {
// The refs for a name are an array if that ref was assigned
// in a v-for. We know there is only one in this case
// https://forum.vuejs.org/t/this-refs-theid-returns-an-array/31995/9
if (this.$refs[fieldName]?.[0]?.$el?.scrollIntoView) {
this.$refs[fieldName][0].$el.scrollIntoView();
}
},
onUpdateDocData(data) {
this.$emit('update-doc-data', data);
},
highlight(fieldName) {
return this.meta[fieldName]?.['@apostrophecms/schema:highlight'];
},
generateItemUniqueKey(field) {
return `${field.name}:${field._id ?? ''}:${this.modelValue?.data?._id ?? ''}`;
},
computeModifiers(field) {
const fieldModifiers = this.fieldModifiers[field.type] || this.modifiers;
return [ ...new Set(fieldModifiers.concat(field.modifiers || [])) ];
}
}
};