diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index 88ec345fcf2..f8b7fdb1ac6 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -20,11 +20,12 @@ * */ import { emit } from '@nextcloud/event-bus' -import { registerFileAction, Permission, FileAction, Node } from '@nextcloud/files' +import { Permission, Node } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import TrashCan from '@mdi/svg/svg/trash-can.svg?raw' +import { registerFileAction, FileAction } from '../services/FileAction' import logger from '../logger' registerFileAction(new FileAction({ diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index a4b373a7d9d..33e8438156b 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -105,6 +105,7 @@ import { Fragment } from 'vue-fragment' import { join } from 'path' import { showError } from '@nextcloud/dialogs' import { translate } from '@nextcloud/l10n' +import CancelablePromise from 'cancelable-promise' import FileIcon from 'vue-material-design-icons/File.vue' import FolderIcon from 'vue-material-design-icons/Folder.vue' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' diff --git a/apps/files/src/services/FileAction.ts b/apps/files/src/services/FileAction.ts new file mode 100644 index 00000000000..8c1d325e645 --- /dev/null +++ b/apps/files/src/services/FileAction.ts @@ -0,0 +1,184 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ + * + * @author John Molakvoæ + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { Node } from '@nextcloud/files' +import logger from '../logger' + +declare global { + interface Window { + OC: any; + _nc_fileactions: FileAction[] | undefined; + } +} + +/** + * TODO: remove and move to @nextcloud/files + * @see https://github.com/nextcloud/nextcloud-files/pull/608 + */ +interface FileActionData { + /** Unique ID */ + id: string + /** Translatable string displayed in the menu */ + displayName: (files: Node[], view) => string + /** Svg as inline string. */ + iconSvgInline: (files: Node[], view) => string + /** Condition wether this action is shown or not */ + enabled?: (files: Node[], view) => boolean + /** + * Function executed on single file action + * @returns true if the action was executed, false otherwise + * @throws Error if the action failed + */ + exec: (file: Node, view) => Promise, + /** + * Function executed on multiple files action + * @returns true if the action was executed, false otherwise + * @throws Error if the action failed + */ + execBatch?: (files: Node[], view) => Promise + /** This action order in the list */ + order?: number, + /** Make this action the default */ + default?: boolean, + /** + * If true, the renderInline function will be called + */ + inline?: (file: Node, view) => boolean, + /** + * If defined, the returned html element will be + * appended before the actions menu. + */ + renderInline?: (file: Node, view) => HTMLElement, +} + +export class FileAction { + + private _action: FileActionData + + constructor(action: FileActionData) { + this.validateAction(action) + this._action = action + } + + get id() { + return this._action.id + } + + get displayName() { + return this._action.displayName + } + + get iconSvgInline() { + return this._action.iconSvgInline + } + + get enabled() { + return this._action.enabled + } + + get exec() { + return this._action.exec + } + + get execBatch() { + return this._action.execBatch + } + + get order() { + return this._action.order + } + + get default() { + return this._action.default + } + + get inline() { + return this._action.inline + } + + get renderInline() { + return this._action.renderInline + } + + private validateAction(action: FileActionData) { + if (!action.id || typeof action.id !== 'string') { + throw new Error('Invalid id') + } + + if (!action.displayName || typeof action.displayName !== 'function') { + throw new Error('Invalid displayName function') + } + + if (!action.iconSvgInline || typeof action.iconSvgInline !== 'function') { + throw new Error('Invalid iconSvgInline function') + } + + if (!action.exec || typeof action.exec !== 'function') { + throw new Error('Invalid exec function') + } + + // Optional properties -------------------------------------------- + if ('enabled' in action && typeof action.enabled !== 'function') { + throw new Error('Invalid enabled function') + } + + if ('execBatch' in action && typeof action.execBatch !== 'function') { + throw new Error('Invalid execBatch function') + } + + if ('order' in action && typeof action.order !== 'number') { + throw new Error('Invalid order') + } + + if ('default' in action && typeof action.default !== 'boolean') { + throw new Error('Invalid default') + } + + if ('inline' in action && typeof action.inline !== 'function') { + throw new Error('Invalid inline function') + } + + if ('renderInline' in action && typeof action.renderInline !== 'function') { + throw new Error('Invalid renderInline function') + } + } + +} + +export const registerFileAction = function(action: FileAction): void { + if (typeof window._nc_fileactions === 'undefined') { + window._nc_fileactions = [] + logger.debug('FileActions initialized') + } + + // Check duplicates + if (window._nc_fileactions.find(search => search.id === action.id)) { + logger.error(`FileAction ${action.id} already registered`, { action }) + return + } + + window._nc_fileactions.push(action) +} + +export const getFileActions = function(): FileAction[] { + return window._nc_fileactions || [] +} diff --git a/apps/files_trashbin/src/actions/restoreAction.ts b/apps/files_trashbin/src/actions/restoreAction.ts index fe896f6b618..0d309a3f3c6 100644 --- a/apps/files_trashbin/src/actions/restoreAction.ts +++ b/apps/files_trashbin/src/actions/restoreAction.ts @@ -22,11 +22,13 @@ import { emit } from '@nextcloud/event-bus' import { generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' -import { registerFileAction, Permission, FileAction, Node } from '@nextcloud/files' +import { Permission, Node } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import History from '@mdi/svg/svg/history.svg?raw' +import { registerFileAction, FileAction } from '../../../files/src/services/FileAction' + registerFileAction(new FileAction({ id: 'restore', displayName() { diff --git a/package.json b/package.json index 2c2bbf28eaa..2feb218a387 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "blueimp-md5": "^2.19.0", "browserslist-useragent-regexp": "^3.0.2", "camelcase": "^6.3.0", + "cancelable-promise": "^4.3.1", "clipboard": "^2.0.11", "colord": "^2.9.3", "core-js": "^3.24.0",