From 9f952461b8adee76a640016c2c9e1b1dac82a295 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Sat, 17 Oct 2026 19:01:19 +0200 Subject: [PATCH] fix(files): register the Nextcloud 32 action before Files opens a file Files 32 looks up the default action once when it opens a file from the URL, as on a public file share. The action was registered from a lazily loaded chunk, so when that chunk came late, as it sometimes did in Firefox, the share page showed the file list instead of the mind map. Register it synchronously from the init script instead. To keep the init script small it is a plain object in the window._nc_fileactions registry of @nextcloud/files 3, whose FileAction only wraps these properties, and @nextcloud/files-legacy is dropped. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- package-lock.json | 69 -------------------------- package.json | 1 - src/__tests__/legacyFileAction.spec.js | 41 +++++++++++++++ src/legacyFileAction.js | 55 +++++++++++++------- src/mindmapviewer.js | 3 +- 5 files changed, 79 insertions(+), 90 deletions(-) create mode 100644 src/__tests__/legacyFileAction.spec.js 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() }