# Acadenice Patches Liste des patches custom appliques sur le fork Acadenice de Docmost. Ce document est maintenu manuellement pour faciliter le rebase upstream. Repo upstream : `github.com/docmost/docmost` Branche fork : `acadenice/main` ## Conventions - Chaque patch est commit isole avec scope `feat(rebrand)` / `feat(custom)` / etc. - Les modifications in-line de fichiers upstream sont documentees ici avec rationale. - Les nouveaux fichiers (extensions Tiptap custom, hooks, etc.) vont dans des emplacements dedies pour minimiser les conflits de rebase. --- ## Patch 001 — Rebrand minimal "Docmost" -> "DocAdenice" **Date** : 2026-05-07 **Scope** : strings UI visibles utilisateur uniquement **Rationale** : nom temporaire pour les beta-testeurs en attendant le vrai rebranding (logo SVG + design system + manifest PWA). Conserve les identifiants techniques pour ne rien casser et faciliter le rebase upstream. ### Fichiers modifies | Fichier | Avant | Apres | |---------|-------|-------| | `apps/client/index.html` | `
| ` : ajout `data-testid={`cell-${row.id}-${field.name}`}` — permet de cibler une cellule specifique par row ID et nom de champ. #### `apps/client/src/features/acadenice/database-view/components/inline-editor.tsx` - Branche `!canWrite` : ajout `data-testid="inline-editor-readonly"` sur le `` — permet aux tests RBAC de verifier que l'editeur est bien en lecture seule. - Branche `default` (`TextInput`) : ajout `data-testid="inline-editor-input"` — permet de cibler l'input dans les tests d'edition inline. #### `apps/client/src/features/acadenice/database-view/renderers/kanban-renderer.tsx` - `KanbanCard` wrapper div : ajout `data-testid={`kanban-card-${row.id}`}` — ciblage par row ID pour les tests de drag. - `KanbanColumn` wrapper div : ajout `data-testid={`kanban-column-${column.label}`}` — ciblage par label de colonne. - Board div (DndContext child) : ajout `data-testid="kanban-board"` — detection de la presence du kanban dans la page. #### `apps/client/src/features/acadenice/database-view/renderers/calendar-renderer.tsx` - Root div du composant : ajout `data-testid="calendar-renderer"` — detection de la presence du calendrier. #### `apps/client/Dockerfile.e2e` (nouveau) - Cree pour docker-compose.e2e.yml : build Vite du client + serve via `serve@14` sur le port 5173. - Utilise uniquement pour les e2e — pas de changement sur le Dockerfile de production. ### Raison des choix - `data-testid` sur les elements conteneurs plutot que sur les elements internes — plus stable face aux refactors de structure interne. - Pas de `data-testid` sur les elements Mantine (Button, Select, etc.) : ces composants ont leurs propres selectors d'accessibilite (role, aria-label) que Playwright prefere nativement. - Aucun `data-testid` ajoute dans les hooks, services, ou extension Tiptap — non necessaire pour les assertions UI e2e. ### Tests impactes 7 scenarios e2e dans `e2e/tests/` utilisent ces testids : - `database-view-insert.spec.ts` : `table-renderer` - `database-view-edit-inline.spec.ts` : `cell-{rowId}-{fieldName}`, `inline-editor-input` - `database-view-realtime-sse.spec.ts` : `table-renderer` - `database-view-rbac-denied.spec.ts` : `inline-editor-readonly` - `database-view-kanban-drag.spec.ts` : `kanban-board`, `kanban-column-{label}`, `kanban-card-{rowId}` - `database-view-calendar-reschedule.spec.ts` : `calendar-renderer` --- ## Patch 009 — R3.2 : Backlinks bidirectionnels + extension Tiptap wikilinks **Date** : 2026-05-08 **Commit** : `2fc310a` **Scope** : Schema DB + module backend backlinks + extension Tiptap wikilinks + panel UI "Linked references" **Rationale** : R3.2 permet a chaque page DocAdenice de savoir quelles autres pages y font reference. Deux vecteurs : wikilinks `[[Page Title]]` / `[[Page Title|alias]]` (nouveau) et mentions `@page` existantes (deja dans Docmost natif). L'indexation est async (fire-and-forget apres chaque save collaboratif) et idempotente (delete-then-insert par page source). ### Table SQL `acadenice_backlink` (prefixe acadenice_, zero conflit upstream) : - `id` UUID PK, `source_page_id` -> `pages.id` CASCADE, `target_page_id` -> `pages.id` CASCADE - `link_type` VARCHAR(20) CHECK IN ('wikilink', 'mention', 'database_embed') - `context_excerpt` TEXT (200 chars autour du lien pour preview UI) - `workspace_id` -> `workspaces.id` CASCADE (scope guard) - UNIQUE(source_page_id, target_page_id, link_type) - 3 index: idx_backlink_target, idx_backlink_source, idx_backlink_workspace - Migration up + down idempotente (ifNotExists) ### Fichiers crees (backend) | Fichier | Role | |---------|------| | `apps/server/src/database/migrations/20260508T100000-create-acadenice-backlink.ts` | Migration Kysely up+down | | `apps/server/src/core/acadenice/backlinks/backlinks.module.ts` | Module NestJS | | `apps/server/src/core/acadenice/backlinks/services/backlink-parser.service.ts` | Walke Tiptap JSON, extrait wikilinks/mentions/databaseView, resoud titres via LIKE workspace-scoped | | `apps/server/src/core/acadenice/backlinks/services/backlink-indexer.service.ts` | Idempotent reindex (delete source -> insert), skip self-refs + null targets | | `apps/server/src/core/acadenice/backlinks/services/backlink.service.ts` | Lecture permission-aware (space_members / public visibility), groupe par link_type | | `apps/server/src/core/acadenice/backlinks/controllers/backlinks.controller.ts` | GET /api/acadenice/pages/:pageId/backlinks (JwtAuthGuard) | | `apps/server/src/core/acadenice/backlinks/events/page-content-updated.listener.ts` | @OnEvent listener -> reindex async, exporte ACADENICE_PAGE_CONTENT_UPDATED_EVENT | | `apps/server/src/core/acadenice/backlinks/spec/backlink-parser.service.spec.ts` | 10 tests Vitest | | `apps/server/src/core/acadenice/backlinks/spec/backlink-indexer.service.spec.ts` | 5 tests Vitest | | `apps/server/src/core/acadenice/backlinks/spec/backlink.service.spec.ts` | 4 tests Vitest | | `apps/server/src/core/acadenice/backlinks/spec/backlinks.controller.spec.ts` | 4 tests Vitest | ### Fichiers crees (frontend) | Fichier | Role | |---------|------| | `apps/client/src/features/acadenice/wikilinks/extension/wikilink-extension.ts` | Tiptap Node inline+atom, attrs pageId/title/alias, Suggestion plugin [[, insertWikilink command, ReactNodeView (broken-link state) | | `apps/client/src/features/acadenice/wikilinks/extension/wikilink-suggestion.ts` | Render callbacks floating-ui (pattern mention) | | `apps/client/src/features/acadenice/wikilinks/extension/wikilink-list.tsx` | Popup suggestions pages via useSearchSuggestionsQuery | | `apps/client/src/features/acadenice/wikilinks/extension/wikilink-list.module.css` | Styles popup | | `apps/client/src/features/acadenice/wikilinks/__tests__/wikilink-extension.test.ts` | 9 tests Vitest (schema/attrs/commands/HTML) | | `apps/client/src/features/acadenice/backlinks/queries/backlinks-query.ts` | useBacklinks(pageId) React Query (staleTime 30s) | | `apps/client/src/features/acadenice/backlinks/components/linked-references-panel.tsx` | Panel accordeon groupe par link_type, excerpt, navigate source, empty/loading/error states | | `apps/client/src/features/acadenice/backlinks/components/linked-references-panel.module.css` | Styles panel | | `apps/client/src/features/acadenice/backlinks/__tests__/linked-references-panel.test.tsx` | 7 tests Vitest (RTL) | ### Fichiers modifies (patches upstream minimaux) | Fichier | Modification | |---------|--------------| | `apps/server/src/collaboration/extensions/persistence.extension.ts` | +import EventEmitter2 + ACADENICE_PAGE_CONTENT_UPDATED_EVENT, +inject eventEmitter, +emit apres enqueuePageHistory | | `apps/server/src/core/core.module.ts` | +import AcadeniceBacklinksModule, +1 entree imports[] | | `apps/client/src/features/editor/extensions/extensions.ts` | +import WikilinkExtension, +1 entree mainExtensions[] | | `apps/client/src/features/editor/full-editor.tsx` | +import LinkedReferencesPanel, +render Divider + panel apres MemoizedPageEditor | | `apps/client/public/locales/en-US/translation.json` | +11 cles (backlinks.* + wikilink.*) | | `apps/client/public/locales/fr-FR/translation.json` | +11 cles FR | ### Choix techniques tranches | Choix | Decision | |-------|----------| | Wikilink syntax | [[Title]] et [[Title\|alias]] supportes (Obsidian style) | | Resolution wikilink | LIKE case-insensitive exact sur `pages.title` dans le workspace. Ambiguite -> null (broken link). Pas de fuzzy. | | Reindex strategy | Full reindex par page (delete-then-insert), idempotent. OK jusqu'a 10k pages. | | Excerpt | 100 chars avant + 100 chars apres l'occurrence du titre. | | Event debounce | Pas de debounce cote listener — Hocuspocus debounce deja la persistance en amont. Fire-and-forget. | | Placement panel | Sticky bottom de la page (apres l'editeur dans full-editor.tsx), Divider de separation. | ### Tests count - Backend: +23 tests Vitest (4 suites spec/) - Frontend: +16 tests Vitest (wikilink-extension + linked-references-panel) - Total cumule fork: 96 (R3.1.d) + 39 nouveaux = 135 tests ### Nouvelles dependances a installer (PAS installee — convention fork) Aucune nouvelle dep pour R3.2. Le Tiptap Suggestion est fourni par `@tiptap/suggestion` (deja en dependance de Docmost pour le systeme mention). floating-ui deja present. ### Verifications skipped (convention fork — Corentin run) - `pnpm install` : non execute - `pnpm typecheck` : non execute - `pnpm test` : non execute - `pnpm migration:up` : non execute (migration lisible et reversible, a tester) - Lint : non execute ### Points a debattre avec Corentin 1. **Placement panel backlinks** : actuellement ajoute en bas de `full-editor.tsx` (dans le conteneur principal de la page). Alternatives : sidebar dedicee (panneau right) ou drawer Mantine. Deplacer sans changer la logique — LinkedReferencesPanel prend juste `pageId`. 2. **Resolution wikilink par pageId** : si l'utilisateur tape `[[Mon titre]]` et que le titre change plus tard, le lien devient broken. L'indexer re-resoud au prochain save de la page source. Pour stocker l'ID resolus des la saisie, il faudrait que la suggestion popup injecte `pageId` directement dans l'attr — c'est deja le cas (la selection dans WikilinkList passe `pageId`). Seuls les wikilinks saisis a la main sans passer par la popup auront `pageId=null`. 3. **Wikilink navigation** : le click navigue vers `/page/${pageId}`. DocAdenice utilise des URLs `/${spaceSlug}/page/${slugId}`. Il faudra un lookup ou un redirect dans App.tsx pour les pageIds sans slugId. Alternative : stocker slugId dans l'attr wikilink au moment de la suggestion. 4. **Index initial** : les pages existantes n'ont pas de backlinks indices. Un script one-shot `BacklinkIndexerService.reindexPage(pageId)` sur toutes les pages peut etre declenche manuellement ou via un endpoint admin `POST /api/acadenice/admin/backlinks/reindex`. ### TODO non bloquants - Script/endpoint de reindex massif initial (pages pre-R3.2) - data-testid sur LinkedReferencesPanel pour e2e Playwright (R3.1.e pattern) - CSS global pour `.wikilink` et `.wikilink--broken` (actuellement inline dans NodeView) - Pagination des backlinks si > 100 (rare mais possible) - Endpoint `DELETE /api/acadenice/admin/backlinks/reindex` pour repartir de zero --- ## Patch 010 — R3.3 : Custom slash commands dynamiques **Date** : 2026-05-08 **Commit** : `4e2af88` **Scope** : Workspace admins peuvent declarer leurs propres commandes `/keyword` sans recompile. 5 action types. Nouvelle permission `slash_commands:manage` (catalogue 23 perms). **Rationale** : Un workspace Notion-like doit permettre l'extensibilite du menu slash sans toucher au code. R3.3 livre un systeme complet admin-UI + runtime editor + securite webhook. ### Table SQL `acadenice_slash_command` (prefixe acadenice_, zero conflit upstream) : - `id` UUID PK, `workspace_id` FK CASCADE, `keyword` VARCHAR(50), `label` VARCHAR(100) - `action_type` VARCHAR(20) CHECK IN ('insert-template','insert-table','embed-url','run-webhook','insert-snippet') - `action_config` JSONB NOT NULL — payload specifique par action_type - `is_enabled` BOOLEAN DEFAULT true, `created_by` FK users RESTRICT - UNIQUE(workspace_id, keyword), INDEX idx_slash_workspace - Migration up + down idempotente (ifNotExists) ### Fichiers crees (backend) | Fichier | Role | |---------|------| | `apps/server/src/database/migrations/20260508T120000-create-acadenice-slash-command.ts` | Migration Kysely up+down | | `apps/server/src/core/acadenice/slash-commands/slash-commands.module.ts` | Module NestJS | | `apps/server/src/core/acadenice/slash-commands/dto/slash-command.dto.ts` | Zod schemas (discriminated union par action_type) + types TS | | `apps/server/src/core/acadenice/slash-commands/services/action-validator.service.ts` | Validation JSONB config per action_type + webhook allowlist | | `apps/server/src/core/acadenice/slash-commands/services/slash-command.service.ts` | CRUD : list/get/create/update/delete/toggle | | `apps/server/src/core/acadenice/slash-commands/controllers/slash-commands.controller.ts` | GET (public auth) + POST/PATCH/DELETE (requires slash_commands:manage) | | `apps/server/src/core/acadenice/slash-commands/spec/action-validator.service.spec.ts` | 13 tests Vitest (tous action types + allowlist) | | `apps/server/src/core/acadenice/slash-commands/spec/slash-command.service.spec.ts` | 10 tests Vitest (CRUD + ConflictException + toggle) | | `apps/server/src/core/acadenice/slash-commands/spec/slash-commands.controller.spec.ts` | 6 tests Vitest (routing + propagation exceptions) | ### Fichiers crees (frontend admin) | Fichier | Role | |---------|------| | `apps/client/src/features/acadenice/slash-commands-admin/services/slash-commands-client.ts` | axios wrapper (list/get/create/update/delete/toggle) | | `apps/client/src/features/acadenice/slash-commands-admin/queries/slash-commands-query.ts` | React Query hooks (list + CRUD mutations + optimistic toggle) | | `apps/client/src/features/acadenice/slash-commands-admin/components/slash-command-list.tsx` | Table Mantine + toggle switch + edit/delete buttons | | `apps/client/src/features/acadenice/slash-commands-admin/components/slash-command-form.tsx` | Modal create/edit polymorphe par action_type (form Mantine + validation) | | `apps/client/src/features/acadenice/slash-commands-admin/pages/slash-commands-page.tsx` | Page `/settings/slash-commands` (admin seulement) | | `apps/client/src/features/acadenice/slash-commands-admin/__tests__/slash-command-list.test.tsx` | 5 tests Vitest+RTL | | `apps/client/src/features/acadenice/slash-commands-admin/__tests__/slash-commands-page.test.tsx` | 3 tests Vitest+RTL (access denied + admin view) | ### Fichiers crees (frontend runtime editor) | Fichier | Role | |---------|------| | `apps/client/src/features/acadenice/slash-commands/hooks/use-custom-slash-commands.ts` | React Query hook (staleTime 2min, degradation graceful) | | `apps/client/src/features/acadenice/slash-commands/executor/actionExecutor.ts` | Dispatch action_type -> editor command ou webhook fetch | | `apps/client/src/features/acadenice/slash-commands/executor/buildCustomSlashItems.tsx` | Convertit SlashCommandDto[] -> SlashMenuItemType[] | | `apps/client/src/features/acadenice/slash-commands/__tests__/use-custom-slash-commands.test.ts` | 4 tests Vitest | | `apps/client/src/features/acadenice/slash-commands/__tests__/buildCustomSlashItems.test.ts` | 7 tests Vitest | ### Fichiers modifies (patches upstream minimaux) | Fichier | Modification | |---------|--------------| | `apps/server/src/core/acadenice/rbac/permissions-catalog.ts` | +1 permission `slash_commands:manage` (catalogue 23 perms) | | `apps/server/src/core/acadenice/rbac/services/seed.service.ts` | Admin role seed : +`slash_commands:manage` | | `apps/server/src/core/core.module.ts` | +import + +1 entree `AcadeniceSlashCommandsModule` | | `apps/client/src/features/editor/components/slash-menu/menu-items.ts` | `getSuggestionItems` accepte `customSlashItems?` — merged dans groupe 'acadenice' | | `apps/client/src/components/settings/settings-sidebar.tsx` | +`IconSlash` import + 1 entree "Slash commands" (admin only) | | `apps/client/src/App.tsx` | +import + +1 route `/settings/slash-commands` | | `apps/client/public/locales/en-US/translation.json` | +52 cles `slash_commands.*` | | `apps/client/public/locales/fr-FR/translation.json` | +52 cles `slash_commands.*` (traduction FR) | ### Securite webhook - URL HTTPS obligatoire (HTTP -> 400) - `ACADENICE_WEBHOOK_ALLOWLIST` env var : liste de prefixes autorises (optionnel, recommande en prod) - Timeout 10s via `AbortController` - `redirect: "error"` — aucun suivi de redirection - Body cap 1 MB — pas de streaming illimite - Auth headers (`Authorization`) NON transmis — les secrets doivent aller dans un proxy ### Extensibilite Le discriminated union Zod (`actionConfigSchema`) permet d'ajouter un nouvel action_type en 3 etapes : 1. Ajouter le schema Zod, 2. Ajouter le case dans `executeAction`, 3. Ajouter le formulaire dans `SlashCommandForm`. Pas de recompile cote base de donnees (JSONB flexible). ### Tests count - Backend : +29 tests Vitest (3 suites spec/) - Frontend : +19 tests Vitest (4 suites) - Total cumule fork : 135 (R3.2) + 48 nouveaux = **183 tests** ### Nouvelles dependances a installer Aucune. Toutes les dependances sont deja presentes dans le monorepo Docmost : - Zod (server) - @mantine/core, @mantine/form, @mantine/notifications (client) - @tanstack/react-query v5 (client) - axios (client) ### Variables d'env nouvelles | Var | Defaut | Role | |-----|--------|------| | `ACADENICE_WEBHOOK_ALLOWLIST` | (vide) | Prefixes URL autorises pour run-webhook, comma-separated. Si vide : tout HTTPS est accepte (avec log WARN). | ### Points a debattre avec Corentin 1. **customSlashItems integration dans SlashCommand extension** : `getSuggestionItems` accepte maintenant `customSlashItems?` mais la `SlashCommand` extension (slash-command.ts) appelle `getSuggestionItems` sans ce parametre. Pour que les custom commands apparaissent dans le menu runtime, il faut soit (a) passer `customSlashItems` via `editor.storage` (mis en place par un wrapper React autour de l'editeur qui appelle `useCustomSlashCommands`), soit (b) modifier la config Suggestion dans `extensions.ts` pour injecter le hook. Decision R3.4 au plus tard. 2. **Webhook en prod** : set `ACADENICE_WEBHOOK_ALLOWLIST` avec les prefixes autorises. Sans allowlist, tout HTTPS est accepte (convenable en dev, pas en prod). 3. **Icon resolution** : le champ `icon` stocke un string nom Tabler. Le runtime utilise `IconCommand` comme fallback universel. Une resolution dynamique par map (`{ IconNotes: IconNotes, ... }`) peut etre ajoutee dans `buildCustomSlashItems` si le besoin de customisation visuelle est fort. 4. **Pagination** : aucune pagination cote API (pas de `limit/offset`). Raisonnable jusqu'a ~200 commandes par workspace. ### TODO non bloquants - Integration complete dans l'extension Suggestion (point 1 ci-dessus) - Migration idempotente : la contrainte UNIQUE est appliquee via `ALTER TABLE ... ADD CONSTRAINT ... IF NOT EXISTS` avec un catch sur l'erreur si deja existante (certaines versions de Kysely ne supportent pas `ifNotExists` sur les contraintes) - Audit log creation/modification/suppression de commandes - data-testid sur les elements cles pour les tests e2e Playwright |