Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;



Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions lib/Controller/FileHandlingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions lib/Controller/PublicFileHandlingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down
49 changes: 49 additions & 0 deletions lib/Listener/RegisterTemplateCreatorListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_MindMap\Listener;

use OCA\Files_MindMap\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Template\RegisterTemplateCreatorEvent;
use OCP\Files\Template\TemplateFileCreator;
use OCP\IL10N;

/** @template-implements IEventListener<RegisterTemplateCreatorEvent|Event> */
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;
});
}
}
13 changes: 2 additions & 11 deletions src/__tests__/MindMap.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -32,7 +32,6 @@ const viewerMixin = {
},
methods: {
doneLoading() {},
handleWebviewerloaded() {},
},
}

Expand All @@ -51,7 +50,7 @@ function mountMindMap(dataOverrides = {}) {
})
}

describe('MindMap.vue', () => {
describe('MindMap', () => {
beforeEach(() => {
window.OCA = { FilesMindMap: { setFile: vi.fn() } }
})
Expand Down Expand Up @@ -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()
})
})
})
50 changes: 0 additions & 50 deletions src/mindmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 + ''
Expand Down
3 changes: 1 addition & 2 deletions src/mindmapviewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
56 changes: 56 additions & 0 deletions src/views/MindMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2024 Jingtao Yan <i@actom.me>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/* global OCA */
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 <script setup> components.
export default {
name: 'MindMap',
inheritAttrs: false,

computed: {
iframeSrc() {
return generateUrl('/apps/files_mindmap/?file={file}', {
file: this.source ?? this.davPath,
})
},

file() {
return this.fileList.find((f) => f.fileid === this.fileid)
},
},

mounted() {
console.debug('mounted file: ', this.file)
if (OCA.FilesMindMap) {
OCA.FilesMindMap.setFile(this.file)
}
this.doneLoading()
},

render(h) {
return h('iframe', {

Check failure on line 45 in src/views/MindMap.js

View workflow job for this annotation

GitHub Actions / test

src/__tests__/MindMap.spec.js > MindMap > lifecycle hooks > calls OCA.FilesMindMap.setFile on mount

TypeError: h is not a function ❯ Proxy.render src/views/MindMap.js:45:10 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4543:16 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6065:46 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6200:5 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5972:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5924:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5434:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6072:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19

Check failure on line 45 in src/views/MindMap.js

View workflow job for this annotation

GitHub Actions / test

src/__tests__/MindMap.spec.js > MindMap > file computed property > returns undefined when no file matches

TypeError: h is not a function ❯ Proxy.render src/views/MindMap.js:45:10 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4543:16 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6065:46 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6200:5 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5972:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5924:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5434:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6072:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19

Check failure on line 45 in src/views/MindMap.js

View workflow job for this annotation

GitHub Actions / test

src/__tests__/MindMap.spec.js > MindMap > file computed property > returns the file whose fileid matches the current fileid

TypeError: h is not a function ❯ Proxy.render src/views/MindMap.js:45:10 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4543:16 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6065:46 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6200:5 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5972:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5924:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5434:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6072:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19

Check failure on line 45 in src/views/MindMap.js

View workflow job for this annotation

GitHub Actions / test

src/__tests__/MindMap.spec.js > MindMap > iframeSrc computed property > falls back to davPath when source is null

TypeError: h is not a function ❯ Proxy.render src/views/MindMap.js:45:10 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4543:16 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6065:46 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6200:5 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5972:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5924:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5434:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6072:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19

Check failure on line 45 in src/views/MindMap.js

View workflow job for this annotation

GitHub Actions / test

src/__tests__/MindMap.spec.js > MindMap > iframeSrc computed property > uses source when available

TypeError: h is not a function ❯ Proxy.render src/views/MindMap.js:45:10 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4543:16 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6065:46 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6200:5 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5972:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5924:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5434:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6072:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19

Check failure on line 45 in src/views/MindMap.js

View workflow job for this annotation

GitHub Actions / test

src/__tests__/MindMap.spec.js > MindMap > renders an iframe element

TypeError: h is not a function ❯ Proxy.render src/views/MindMap.js:45:10 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4543:16 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6065:46 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6200:5 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5972:7 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5924:9 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5434:11 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:6072:11 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:254:19
style: {
width: '100%',
height: 'calc(100vh - var(--header-height))',
marginTop: 'var(--header-height)',
position: 'absolute',
},
attrs: { src: this.iframeSrc },
on: { load: () => { console.debug('File:', this.file) } },
})
},
}
85 changes: 0 additions & 85 deletions src/views/MindMap.vue

This file was deleted.

Loading