287 lines
7.8 KiB
TypeScript
287 lines
7.8 KiB
TypeScript
import { WebApplication } from '@/ui_models/application';
|
|
import { WebDirective } from './../../types';
|
|
import template from '%/directives/actions-menu.pug';
|
|
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
|
import { SNItem, Action, SNActionsExtension, UuidString, CopyPayload } from '@standardnotes/snjs';
|
|
import { ActionResponse } from '@standardnotes/snjs';
|
|
import { ActionsExtensionMutator } from '@standardnotes/snjs';
|
|
|
|
type ActionsMenuScope = {
|
|
application: WebApplication
|
|
item: SNItem
|
|
}
|
|
|
|
type ActionSubRow = {
|
|
onClick: () => void
|
|
label: string
|
|
subtitle: string
|
|
spinnerClass?: string
|
|
}
|
|
|
|
type ExtensionState = {
|
|
loading: boolean
|
|
error: boolean
|
|
}
|
|
|
|
type ActionsMenuState = {
|
|
extensions: SNActionsExtension[]
|
|
extensionsState: Record<UuidString, ExtensionState>
|
|
selectedActionId?: number
|
|
menuItems: {
|
|
uuid: UuidString,
|
|
name: string,
|
|
loading: boolean,
|
|
error: boolean,
|
|
hidden: boolean,
|
|
actions: (Action & {
|
|
subrows?: ActionSubRow[]
|
|
})[]
|
|
}[]
|
|
}
|
|
|
|
class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements ActionsMenuScope {
|
|
application!: WebApplication
|
|
item!: SNItem
|
|
|
|
/* @ngInject */
|
|
constructor(
|
|
$timeout: ng.ITimeoutService
|
|
) {
|
|
super($timeout);
|
|
}
|
|
|
|
$onInit() {
|
|
super.$onInit();
|
|
this.initProps({
|
|
item: this.item
|
|
});
|
|
this.loadExtensions();
|
|
this.autorun(() => {
|
|
this.rebuildMenuState({
|
|
hiddenExtensions: this.appState.actionsMenu.hiddenExtensions
|
|
});
|
|
});
|
|
}
|
|
|
|
/** @override */
|
|
getInitialState() {
|
|
const extensions = this.application.actionsManager.getExtensions().sort((a, b) => {
|
|
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
|
}).map((extension) => {
|
|
return new SNActionsExtension(CopyPayload(extension.payload, {
|
|
content: {
|
|
...extension.payload.safeContent,
|
|
actions: []
|
|
}
|
|
}));
|
|
});
|
|
const extensionsState: Record<UuidString, ExtensionState> = {};
|
|
extensions.map((extension) => {
|
|
extensionsState[extension.uuid] = {
|
|
loading: true,
|
|
error: false,
|
|
};
|
|
});
|
|
return {
|
|
extensions,
|
|
extensionsState,
|
|
hiddenExtensions: {},
|
|
menuItems: [],
|
|
};
|
|
}
|
|
|
|
rebuildMenuState({
|
|
extensions = this.state.extensions,
|
|
extensionsState = this.state.extensionsState,
|
|
selectedActionId = this.state.selectedActionId,
|
|
hiddenExtensions = this.appState.actionsMenu.hiddenExtensions,
|
|
} = {}) {
|
|
return this.setState({
|
|
extensions,
|
|
extensionsState,
|
|
selectedActionId,
|
|
menuItems: extensions.map(extension => {
|
|
const state = extensionsState[extension.uuid];
|
|
const hidden = hiddenExtensions[extension.uuid];
|
|
return {
|
|
uuid: extension.uuid,
|
|
name: extension.name,
|
|
loading: state?.loading ?? false,
|
|
error: state?.error ?? false,
|
|
hidden: hidden ?? false,
|
|
deprecation: extension.deprecation!,
|
|
actions: extension.actionsWithContextForItem(this.item).map(action => {
|
|
if (action.id === selectedActionId) {
|
|
return {
|
|
...action,
|
|
subrows: this.subRowsForAction(action, extension)
|
|
};
|
|
} else {
|
|
return action;
|
|
}
|
|
})
|
|
};
|
|
})
|
|
});
|
|
}
|
|
|
|
async loadExtensions() {
|
|
await Promise.all(this.state.extensions.map(async (extension: SNActionsExtension) => {
|
|
this.setLoadingExtension(extension.uuid, true);
|
|
const updatedExtension = await this.application.actionsManager!.loadExtensionInContextOfItem(
|
|
extension,
|
|
this.item
|
|
);
|
|
if (updatedExtension) {
|
|
await this.updateExtension(updatedExtension!);
|
|
} else {
|
|
this.setErrorExtension(extension.uuid, true);
|
|
}
|
|
this.setLoadingExtension(extension.uuid, false);
|
|
}));
|
|
}
|
|
|
|
async executeAction(action: Action, extensionUuid: UuidString) {
|
|
if (action.verb === 'nested') {
|
|
this.rebuildMenuState({
|
|
selectedActionId: action.id
|
|
});
|
|
return;
|
|
}
|
|
const extension = this.application.findItem(extensionUuid) as SNActionsExtension;
|
|
await this.updateAction(action, extension, { running: true });
|
|
const response = await this.application.actionsManager!.runAction(
|
|
action,
|
|
this.item,
|
|
async () => {
|
|
/** @todo */
|
|
return '';
|
|
}
|
|
);
|
|
if (response.error) {
|
|
await this.updateAction(action, extension, { error: true });
|
|
return;
|
|
}
|
|
await this.updateAction(action, extension, { running: false });
|
|
this.handleActionResponse(action, response);
|
|
await this.reloadExtension(extension);
|
|
}
|
|
|
|
handleActionResponse(action: Action, result: ActionResponse) {
|
|
switch (action.verb) {
|
|
case 'render': {
|
|
const item = result.item;
|
|
this.application.presentRevisionPreviewModal(
|
|
item.uuid,
|
|
item.content
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private subRowsForAction(parentAction: Action, extension: Pick<SNActionsExtension, 'uuid'>): ActionSubRow[] | undefined {
|
|
if (!parentAction.subactions) {
|
|
return undefined;
|
|
}
|
|
return parentAction.subactions.map((subaction) => {
|
|
return {
|
|
id: subaction.id,
|
|
onClick: () => {
|
|
this.executeAction(subaction, extension.uuid);
|
|
},
|
|
label: subaction.label,
|
|
subtitle: subaction.desc,
|
|
spinnerClass: subaction.running ? 'info' : undefined
|
|
};
|
|
});
|
|
}
|
|
|
|
private async updateAction(
|
|
action: Action,
|
|
extension: SNActionsExtension,
|
|
params: {
|
|
running?: boolean
|
|
error?: boolean
|
|
}
|
|
) {
|
|
const updatedExtension = await this.application.changeItem(extension.uuid, (mutator) => {
|
|
const extensionMutator = mutator as ActionsExtensionMutator;
|
|
extensionMutator.actions = extension!.actions.map((act) => {
|
|
if (act && params && act.verb === action.verb && act.url === action.url) {
|
|
return {
|
|
...action,
|
|
running: params?.running,
|
|
error: params?.error,
|
|
} as Action;
|
|
}
|
|
return act;
|
|
});
|
|
}) as SNActionsExtension;
|
|
await this.updateExtension(updatedExtension);
|
|
}
|
|
|
|
private async updateExtension(extension: SNActionsExtension) {
|
|
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
|
|
if (extension.uuid === ext.uuid) {
|
|
return extension;
|
|
}
|
|
return ext;
|
|
});
|
|
await this.rebuildMenuState({
|
|
extensions
|
|
});
|
|
}
|
|
|
|
private async reloadExtension(extension: SNActionsExtension) {
|
|
const extensionInContext = await this.application.actionsManager!.loadExtensionInContextOfItem(
|
|
extension,
|
|
this.item
|
|
);
|
|
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
|
|
if (extension.uuid === ext.uuid) {
|
|
return extensionInContext!;
|
|
}
|
|
return ext;
|
|
});
|
|
this.rebuildMenuState({
|
|
extensions
|
|
});
|
|
}
|
|
|
|
public toggleExtensionVisibility(extensionUuid: UuidString) {
|
|
this.appState.actionsMenu.toggleExtensionVisibility(extensionUuid);
|
|
}
|
|
|
|
private setLoadingExtension(extensionUuid: UuidString, value = false) {
|
|
const { extensionsState } = this.state;
|
|
extensionsState[extensionUuid].loading = value;
|
|
this.rebuildMenuState({
|
|
extensionsState
|
|
});
|
|
}
|
|
|
|
private setErrorExtension(extensionUuid: UuidString, value = false) {
|
|
const { extensionsState } = this.state;
|
|
extensionsState[extensionUuid].error = value;
|
|
this.rebuildMenuState({
|
|
extensionsState
|
|
});
|
|
}
|
|
}
|
|
|
|
export class ActionsMenu extends WebDirective {
|
|
constructor() {
|
|
super();
|
|
this.restrict = 'E';
|
|
this.template = template;
|
|
this.replace = true;
|
|
this.controller = ActionsMenuCtrl;
|
|
this.controllerAs = 'self';
|
|
this.bindToController = true;
|
|
this.scope = {
|
|
item: '=',
|
|
application: '='
|
|
};
|
|
}
|
|
}
|