diff --git a/ACADENICE_PATCHES.md b/ACADENICE_PATCHES.md index 323aa6a0..eef4116a 100644 --- a/ACADENICE_PATCHES.md +++ b/ACADENICE_PATCHES.md @@ -683,3 +683,119 @@ En attendant, le rendu est identique fonctionnellement (HTML table + colonnes de - `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