diff --git a/package-lock.json b/package-lock.json index 8a5f0643..a61e4afe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@nextcloud/dialogs": "^7.5.0", "@nextcloud/event-bus": "^3.1.0", "@nextcloud/files": "^4.1.0", - "@nextcloud/files-legacy": "npm:@nextcloud/files@^3.12.2", "@nextcloud/l10n": "^3.0.0", "@nextcloud/logger": "^3.0.0", "@nextcloud/paths": "^3.1.0", @@ -1503,68 +1502,6 @@ "node": "^24.0.0 || >=26.0.0" } }, - "node_modules/@nextcloud/files-legacy": { - "name": "@nextcloud/files", - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", - "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "cancelable-promise": "^4.3.1", - "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.1", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/files-legacy/node_modules/@nextcloud/files": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", - "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", - "license": "AGPL-3.0-or-later", - "optional": true, - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "cancelable-promise": "^4.3.1", - "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.1", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/files-legacy/node_modules/@nextcloud/sharing": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", - "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.0" - } - }, "node_modules/@nextcloud/image-editor": { "version": "1.0.0-beta.7", "resolved": "https://registry.npmjs.org/@nextcloud/image-editor/-/image-editor-1.0.0-beta.7.tgz", @@ -4547,12 +4484,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cancelable-promise": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/cancelable-promise/-/cancelable-promise-4.3.1.tgz", - "integrity": "sha512-A/8PwLk/T7IJDfUdQ68NR24QHa8rIlnN/stiJEBo6dmVUkD4K14LswG0w3VwdeK/o7qOwRUR1k2MhK5Rpy2m7A==", - "license": "MIT" - }, "node_modules/caniuse-lite": { "version": "1.0.30001810", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", diff --git a/package.json b/package.json index dd76bb0a..f4a9fd25 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@nextcloud/dialogs": "^7.5.0", "@nextcloud/event-bus": "^3.1.0", "@nextcloud/files": "^4.1.0", - "@nextcloud/files-legacy": "npm:@nextcloud/files@^3.12.2", "@nextcloud/l10n": "^3.0.0", "@nextcloud/logger": "^3.0.0", "@nextcloud/paths": "^3.1.0", diff --git a/src/__tests__/legacyFileAction.spec.js b/src/__tests__/legacyFileAction.spec.js new file mode 100644 index 00000000..f50d8003 --- /dev/null +++ b/src/__tests__/legacyFileAction.spec.js @@ -0,0 +1,41 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { registerLegacyFileAction } from '../legacyFileAction.js' + +vi.mock('@nextcloud/viewer', () => ({ getViewer: vi.fn() })) + +describe('registerLegacyFileAction', () => { + afterEach(() => { + delete window._nc_fileactions + }) + + it('registers one hidden default action in the files 3 registry', () => { + registerLegacyFileAction() + registerLegacyFileAction() + + expect(window._nc_fileactions).toHaveLength(1) + expect(window._nc_fileactions[0]).toMatchObject({ id: 'file_mindmap', default: 'hidden' }) + }) + + it('keeps actions registered before it', () => { + window._nc_fileactions = [{ id: 'other' }] + registerLegacyFileAction() + + expect(window._nc_fileactions.map((action) => action.id)).toEqual(['other', 'file_mindmap']) + }) + + it('is enabled for a single readable mind map only', () => { + registerLegacyFileAction() + const { enabled } = window._nc_fileactions[0] + const km = { mime: 'application/km', permissions: 1 } + + expect(enabled([km])).toBe(true) + expect(enabled([{ ...km, permissions: 0 }])).toBe(false) + expect(enabled([{ ...km, mime: 'text/plain' }])).toBe(false) + expect(enabled([km, km])).toBe(false) + }) +}) diff --git a/src/legacyFileAction.js b/src/legacyFileAction.js index 979f348d..cc3fe218 100644 --- a/src/legacyFileAction.js +++ b/src/legacyFileAction.js @@ -4,27 +4,44 @@ */ import SvgPencil from '@mdi/svg/svg/pencil.svg?raw' -import { DefaultType, FileAction, Permission, registerFileAction } from '@nextcloud/files-legacy' import { translate as t } from '@nextcloud/l10n' import { getViewer } from '@nextcloud/viewer' import { HANDLER_ID, MIMES } from './constants.js' import logger from './logger.js' -registerFileAction(new FileAction({ - id: 'file_mindmap', - displayName: () => t('files_mindmap', 'Edit'), - iconSvgInline: () => SvgPencil, - enabled: (nodes) => nodes.length === 1 && MIMES.includes(nodes[0].mime) && (nodes[0].permissions & Permission.READ) !== 0, - async exec(node) { - try { - await getViewer().open([node], node, {}, HANDLER_ID) - return true - } catch (error) { - logger.error('Could not open the mind map', { error }) - const { showError } = await import('@nextcloud/dialogs') - showError(error.message) - return false - } - }, - default: DefaultType.HIDDEN, -})) +const ACTION_ID = 'file_mindmap' +const PERMISSION_READ = 1 + +/** + * Register the hidden default action that opens mind maps from the Files app of Nextcloud 32. + * + * Files 32 looks up the default action once when it opens a file from the URL, as on a public + * file share, so this has to run synchronously in the init script. The action is a plain object + * in the `window._nc_fileactions` registry of `@nextcloud/files` 3, whose `FileAction` only + * wraps these properties, so that library does not load on every page. + */ +export function registerLegacyFileAction() { + window._nc_fileactions ??= [] + if (window._nc_fileactions.some((action) => action.id === ACTION_ID)) { + return + } + + window._nc_fileactions.push({ + id: ACTION_ID, + displayName: () => t('files_mindmap', 'Edit'), + iconSvgInline: () => SvgPencil, + enabled: (nodes) => nodes.length === 1 && MIMES.includes(nodes[0].mime) && (nodes[0].permissions & PERMISSION_READ) !== 0, + async exec(node) { + try { + await getViewer().open([node], node, {}, HANDLER_ID) + return true + } catch (error) { + logger.error('Could not open the mind map', { error }) + const { showError } = await import('@nextcloud/dialogs') + showError(error.message) + return false + } + }, + default: 'hidden', + }) +} diff --git a/src/mindmapviewer.js b/src/mindmapviewer.js index 5e98fda6..b999c82a 100644 --- a/src/mindmapviewer.js +++ b/src/mindmapviewer.js @@ -7,6 +7,7 @@ import { translate as t } from '@nextcloud/l10n' import { registerHandler } from '@nextcloud/viewer' import { HANDLER_ID, MIMES, TAG_NAME } from './constants.js' +import { registerLegacyFileAction } from './legacyFileAction.js' import MindMap from './views/MindMap.js' const version = Number.parseInt((window.OC?.config?.version ?? '0').split('.')[0]) @@ -25,5 +26,5 @@ registerHandler({ // From 33 on the viewer registers the actions for its handlers itself if (version < 33) { - import('./legacyFileAction.js') + registerLegacyFileAction() }