UNPKG

apostrophe

Version:
368 lines (357 loc) • 10.6 kB
import AposModifiedMixin from 'Modules/@apostrophecms/ui/mixins/AposModifiedMixin'; import AposEditorMixin from 'Modules/@apostrophecms/modal/mixins/AposEditorMixin'; import { createId } from '@paralleldrive/cuid2'; import { klona } from 'klona'; import { get } from 'lodash'; import { detectDocChange } from 'Modules/@apostrophecms/schema/lib/detectChange'; import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js'; export default { name: 'AposArrayEditor', mixins: [ AposModifiedMixin, AposEditorMixin ], props: { items: { required: true, type: Array }, meta: { type: Object, default: () => ({}) }, field: { required: true, type: Object }, inputSchema: { type: Array, required: true }, serverError: { type: Object, default: null }, docId: { type: String, default: null } }, emits: [ 'modal-result' ], data() { // Automatically add `_id` to default items const items = this.items.map(item => ({ ...item, _id: item._id || createId() })); return { currentId: null, currentDoc: { data: {} }, modal: { active: false, type: 'overlay', showModal: false, triggerFocusRefresh: 0 }, modalTitle: { key: 'apostrophe:editType', type: this.$t(this.field.label) }, titleFieldChoices: null, // If we don't clone, then we're making // permanent modifications whether the user // clicks save or not next: klona(items), original: klona(items), triggerValidation: false, minError: false, maxError: false, cancelDescription: 'apostrophe:arrayCancelDescription' }; }, computed: { moduleOptions() { return window.apos.schema || {}; }, itemError() { return this.currentDoc && this.currentDoc.hasErrors; }, valid() { return !(this.minError || this.maxError || this.itemError); }, maxed() { return (this.field.max !== undefined) && (this.next.length >= this.field.max); }, schema() { // For AposDocEditorMixin return (this.inputSchema || this.field.schema || []) .filter(field => apos.schema.components.fields[field.type]); }, countLabel() { return this.$t('apostrophe:numberAdded', { count: this.next.length }); }, // Here in the array editor we use effectiveMin to factor in the // required property because there is no other good place to do that, // unlike the input field wrapper which has a separate visual // representation of "required". minLabel() { if (this.effectiveMin) { return this.$t('apostrophe:minUi', { number: this.effectiveMin }); } else { return false; } }, maxLabel() { if ((typeof this.field.max) === 'number') { return this.$t('apostrophe:maxUi', { number: this.field.max }); } else { return false; } }, effectiveMin() { if (this.field.min) { return this.field.min; } else if (this.field.required) { return 1; } else { return 0; } }, currentDocServerErrors() { let serverErrors = null; ((this.serverError && this.serverError.data && this.serverError.data.errors) || []) .forEach(error => { const [ _id, fieldName ] = error.path.split('.'); if (_id === this.currentId) { serverErrors = serverErrors || {}; serverErrors[fieldName] = error; } }); return serverErrors; }, currentDocMeta() { return this.meta[this.currentId]?.aposMeta || {}; }, isModified() { if (this.currentId) { const currentIndex = this.next.findIndex(item => item._id === this.currentId); if (detectDocChange(this.schema, this.next[currentIndex], this.currentDoc.data)) { return true; } } if (this.next.length !== this.original.length) { return true; } for (let i = 0; (i < this.next.length); i++) { if (this.next[i]._id !== this.original[i]._id) { return true; } if (detectDocChange(this.schema, this.next[i], this.original[i])) { return true; } } return false; } }, async mounted() { this.modal.active = true; await this.evaluateExternalConditions(); if (this.next.length) { this.setCurrentDoc(this.next.at(0)._id); } if (this.serverError?.data?.errors?.length) { const first = this.serverError.data.errors[0]; const [ _id, name ] = first.path?.split('.') || []; if (_id) { await this.select(_id); const aposSchema = this.$refs.schema; await this.$nextTick(); name && aposSchema.scrollFieldIntoView(name); } } this.titleFieldChoices = await this.getTitleFieldChoices(); this.modal.triggerFocusRefresh++; }, methods: { setCurrentDoc(_id) { this.currentId = _id; this.currentDoc = { hasErrors: false, data: this.next.find(item => item._id === _id) }; this.evaluateConditions(); this.triggerValidation = false; }, async select(_id) { if (this.currentId === _id) { return; } if (await this.validate(true, false)) { this.currentDocToCurrentItem(); this.setCurrentDoc(_id); } }, update(items) { // Take care to use the same items in order to avoid // losing too much state inside draggable, otherwise // drags fail this.next = items.map(item => this.next.find(_item => item._id === _item._id)); if (this.currentId) { if (!this.next.some(item => item._id === this.currentId)) { this.currentId = this.next.at(0)?._id || null; this.currentDoc = { data: this.next.at(0) || {} }; } } this.updateMinMax(); }, currentDocUpdate(currentDoc) { this.currentDoc = currentDoc; this.evaluateConditions(); }, async add() { if (await this.validate(true, false)) { const item = this.newInstance(); item._id = createId(); this.next.push(item); await this.select(item._id); this.updateMinMax(); this.modal.triggerFocusRefresh++; } }, updateMinMax() { let minError = false; let maxError = false; if (this.effectiveMin) { if (this.next.length < this.effectiveMin) { minError = true; } } if (this.field.max !== undefined) { if (this.next.length > this.field.max) { maxError = true; } } this.minError = minError; this.maxError = maxError; }, async submit() { if (await this.validate(true, true)) { this.currentDocToCurrentItem(); for (const item of this.next) { item.metaType = 'arrayItem'; item.scopedArrayName = this.field.scopedArrayName; } this.$emit('modal-result', this.next); this.modal.showModal = false; } }, currentDocToCurrentItem() { if (!this.currentId) { return; } const currentIndex = this.next.findIndex(item => item._id === this.currentId); this.next[currentIndex] = this.currentDoc.data; }, getFieldValue(name) { return this.currentDoc.data[name]; }, async validate(validateItem, validateLength) { if (validateItem && this.next.length > 0 && this.currentId) { this.triggerValidation = true; } await this.$nextTick(); if (validateLength) { this.updateMinMax(); } if ( (validateLength && (this.minError || this.maxError)) || (validateItem && (this.currentDoc && this.currentDoc.hasErrors)) ) { await apos.notify('apostrophe:resolveErrorsFirst', { type: 'warning', icon: 'alert-circle-icon', dismiss: true }); return false; } else { return true; } }, newInstance() { return newInstance(this.schema); }, label(item) { let candidate; if (this.field.titleField) { // Initial field value candidate = get(item, this.field.titleField); // If the titleField references a select input, use the // select label as the slat label, rather than the value. if (this.titleFieldChoices) { const choice = this.titleFieldChoices .find(choice => choice.value === candidate); if (choice && choice.label) { candidate = choice.label; } } } else if (this.schema.find(field => field.name === 'title') && (item.title !== undefined)) { candidate = item.title; } if ((candidate == null) || candidate === '') { for (let i = 0; (i < this.next.length); i++) { if (this.next[i]._id === item._id) { candidate = `#${i + 1}`; break; } } } return candidate; }, withLabels(items) { const result = items.map(item => ({ ...item, title: this.label(item) })); return result; }, async getTitleFieldChoices() { // If the titleField references a select input, get it's choices // to use as labels for the slat UI let choices = null; const titleField = this.schema.find(field => field.name === this.field.titleField); // The titleField is a select if (titleField?.choices) { // Choices are provided by a method if (typeof titleField.choices === 'string') { const action = `${this.moduleOptions.action}/choices`; try { const result = await apos.http.get( action, { qs: { fieldId: titleField._id } } ); if (result && result.choices) { choices = result.choices; } } catch (e) { // eslint-disable-next-line no-console console.error(this.$t('apostrophe:errorFetchingTitleFieldChoicesByMethod', { name: titleField.name })); } // Choices are a normal, hardcoded array } else if (Array.isArray(titleField.choices)) { choices = titleField.choices; } } return choices; } } };