c61c42ce1d
ref 8ea1dfb
ref https://linear.app/tryghost/issue/ONC-111
* undid the reversion for the performance improvements
* built upon new tests for the posts list functionality in admin,
including right click actions
* added tests for pages view in Admin
This was reverted because it broke the Pages list view in Admin, which
is a thin extension of the Posts functionality in admin (route &
controller). That has been fixed and tests added.
This was originally reverted because the changes to improve loading
response times broke right click (bulk) actions in the posts list. This
was not caught because it turned out we had near-zero test coverage of
that part of the codebase. Test coverage has been expanded for the posts
list, and while not comprehensive, is a much better place for us to be
in.
209 lines
5.4 KiB
JavaScript
209 lines
5.4 KiB
JavaScript
import Component from '@glimmer/component';
|
|
import SelectionList from './posts-list/selection-list';
|
|
import {action} from '@ember/object';
|
|
import {inject as service} from '@ember/service';
|
|
import {task} from 'ember-concurrency';
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
export default class GhContextMenu extends Component {
|
|
@service dropdown;
|
|
@service modals;
|
|
|
|
@tracked isOpen = false;
|
|
@tracked left = 0;
|
|
@tracked top = 0;
|
|
@tracked yPlacement = 'bottom';
|
|
@tracked xPlacement = 'right';
|
|
@tracked selectionList = new SelectionList();
|
|
element = null;
|
|
|
|
/**
|
|
* The current state of the context menu
|
|
* @type {'default'|'open'|'modal'|'loading'}
|
|
* default: default state
|
|
* open: menu open
|
|
* modal: modal open
|
|
* loading: performing an action
|
|
*/
|
|
state = 'default';
|
|
|
|
#originalConfirm = null;
|
|
#modal = null;
|
|
|
|
setState(state) {
|
|
switch (state) {
|
|
case this.state:
|
|
return;
|
|
case 'default':
|
|
this.isOpen = false;
|
|
this.#closeModal();
|
|
this.selectionList.unfreeze();
|
|
this.state = state;
|
|
return;
|
|
case 'open':
|
|
if (this.state !== 'default') {
|
|
return;
|
|
}
|
|
this.isOpen = true;
|
|
this.selectionList.freeze();
|
|
this.#closeModal();
|
|
this.state = state;
|
|
return;
|
|
case 'modal':
|
|
if (this.state !== 'open') {
|
|
return;
|
|
}
|
|
this.isOpen = false;
|
|
this.selectionList.freeze();
|
|
this.state = state;
|
|
return;
|
|
case 'loading':
|
|
if (this.state !== 'open' && this.state !== 'modal') {
|
|
return;
|
|
}
|
|
this.isOpen = false;
|
|
this.selectionList.freeze();
|
|
this.state = state;
|
|
return;
|
|
}
|
|
}
|
|
|
|
get name() {
|
|
return this.args.name;
|
|
}
|
|
|
|
get style() {
|
|
return `left: ${this.left}px; top: ${this.top}px;`;
|
|
}
|
|
|
|
get class() {
|
|
return `gh-placement-${this.yPlacement} gh-placement-${this.xPlacement}`;
|
|
}
|
|
|
|
@action
|
|
setup(element) {
|
|
this.element = element;
|
|
const dropdownService = this.dropdown;
|
|
dropdownService.on('close', this, this.close);
|
|
dropdownService.on('toggle', this, this.toggle);
|
|
}
|
|
|
|
willDestroy() {
|
|
super.willDestroy(...arguments);
|
|
this.element = null;
|
|
const dropdownService = this.dropdown;
|
|
dropdownService.off('close', this, this.close);
|
|
dropdownService.off('toggle', this, this.toggle);
|
|
}
|
|
|
|
@action
|
|
open() {
|
|
this.setState('open');
|
|
}
|
|
|
|
@action
|
|
close() {
|
|
if (this.state === 'open') {
|
|
this.setState('default');
|
|
}
|
|
}
|
|
|
|
@action
|
|
onContextMenuOutside(event) {
|
|
this.close();
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
// Called by the dropdown service when the context menu should open
|
|
@action
|
|
toggle(options) {
|
|
const targetDropdownName = options.target;
|
|
if (this.name === targetDropdownName) {
|
|
if (options.left !== undefined) {
|
|
this.left = options.left;
|
|
this.top = options.top;
|
|
}
|
|
if (options.selectionList) {
|
|
this.selectionList = options.selectionList;
|
|
}
|
|
|
|
this.calculatePlacement();
|
|
this.open();
|
|
} else {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
get listElement() {
|
|
return this.element?.firstElementChild?.firstElementChild;
|
|
}
|
|
|
|
calculatePlacement() {
|
|
if (!this.element || !this.listElement) {
|
|
return;
|
|
}
|
|
|
|
const windowHeight = window.innerHeight;
|
|
const windowWidth = window.innerWidth;
|
|
const menuHeight = this.listElement.offsetHeight;
|
|
const menuWidth = this.listElement.offsetWidth;
|
|
const padding = 10;
|
|
|
|
// Do we have enough place to place the menu below?
|
|
if (this.top + menuHeight + padding < windowHeight) {
|
|
this.yPlacement = 'bottom';
|
|
} else {
|
|
this.yPlacement = 'top';
|
|
}
|
|
|
|
// Do we have enough place to place the menu to the right?
|
|
if (this.left + menuWidth + padding < windowWidth) {
|
|
this.xPlacement = 'right';
|
|
} else {
|
|
this.xPlacement = 'left';
|
|
}
|
|
}
|
|
|
|
@action
|
|
stopClicks(event) {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
@task
|
|
*confirmWrapperTask(...args) {
|
|
this.setState('loading');
|
|
let result = yield this.#originalConfirm.perform(...args);
|
|
this.#originalConfirm = null;
|
|
this.setState('default');
|
|
return result;
|
|
}
|
|
|
|
openModal(Modal, data) {
|
|
this.#originalConfirm = data.confirm;
|
|
data.confirm = this.confirmWrapperTask;
|
|
|
|
this.setState('modal');
|
|
|
|
this.#modal = this.modals.open(Modal, data);
|
|
this.#modal.then(() => {
|
|
// We need to delay a little bit for the click event to be processed
|
|
// Since the click event is bubbling back to window, where it will trigger a list deselect
|
|
setTimeout(() => {
|
|
this.setState('default');
|
|
}, 10);
|
|
});
|
|
}
|
|
|
|
#closeModal() {
|
|
this.#modal?.close();
|
|
this.#modal = null;
|
|
}
|
|
|
|
async performTask(taskObj) {
|
|
this.setState('loading');
|
|
await taskObj.perform();
|
|
this.setState('default');
|
|
}
|
|
}
|