From bd8cf4c3ffb98da059ca08c9377f3f004c834181 Mon Sep 17 00:00:00 2001 From: Stefan Ernst <24554551+stefan-ernst@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:58:49 +0100 Subject: [PATCH] feat(plugins): add the editor.markdown extension point (WI-1787) Expose a plugin extension point so an installed plugin can render its own Markdown editor (e.g. a plain textarea). The host stays the only writer: it holds the content, validates it, and persists through the normal item/page APIs. The editor plugin itself lives in a separate repository. Backend: - Extension gains mode (toggle/replace, default toggle) and entityTypes. - LoadPlugin validates editor.markdown manifests (component required, known entity types, known mode) before compilation and again for the resolved manifest+metadata set. - GetExtensions now applies the same license gate as GetCapabilities, so an unlicensed plugin cannot advertise an editor provider. - GET /api/plugins/extensions accepts ?point= so an editor surface receives only the point it needs. Frontend: - extensions store: ensureExtensionsLoaded() shares one in-flight request; getEditorProviders() filters by entity type and sorts by order. - PluginBridge: reject messages whose event.source is not the bridge's iframe; add the editor protocol (init/update/save/cancel/dispose and the plugin -> host counterparts), with every plugin message required to echo the sessionId so two editors on one page cannot cross-talk. - PluginEditorHost.svelte: resolves a provider, shows an opt-in toggle, mounts the plugin iframe, forwards save/cancel to the parent, and mirrors MilkdownEditor's imperative surface. Unsaved edits are confirmed before switching, and a save over 256 KB is refused. - ItemDetailDescription enables providers for the item description. The react-demo manifest drops its unparsed displayMode field. --- .../src/lib/editors/PluginEditorHost.svelte | 297 ++++++++++++++++++ .../items/ItemDetailDescription.svelte | 6 +- frontend/src/lib/locales/de/ui.js | 8 + frontend/src/lib/locales/en/ui.js | 8 + frontend/src/lib/locales/fr/ui.js | 8 + frontend/src/lib/locales/ru/ui.js | 8 + frontend/src/lib/locales/zh-CN/ui.js | 7 + frontend/src/lib/services/PluginBridge.js | 74 +++++ frontend/src/lib/stores/extensions.svelte.js | 77 ++++- internal/handlers/plugins.go | 10 + internal/plugins/manager.go | 52 +++ internal/plugins/types.go | 37 ++- plugin-src/react-demo/manifest.json | 1 - 13 files changed, 565 insertions(+), 28 deletions(-) create mode 100644 frontend/src/lib/editors/PluginEditorHost.svelte diff --git a/frontend/src/lib/editors/PluginEditorHost.svelte b/frontend/src/lib/editors/PluginEditorHost.svelte new file mode 100644 index 000000000..9c927598d --- /dev/null +++ b/frontend/src/lib/editors/PluginEditorHost.svelte @@ -0,0 +1,297 @@ + + +
+ {#if providers.length > 0 && !readonly} +
+ {#if activeProvider} + {activeProvider.label || activeProvider.id} + + {:else} + {#each providers as provider (provider.id)} + + {/each} + {/if} +
+ {/if} + + {#if activeProvider} + {#if activeStyles} + + {/if} + {#if pluginError} + + {/if} + {#if !pluginReady} +
+ +
+ {/if} + + {:else} + + {/if} +
+ + diff --git a/frontend/src/lib/features/items/ItemDetailDescription.svelte b/frontend/src/lib/features/items/ItemDetailDescription.svelte index 1b0209654..3c3c23b50 100644 --- a/frontend/src/lib/features/items/ItemDetailDescription.svelte +++ b/frontend/src/lib/features/items/ItemDetailDescription.svelte @@ -9,6 +9,7 @@ import Button from '../../components/Button.svelte'; import FileInput from '../../components/FileInput.svelte'; import MilkdownEditor from '../../editors/LazyMilkdownEditor.svelte'; + import PluginEditorHost from '../../editors/PluginEditorHost.svelte'; import AttachmentDiagramList from '../assets/AttachmentDiagramList.svelte'; import AIActionsDropdown from './AIActionsDropdown.svelte'; import { getShortcut, matchesShortcut, getDisplayString } from '../../utils/keyboardShortcuts.js'; @@ -177,14 +178,17 @@ {#if editingDescription}
-