#160: Plugin-Block - Bearbeiten-Knopf nach Moduswechsel wieder da #161

Merged
stwaidele merged 1 commits from fix-160-plugin-block-editable into main 2026-07-20 23:22:40 +02:00
2 changed files with 70 additions and 1 deletions
Showing only changes of commit db0e563f95 - Show all commits

View File

@ -115,6 +115,47 @@ test('a block round-trips: insert → edit data → reload → render', async ({
await admin.close();
});
test('the edit affordance appears when edit mode starts after load (issue #160)', async ({
browser,
}) => {
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
await installAsRequired(admin);
const pond = await personalPond(admin);
const created = await createPage(admin, pond.id, `E2E Block Stale Editable ${Date.now()}`);
// Seed a page that already carries a block (the edit round-trip persists
// "+e" into the document before the reload below).
const seeded = await openEditor(admin, pond.slug, created.slug);
await insertBlock(seeded);
await seeded.locator('.plugin-block__bar button').click();
await expect(frameBody(seeded)).toHaveText('editing:+e');
await seeded.locator('.plugin-block__bar button').click();
await expect(frameBody(seeded)).toHaveText('block:+e');
// A fresh load mounts the node view in read mode. Entering edit mode goes
// through `setEditable`, which dispatches no transaction — the bug (#160)
// was a stale `editable` from mount keeping the edit button hidden.
await seeded.reload();
await expect(frameBody(seeded)).toHaveText('block:+e', { timeout: 15000 });
await expect(seeded.locator('.plugin-block__surface')).toHaveAttribute('data-state', 'ready');
await seeded.locator('.editor-page__mode-toggle').click();
await expect(seeded.locator('.ProseMirror')).toHaveAttribute('contenteditable', 'true');
const editButton = seeded.locator('.plugin-block__bar button');
await expect(editButton).toBeVisible();
// The button is functional, not just painted.
await editButton.click();
await expect(frameBody(seeded)).toHaveText('editing:+e+e');
// Leaving page edit mode while the plugin's edit UI is open must drop the
// frame back to render — read mode has no bar to leave edit mode with.
await seeded.locator('.editor-page__mode-toggle').click();
await expect(frameBody(seeded)).toHaveText('block:+e+e');
await admin.close();
});
test('two collaborating users see block-data changes live', async ({ browser }) => {
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');

View File

@ -27,6 +27,26 @@ declare module '@tiptap/core' {
type BlockMode = 'render' | 'edit';
/**
* `editor.isEditable`, kept live (issue #160): the page's read/edit toggle
* flips editability through `setEditable`, which emits `update` but dispatches
* no transaction so React node views never re-render on their own and a
* plain `editor.isEditable` read goes stale at mount (the page always mounts
* in read mode, hiding the block's edit button for the whole session).
*/
function useEditorEditable(editor: NodeViewProps['editor']): boolean {
const [editable, setEditable] = useState(editor.isEditable);
useEffect(() => {
const sync = (): void => setEditable(editor.isEditable);
sync();
editor.on('update', sync);
return () => {
editor.off('update', sync);
};
}, [editor]);
return editable;
}
/**
* The live surface of one plugin block (issue #76): a sandboxed iframe (#73)
* driven through the plugin's `render`/`edit` lifecycle. Data flows two ways
@ -133,6 +153,13 @@ function ActivePluginBlock({
.catch(() => undefined);
}
// Losing editability while the plugin's edit UI is open (the page toggled
// back to read mode) must drop the frame back to render — read mode shows
// no bar to leave edit mode with (issue #160).
useEffect(() => {
if (!editable && modeRef.current === 'edit') void switchMode('render');
}, [editable]);
return (
<div className="plugin-block__surface" data-state={state} data-mode={mode}>
<div className="plugin-block__bar" contentEditable={false}>
@ -208,6 +235,7 @@ function PluginBlockView({
const blockType = node.attrs.blockType as string;
const plugins = usePondPlugins(scope.pondId);
const plugin = plugins.data?.find((entry) => entry.id === pluginId && entry.kind === 'code');
const editable = useEditorEditable(editor);
return (
<NodeViewWrapper
@ -219,7 +247,7 @@ function PluginBlockView({
plugin={plugin}
blockType={blockType}
data={node.attrs.data}
editable={editor.isEditable}
editable={editable}
updateData={(data) => updateAttributes({ data })}
/>
) : plugins.isSuccess ? (