From ed8b61efefabe97bd15709fbb14efc8b713e7fe9 Mon Sep 17 00:00:00 2001 From: BOTSU Date: Tue, 4 Aug 2026 06:24:01 +0200 Subject: [PATCH] feat: auto-save local BOTSU documents Add a strict local draft update helper and wire the document editor to auto-save changes on this device. Keep the UI honest about local-only persistence while adding editor coverage to test:botsu. --- apps/client/package.json | 2 +- .../botsu/documents/BotsuDocumentEditor.tsx | 43 +++++++++---------- .../client/src/botsu/documents/editor.test.ts | 17 ++++++++ apps/client/src/botsu/documents/model.test.ts | 37 ++++++++++++++++ apps/client/src/botsu/documents/model.ts | 19 ++++++++ 5 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 apps/client/src/botsu/documents/editor.test.ts diff --git a/apps/client/package.json b/apps/client/package.json index 053e91b..99f9a2c 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -16,7 +16,7 @@ "check:prettier": "prettier --check .", "fix:prettier": "prettier --write .", "typecheck": "tsc --noEmit", - "test:botsu": "node --experimental-strip-types --test src/botsu/apps/catalog.test.ts src/botsu/apps/launcher.test.ts src/botsu/documents/model.test.ts src/botsu/profile/summary.test.ts src/botsu/presence/compact-presence.test.ts src/botsu/presence/connection-generation.test.ts src/botsu/presence/cursor-publisher.test.ts src/botsu/presence/cursor-state.test.ts src/botsu/presence/join-destination.test.ts src/botsu/presence/location-publisher.test.ts src/botsu/presence/socket-message.test.ts src/botsu/presence/state.test.ts src/botsu/shell/layout.test.ts src/botsu/start/model.test.ts src/botsu/start/pixel-canvas-bridge.test.ts src/botsu/start/pixel-canvas-publisher.test.ts src/botsu/workspace/editor.test.ts src/botsu/workspace/permissions.test.ts src/botsu/workspace/state.test.ts", + "test:botsu": "node --experimental-strip-types --test src/botsu/apps/catalog.test.ts src/botsu/apps/launcher.test.ts src/botsu/documents/model.test.ts src/botsu/documents/editor.test.ts src/botsu/profile/summary.test.ts src/botsu/presence/compact-presence.test.ts src/botsu/presence/connection-generation.test.ts src/botsu/presence/cursor-publisher.test.ts src/botsu/presence/cursor-state.test.ts src/botsu/presence/join-destination.test.ts src/botsu/presence/location-publisher.test.ts src/botsu/presence/socket-message.test.ts src/botsu/presence/state.test.ts src/botsu/shell/layout.test.ts src/botsu/start/model.test.ts src/botsu/start/pixel-canvas-bridge.test.ts src/botsu/start/pixel-canvas-publisher.test.ts src/botsu/workspace/editor.test.ts src/botsu/workspace/permissions.test.ts src/botsu/workspace/state.test.ts", "typecheck:botsu": "../../node_modules/.bin/tsc -p tsconfig.botsu.json && ../../node_modules/.bin/tsc -p tsconfig.botsu-ui.json", "prepare": "husky install", "commit": "git-cz", diff --git a/apps/client/src/botsu/documents/BotsuDocumentEditor.tsx b/apps/client/src/botsu/documents/BotsuDocumentEditor.tsx index a8fe8be..cdd0a59 100644 --- a/apps/client/src/botsu/documents/BotsuDocumentEditor.tsx +++ b/apps/client/src/botsu/documents/BotsuDocumentEditor.tsx @@ -1,10 +1,10 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { loadBotsuDocumentDraft, parseBotsuDocumentDraft, parseBotsuDocumentId, - saveBotsuDocumentDraft, + updateBotsuDocumentDraft, type BotsuDocumentDraft, } from './model'; @@ -26,26 +26,28 @@ const createInitialDraft = (id: string): BotsuDocumentDraft => { function DocumentEditor({ id }: { id: string }) { const [draft, setDraft] = useState(() => createInitialDraft(id)); - const [status, setStatus] = useState('Brouillon local non synchronisé'); - const [error, setError] = useState(); + const [status, setStatus] = useState('Auto-sauvegarde locale prête'); + const [autosaveError, setAutosaveError] = useState(); - const save = () => { + useEffect(() => { try { - const next = parseBotsuDocumentDraft({ - ...draft, - title: draft.title.trim(), - updatedAt: Date.now(), - }); - saveBotsuDocumentDraft(window.localStorage, next); - setDraft(next); - setError(undefined); - setStatus('Enregistré sur cet appareil'); + const next = updateBotsuDocumentDraft( + window.localStorage, + draft.id, + { title: draft.title, body: draft.body }, + Date.now + ); + if (next.title !== draft.title) { + setDraft(next); + } + setAutosaveError(undefined); + setStatus('Enregistré automatiquement sur cet appareil'); } catch { - setError( - 'Enregistrement impossible. Le titre est obligatoire et le stockage doit être disponible.' + setAutosaveError( + 'Auto-sauvegarde impossible. Le stockage local doit être disponible sur cet appareil.' ); } - }; + }, [draft.body, draft.id, draft.title]); return (
@@ -59,9 +61,6 @@ function DocumentEditor({ id }: { id: string }) { Tous les documents - @@ -98,9 +97,9 @@ function DocumentEditor({ id }: { id: string }) { /> - {error ? ( + {autosaveError ? (

- {error} + {autosaveError}

) : (

diff --git a/apps/client/src/botsu/documents/editor.test.ts b/apps/client/src/botsu/documents/editor.test.ts new file mode 100644 index 0000000..b26a3da --- /dev/null +++ b/apps/client/src/botsu/documents/editor.test.ts @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const editorUrl = new URL('./BotsuDocumentEditor.tsx', import.meta.url); + +test('document editor auto-saves local drafts instead of requiring a manual save button', async () => { + const editor = await readFile(editorUrl, 'utf8'); + + assert.match(editor, /updateBotsuDocumentDraft/); + assert.match(editor, /useEffect/); + assert.match(editor, /Auto-sauvegarde locale/); + assert.match(editor, /Enregistré automatiquement sur cet appareil/); + assert.match(editor, /setAutosaveError/); + assert.doesNotMatch(editor, /