854f616f70
refs https://github.com/TryGhost/Team/issues/2677 - This extends the bulk editing UI to pages. - New endpoints for editing pages in bulk - Support for type in bulk edit UI - Fixed empty messages for lists - Minor bugfixes (e.g. save button when adding tags became red because task didn't return true) --- This pull request adds support for bulk editing and deleting of pages in the admin UI and the API. It refactors the context menu component and the list templates to handle different types of content (posts or pages) dynamically. It also updates the selection list utility and the no posts box component to work with the new feature. It modifies the `posts.js` and `pages.js` API files and the corresponding input and output serializers and routes.
242 lines
6.4 KiB
JavaScript
242 lines
6.4 KiB
JavaScript
import {tracked} from '@glimmer/tracking';
|
|
|
|
export default class SelectionList {
|
|
@tracked selectedIds = new Set();
|
|
@tracked inverted = false;
|
|
@tracked lastSelectedId = null;
|
|
@tracked lastShiftSelectionGroup = new Set();
|
|
|
|
enabled = true;
|
|
|
|
infinityModel;
|
|
|
|
#frozen = false;
|
|
|
|
/**
|
|
* When doing right click on an item, we temporarily select it, but want to clear it as soon as we close the context menu.
|
|
*/
|
|
#clearOnNextUnfreeze = false;
|
|
|
|
constructor(infinityModel) {
|
|
this.infinityModel = infinityModel ?? {content: []};
|
|
}
|
|
|
|
freeze() {
|
|
this.#frozen = true;
|
|
}
|
|
|
|
unfreeze() {
|
|
this.#frozen = false;
|
|
if (this.#clearOnNextUnfreeze) {
|
|
this.clearSelection();
|
|
this.#clearOnNextUnfreeze = false;
|
|
}
|
|
}
|
|
|
|
clearOnNextUnfreeze() {
|
|
this.#clearOnNextUnfreeze = true;
|
|
}
|
|
|
|
/**
|
|
* Returns an NQL filter for all items, not the selection
|
|
*/
|
|
get allFilter() {
|
|
return this.infinityModel.extraParams?.filter ?? '';
|
|
}
|
|
|
|
/**
|
|
* Returns an NQL filter for the current selection
|
|
*/
|
|
get filter() {
|
|
if (this.inverted) {
|
|
if (this.allFilter) {
|
|
if (this.selectedIds.size === 0) {
|
|
return this.allFilter;
|
|
}
|
|
return `(${this.allFilter})+id:-['${[...this.selectedIds].join('\',\'')}']`;
|
|
}
|
|
if (this.selectedIds.size === 0) {
|
|
// Select all
|
|
return '';
|
|
}
|
|
return `id:-['${[...this.selectedIds].join('\',\'')}']`;
|
|
}
|
|
if (this.selectedIds.size === 0) {
|
|
// Select nothing
|
|
return 'id:nothing';
|
|
}
|
|
// Only based on the ids
|
|
return `id:['${[...this.selectedIds].join('\',\'')}']`;
|
|
}
|
|
|
|
/**
|
|
* Create an empty copy
|
|
*/
|
|
cloneEmpty() {
|
|
return new SelectionList(this.infinityModel);
|
|
}
|
|
|
|
/**
|
|
* Return a list of models that are already loaded in memory.
|
|
* Keep in mind that when using CMD + A, we don't have all items in memory!
|
|
*/
|
|
get availableModels() {
|
|
const arr = [];
|
|
for (const item of this.infinityModel.content) {
|
|
if (this.isSelected(item.id)) {
|
|
arr.push(item);
|
|
}
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
get first() {
|
|
return this.availableModels[0];
|
|
}
|
|
|
|
get isSingle() {
|
|
return this.selectedIds.size === 1 && !this.inverted;
|
|
}
|
|
|
|
get count() {
|
|
if (!this.inverted) {
|
|
return this.selectedIds.size;
|
|
}
|
|
return Math.max((this.infinityModel.meta?.pagination?.total ?? 0) - this.selectedIds.size, 1);
|
|
}
|
|
|
|
isSelected(id) {
|
|
if (this.inverted) {
|
|
return !this.selectedIds.has(id);
|
|
}
|
|
return this.selectedIds.has(id);
|
|
}
|
|
|
|
toggleItem(id) {
|
|
if (this.#frozen) {
|
|
return;
|
|
}
|
|
this.lastShiftSelectionGroup = new Set();
|
|
|
|
if (this.selectedIds.has(id)) {
|
|
this.selectedIds.delete(id);
|
|
|
|
if (!this.inverted) {
|
|
if (this.lastSelectedId === id) {
|
|
this.lastSelectedId = null;
|
|
}
|
|
} else {
|
|
// Shift behaviour in inverted mode needs a review
|
|
this.lastSelectedId = id;
|
|
}
|
|
} else {
|
|
this.selectedIds.add(id);
|
|
|
|
if (!this.inverted) {
|
|
this.lastSelectedId = id;
|
|
} else {
|
|
// Shift behaviour in inverted mode needs a review
|
|
this.lastSelectedId = id;
|
|
}
|
|
}
|
|
|
|
// Force update
|
|
// eslint-disable-next-line no-self-assign
|
|
this.selectedIds = this.selectedIds;
|
|
}
|
|
|
|
clearUnavailableItems() {
|
|
const newSelection = new Set();
|
|
for (const item of this.infinityModel.content) {
|
|
if (this.selectedIds.has(item.id)) {
|
|
newSelection.add(item.id);
|
|
}
|
|
}
|
|
this.selectedIds = newSelection;
|
|
}
|
|
|
|
/**
|
|
* Select all items between the last selection or the first one if none
|
|
*/
|
|
shiftItem(id) {
|
|
if (this.#frozen) {
|
|
return;
|
|
}
|
|
if (this.lastSelectedId === null) {
|
|
// Do a normal toggle
|
|
this.toggleItem(id);
|
|
return;
|
|
}
|
|
|
|
// Unselect last selected items
|
|
for (const item of this.lastShiftSelectionGroup) {
|
|
if (this.inverted) {
|
|
this.selectedIds.add(item);
|
|
} else {
|
|
this.selectedIds.delete(item);
|
|
}
|
|
}
|
|
this.lastShiftSelectionGroup = new Set();
|
|
|
|
// todo
|
|
let running = false;
|
|
|
|
for (const item of this.infinityModel.content) {
|
|
// Exlusing the last selected item
|
|
if (item.id === this.lastSelectedId || item.id === id) {
|
|
if (!running) {
|
|
running = true;
|
|
|
|
// Skip last selected on its own
|
|
if (item.id === this.lastSelectedId) {
|
|
continue;
|
|
}
|
|
} else {
|
|
// Still include id
|
|
if (item.id === id) {
|
|
this.lastShiftSelectionGroup.add(item.id);
|
|
|
|
if (this.inverted) {
|
|
this.selectedIds.delete(item.id);
|
|
} else {
|
|
this.selectedIds.add(item.id);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (running) {
|
|
this.lastShiftSelectionGroup.add(item.id);
|
|
if (this.inverted) {
|
|
this.selectedIds.delete(item.id);
|
|
} else {
|
|
this.selectedIds.add(item.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Force update
|
|
// eslint-disable-next-line no-self-assign
|
|
this.selectedIds = this.selectedIds;
|
|
}
|
|
|
|
selectAll() {
|
|
if (this.#frozen) {
|
|
return;
|
|
}
|
|
this.selectedIds = new Set();
|
|
this.inverted = !this.inverted;
|
|
this.lastSelectedId = null;
|
|
}
|
|
|
|
clearSelection(options = {}) {
|
|
if (this.#frozen && !options.force) {
|
|
return;
|
|
}
|
|
this.selectedIds = new Set();
|
|
this.inverted = false;
|
|
this.lastSelectedId = null;
|
|
}
|
|
}
|