diff --git a/ACADENICE_PATCHES.md b/ACADENICE_PATCHES.md index eef4116a..9d86e0be 100644 --- a/ACADENICE_PATCHES.md +++ b/ACADENICE_PATCHES.md @@ -799,3 +799,118 @@ Aucune nouvelle dep pour R3.2. Le Tiptap Suggestion est fourni par `@tiptap/sugg - 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