apostrophe
Version:
The Apostrophe Content Management System.
312 lines (297 loc) • 9.9 kB
JavaScript
import { detectDocChange } from 'Modules/@apostrophecms/schema/lib/detectChange';
// TODO: Reconcile the overlap in this mixin between the pages and pieces
// managers. Does it need to be a mixin? This may be resolved when switching to
// Vue 3 using the composition API. - AB
import { klona } from 'klona';
export default {
data() {
return {
icons: {},
// If passing in chosen items from the relationship input, use those
// as initially checked.
checked: Array.isArray(this.chosen) ? this.chosen.map(item => item._id) : [],
checkedDocs: Array.isArray(this.chosen) ? klona(this.chosen) : [],
// Remember relationship subfield values even if a document
// is temporarily deselected, easing the user's pain if they
// inadvertently deselect something for a moment
subfields: Object.fromEntries((this.chosen || [])
.filter(doc => doc._fields)
.map(doc => [ doc._id, doc._fields ])
),
selectPending: new Set()
};
},
props: {
chosen: {
type: [ Array, Boolean ],
default: false
},
relationshipField: {
type: [ Object, Boolean ],
default: false
}
},
emits: [ 'modal-result', 'sort' ],
computed: {
relationshipErrors() {
if (!this.relationshipField) {
return false;
}
if (this.relationshipField.required && !this.checked.length) {
// Treated as min for consistency with AposMinMaxCount
return 'min';
}
if (
this.relationshipField.min &&
this.checked.length < this.relationshipField.min
) {
return 'min';
}
if (
this.relationshipField.max &&
this.checked.length > this.relationshipField.max
) {
return 'max';
}
return false;
},
sort(action) {
this.$emit('sort', action);
},
selectAllChoice() {
const checkCount = this.checked.length;
const itemCount = (this.items && this.items.length) || 0;
return {
value: 'checked',
indeterminate: checkCount > 0 && checkCount !== itemCount
};
},
selectAllState() {
if (this.checked.length && !this.selectAllChoice.indeterminate) {
return 'checked';
}
if (this.checked.length && this.selectAllChoice.indeterminate) {
return 'indeterminate';
}
return 'empty';
},
// Default implementation of isModified is based on whether the
// selection has changed, but you can override this and combine
// that bit with your own if your manager allows in-context editing
// of a piece (i.e. AposMediaManager)
isModified() {
return this.relationshipIsModified();
},
manuallyPublished() {
return this.moduleOptions.localized && !this.moduleOptions.autopublish;
},
showLocalePicker() {
return Object.keys(window.apos.i18n.locales).length > 1 &&
this.moduleOptions.localized !== false &&
!this.modalData.hasContextLocale;
}
},
mounted() {
this.docsManagerAddEventHandlers();
},
unmounted() {
this.docsManagerRemoveEventHandlers();
},
watch: {
items: function(newValue) {
if (newValue.length) {
this.generateUi();
}
if (this.selectPending.size > 0) {
const newChecked = [ ...this.checked, ...this.selectPending ];
this.checked = [ ...new Set(newChecked) ];
this.selectPending = new Set();
}
},
checkedDocs(after, before) {
for (const doc of before) {
this.subfields[doc._id] = doc._fields;
}
for (const doc of after) {
if (this.subfields[doc._id] && !Object.keys(doc._fields || {}).length) {
doc._fields = this.subfields[doc._id];
}
}
},
checked() {
this.updateCheckedDocs();
}
},
methods: {
// It would have been nice for this to be computed, however
// AposMediaManagerDisplay does not re-render when it is
// a computed prop rather than a method call in the template.
maxReached(checkedCount = this.checked.length) {
// Reaching max and exceeding it are different things
return this.relationshipField.max && checkedCount >= this.relationshipField.max;
},
selectAll() {
if (!this.checked.length) {
const ids = [];
this.items.forEach((item) => {
const notPublished = this.manuallyPublished && !item.lastPublishedAt;
if (this.relationshipField && (this.maxReached(ids.length) || notPublished)) {
return;
}
ids.push(item._id);
});
this.checked = ids;
return;
}
if (this.allPiecesSelection) {
this.allPiecesSelection.isSelected = false;
}
this.checked = [];
},
iconSize(header) {
if (header.icon) {
if (header.icon === 'Circle') {
return 8;
} else {
return 10;
}
}
},
generateUi () {
this.generateIcons();
},
generateIcons () {
// fetch all icons used in the table
const icons = {};
const temp = [];
const customValues = [ 'true', 'false' ];
this.headers.forEach(h => {
if (h.cellValue && h.cellValue.icon) {
temp.push(h.cellValue.icon);
}
if (h.columnHeaderIcon) {
temp.push(h.columnHeaderIcon);
}
customValues.forEach(val => {
if (h.cellValue && h.cellValue[val] && h.cellValue[val].icon) {
temp.push(h.cellValue[val].icon);
}
});
// Include only unique in final object
Array.from(new Set(temp)).forEach(icon => {
icons[icon] = `${icon.toLowerCase()}-icon`;
});
});
this.icons = icons;
// prep item checkbox fields
},
saveRelationship() {
this.$emit('modal-result', this.checkedDocs);
this.modal.showModal = false;
},
// Easy to reuse if you have a custom isModified method
relationshipIsModified() {
if (!this.relationshipField) {
return false;
}
if (this.chosen.length !== this.checkedDocs.length) {
return true;
}
for (let i = 0; (i < this.chosen.length); i++) {
if (this.chosen[i]._id !== this.checkedDocs[i]._id) {
return true;
}
if (this.relationshipField.schema) {
if (
detectDocChange(
this.relationshipField.schema,
this.chosen[i]._fields,
this.checkedDocs[i]._fields
)
) {
return true;
}
}
}
},
docsManagerAddEventHandlers() {
apos.bus.$on('content-changed', this.docsManagerOnContentChanged);
},
docsManagerRemoveEventHandlers() {
apos.bus.$off('content-changed', this.docsManagerOnContentChanged);
},
docsManagerOnContentChanged({
doc, select, action
}) {
if (!select) {
return;
}
if ((doc.type === this.moduleName) || (doc.slug.startsWith('/') && (this.moduleName === '@apostrophecms/page'))) {
// For now we add it to a set of ids to be selected on the next refresh
// of the items array, which works well for newly inserted documents.
// In the future this will be replaced with logic that can deal with
// selecting any document, but we should implement the "Really Select
// All, Not Just This Page" action first.
//
// We know we always work with drafts in the media manager, so let's
// adapt a published doc _id without a fuss
this.selectPending.add(doc._id.replace(':published', ':draft'));
}
if (action === 'archive') {
this.checked = this.checked.filter(checkedId => doc._id !== checkedId);
}
},
// update this.checkedDocs based on this.checked. The default
// implementation is suitable for paginated lists. Can be overridden
// for other cases.
updateCheckedDocs() {
// Keep `checkedDocs` in sync with `checked`, first removing from
// `checkedDocs` if no longer in `checked`
this.checkedDocs = this.checkedDocs.filter(doc => {
return this.checked.includes(doc._id);
});
// then adding to `checkedDocs` if not there yet. These should be in
// `items` which is assumed to contain a flat list of items currently
// visible.
//
// TODO: Once we have the option to select all docs of a type even if not
// currently visible in the manager this will need to make calls to the
// database.
this.checked.forEach(id => {
if (this.checkedDocs.findIndex(doc => doc._id === id) === -1) {
const found = this.items.find(item => item._id === id);
found && this.checkedDocs.push(found);
}
});
if (this.allPiecesSelection) {
const totalChecked = this.checked.length === this.allPiecesSelection.total;
this.allPiecesSelection.isSelected = totalChecked ||
(this.checked.length && this.maxReached());
}
},
getDocModuleName(doc) {
// Don't assume the piece has the type of the module,
// this could be a virtual piece type such as "submitted-draft"
// that manages docs of many types
if (doc) {
return doc.slug.startsWith('/') ? '@apostrophecms/page' : doc.type;
}
return this.moduleName;
},
getContentChangedTypes(doc, types) {
const submitDraftType = '@apostrophecms/submitted-draft';
if (doc) {
const moduleName = this.getDocModuleName(doc);
return [
moduleName,
submitDraftType,
...moduleName !== doc.type ? [ doc.type ] : []
];
}
if (!types) {
return [ this.moduleName, submitDraftType ];
}
return [ ...types, submitDraftType ];
}
}
};