diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index c2022d4..4718e91 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -22,6 +22,8 @@ use OCA\Files_MindMap\Listener\LoadAdditionalListener; use OCA\Files_MindMap\Listener\LoadViewerListener; use OCA\Files_MindMap\Listener\LoadPublicViewerListener; +use OCA\Files_MindMap\Listener\RegisterTemplateCreatorListener; +use OCP\Files\Template\RegisterTemplateCreatorEvent; @@ -47,6 +49,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadPublicViewerListener::class); $context->registerEventListener(LoadViewer::class, LoadViewerListener::class); + $context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class); } public function boot(IBootContext $context): void { diff --git a/lib/Controller/FileHandlingController.php b/lib/Controller/FileHandlingController.php index 6230765..43d601f 100644 --- a/lib/Controller/FileHandlingController.php +++ b/lib/Controller/FileHandlingController.php @@ -79,6 +79,9 @@ public function load($dir, $filename) { } $fileContents = $file->getContent(); if ($fileContents !== false) { + if ($fileContents === '') { + $fileContents = '{"root":{"data":{"id":"root","text":"New mind map"},"children":[]}}'; + } $writable = $file->isUpdateable(); $mime = $file->getMimeType(); $mTime = $file->getMTime(); diff --git a/lib/Controller/PublicFileHandlingController.php b/lib/Controller/PublicFileHandlingController.php index 0d2f47f..7af9a10 100644 --- a/lib/Controller/PublicFileHandlingController.php +++ b/lib/Controller/PublicFileHandlingController.php @@ -121,6 +121,9 @@ public function load($token) { $fileContents = $fileNode->getContent(); if ($fileContents !== false) { + if ($fileContents === '') { + $fileContents = '{"root":{"data":{"id":"root","text":"New mind map"},"children":[]}}'; + } $writeable = $this->checkPermissions($share, \OCP\Constants::PERMISSION_UPDATE); return new DataResponse( [ diff --git a/lib/Listener/RegisterTemplateCreatorListener.php b/lib/Listener/RegisterTemplateCreatorListener.php new file mode 100644 index 0000000..1526f15 --- /dev/null +++ b/lib/Listener/RegisterTemplateCreatorListener.php @@ -0,0 +1,49 @@ + */ +final class RegisterTemplateCreatorListener implements IEventListener { + public function __construct(private IL10N $l10n) {} + + public function handle(Event $event): void { + if (!($event instanceof RegisterTemplateCreatorEvent)) { + return; + } + + $event->getTemplateManager()->registerTemplateFileCreator(function () { + $creator = new TemplateFileCreator( + Application::APPNAME, + $this->l10n->t('New mind map'), + '.km', + ); + $creator->addMimetype('application/km'); + + $iconContent = file_get_contents(__DIR__ . '/../../img/mindmap.svg'); + if ($iconContent !== false) { + if (method_exists($creator, 'setIconSvgInline')) { + $creator->setIconSvgInline($iconContent); + } else { + $creator->setIconClass('icon-mindmap'); + } + } else { + $creator->setIconClass('icon-template-add'); + } + + $creator->setActionLabel($this->l10n->t('Create new mind map')); + return $creator; + }); + } +} diff --git a/package.json b/package.json index 43beefe..5186cdd 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,9 @@ "@nextcloud/sharing": "^0.3.0", "jszip": "^3.10.1" }, + "peerDependencies": { + "vue": ">=2.7.0" + }, "devDependencies": { "@nextcloud/browserslist-config": "^3.0.0", "@nextcloud/eslint-config": "^v8.3.0", diff --git a/src/__tests__/MindMap.spec.js b/src/__tests__/MindMap.spec.js index ee0302e..0fc27ea 100644 --- a/src/__tests__/MindMap.spec.js +++ b/src/__tests__/MindMap.spec.js @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { shallowMount } from '@vue/test-utils' -import MindMap from '../views/MindMap.vue' +import MindMap from '../views/MindMap.js' vi.mock('@nextcloud/l10n', () => ({ getLanguage: () => 'en' })) vi.mock('@nextcloud/router', () => ({ @@ -32,7 +32,6 @@ const viewerMixin = { }, methods: { doneLoading() {}, - handleWebviewerloaded() {}, }, } @@ -51,7 +50,7 @@ function mountMindMap(dataOverrides = {}) { }) } -describe('MindMap.vue', () => { +describe('MindMap', () => { beforeEach(() => { window.OCA = { FilesMindMap: { setFile: vi.fn() } } }) @@ -106,13 +105,5 @@ describe('MindMap.vue', () => { }) expect(window.OCA.FilesMindMap.setFile).toHaveBeenCalled() }) - - it('removes the webviewerloaded event listener on destroy', () => { - const spy = vi.spyOn(document, 'removeEventListener') - const wrapper = mountMindMap() - wrapper.unmount() - expect(spy).toHaveBeenCalledWith('webviewerloaded', expect.anything()) - spy.mockRestore() - }) }) }) diff --git a/src/mindmap.js b/src/mindmap.js index 7adf836..e37bda3 100644 --- a/src/mindmap.js +++ b/src/mindmap.js @@ -7,22 +7,16 @@ /* global OCA */ // eslint-disable-next-line import/no-unresolved import SvgPencil from '@mdi/svg/svg/pencil.svg?raw' -// eslint-disable-next-line import/no-unresolved -import MindMapSvg from '../img/mindmap.svg?raw' import { DefaultType, registerFileAction, - File, Permission, - getUniqueName, } from '@nextcloud/files' import { FileAction, registerFileAction as legacyRegisterFileAction, - addNewFileMenuEntry as legacyAddNewFileMenuEntry, } from '@nextcloud/files-legacy' -import { emit } from '@nextcloud/event-bus' import axios from '@nextcloud/axios' import { getCurrentUser } from '@nextcloud/auth' import { dirname } from '@nextcloud/paths' @@ -230,50 +224,6 @@ const FilesMindMap = { } }, - registerNewFileMenuPlugin() { - legacyAddNewFileMenuEntry({ - id: 'mindmapfile', - displayName: t('files_mindmap', 'New mind map file'), - ...(version >= 33 ? { iconSvgInline: MindMapSvg } : { iconClass: 'icon-mindmap' }), - enabled(context) { - // only attach to main file list, public view is not supported yet - console.debug('addNewFileMenuEntry', context) - return (context.permissions & Permission.CREATE) !== 0 - }, - async handler(context, content) { - const contentNames = content.map((node) => node.basename) - const fileName = getUniqueName(t('files_mindmap', 'New mind map.km'), contentNames) - const source = context.encodedSource + '/' + encodeURIComponent(fileName) - - const response = await axios({ - method: 'PUT', - url: source, - headers: { - Overwrite: 'F', - }, - data: ' ', - }) - - const fileid = parseInt(response.headers['oc-fileid']) - const file = new File({ - source: context.source + '/' + fileName, - id: fileid, - mtime: new Date(), - mime: 'application/km', - owner: getCurrentUser()?.uid || null, - permissions: Permission.ALL, - root: context?.root || '/files/' + getCurrentUser()?.uid, - }) - - // FilesMindMap.showMessage(t('files_mindmap', 'Created "{name}"', { name: fileName })) - - emit('files:node:created', file) - - OCA.Viewer.openWith('mindmap', { path: file.path }) - }, - }) - }, - setFile(file) { const filename = file.filename + '' const basename = file.basename + '' diff --git a/src/mindmapviewer.js b/src/mindmapviewer.js index 5d7ed07..3269f4b 100644 --- a/src/mindmapviewer.js +++ b/src/mindmapviewer.js @@ -5,13 +5,12 @@ */ /* global OCA */ -import MindMap from './views/MindMap.vue' +import MindMap from './views/MindMap.js' import FilesMindMap from './mindmap.js' OCA.FilesMindMap = FilesMindMap FilesMindMap.init() -FilesMindMap.registerNewFileMenuPlugin() FilesMindMap.registerFileActions() const supportedMimes = OCA.FilesMindMap.getSupportedMimetypes() diff --git a/src/views/MindMap.js b/src/views/MindMap.js new file mode 100644 index 0000000..afb8f47 --- /dev/null +++ b/src/views/MindMap.js @@ -0,0 +1,59 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2024 Jingtao Yan + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/* global OCA */ +import { h as _h } from 'vue' +import { generateUrl } from '@nextcloud/router' + +console.debug('MindMap Vue Loading') + +// Plain Options API component — intentionally NOT a Vue SFC. +// The viewer app (Vue 2.7) renders this component; using a compiled SFC would +// bundle Vue 3 render helpers (createElementBlock, openBlock) that are +// incompatible with the Vue 2.7 runtime. The render(h) signature receives +// Vue 2.7's h function directly from the runtime, keeping VNodes compatible. +// The viewer also injects its Mime mixin (providing doneLoading, source, +// davPath, fileList, fileid etc.) via component.mixins — that merge only +// works for plain Options API objects, not - -