diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa4c04..49769a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog - FlowDeck +## v7.22.0 (2026-10-01) — Audit : A27 phase 2a (l'éditeur, 2 516 L) + +### Changed + +- **A27 (phase 2a)** — `_page_editor_scripts.html` : les **2 516 lignes** du + gros bloc interpolé partent vers `static/js/page_editor_scripts.js` +- Recette « config JSON » : les **8 interpolations Jinja** lisent maintenant + `PD = JSON.parse(document.getElementById('page-data'))` — le bloc JSON + `#page-data` **existait déjà** juste avant le script, même ordre + d'exécution, garde `__fdEditorScriptsLoaded` préservée +- Côté route (`view_page_root`) : `page_data` enrichi de `updated_at`, + `created_at`, `user_id`, `is_shared` (le dérivé est **hoisté** — une seule + expression sert le ctx ET le JSON) et `clip_icon` (macro `fd_icon` rendue + côté serveur). `workspace_key` reste vide comme avant (jamais défini dans + ce contexte → parité stricte) +- `node --check` vert ; template : 2 tags restants (JSON config + src) + +### Fixed + +- **8 tests adaptés** à l'extraction (ils lisaient le template source) : + `test_ai_writing` ×2 (+ helper `_read_js`), `test_pwa_offline`, + `test_v511` front_end_wired, `test_v55` ×3 (lightbox, previews, endpoints) + → lisent `static/js/page_editor_scripts.js` ; `test_page_editor_renders_ + page_is_shared` → **parsing du JSON `#page-data`** (`is_shared is True`, + la valeur sert toujours à la page) + +### Notes + +- Cumul A27 : **6 759 L extraites** (13 904 → 7 145 inline) +- Reste : local_workspace 2 031, base 1 523 (structurel {% for %}/{% block %}), + database_table 1 323 (if/else), settings 1 093, realtime 531, board 146 + ≈ 6 653 L + 120 warnings eslint à nettoyer +- Suite complète : **1089/1089** + ## v7.21.0 (2026-10-01) — Audit : A27 phase 1 (4 243 L de JS extraites) ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index c9427e4..c919231 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1147,7 +1147,7 @@ Quality DB views, Agent IA Palette → Realtime + E ### 🟡 P2 — Moyennes - [x] **A26 — Config piège** : (a) `FLOWDECK_STANDALONE` documenté dans `config.py:26` mais **jamais lu** (le champ réel est `STANDALONE`, pas de `env_prefix`, `extra="ignore"` avale la coquille) ; (b) `.env.example` documente `postgresql://…` mais `db_path` (`config.py:133`) retombe silencieusement sur SQLite ; (c) `db_path` fait `Path("/" + p)` (`config.py:132`) → pour `sqlite:////data/flowdeck.db` le résultat est le chemin UNC `\\data\flowdeck.db` (vérifié : inexistant) ; (d) `app_secret_key="change-me-to-random"` (`config.py:36`) + `gitea_oauth_client_secret="test-secret"` sans aucun garde au boot alors qu'il signe `flowdeck_session` (`session.py:14`). *Fix : 1 normalisation de `db_path` + `raise` au boot si secret par défaut + corriger le commentaire/`env_prefix`. Effort : **S**.* -- [x] **A27 — 13 900 lignes de JS inline — PARTIEL 2026-10-01 (4 243 extraites) dans 37 blocs**, ~3 800 livrées sur **chaque** page (`base.html` 1520 + `agent_panel` 1805 + `_icon_picker` 297 + `_header` 124 + `_notification_bell` 69), et **0 linté** : `eslint.config.mjs:50` ne couvre que `static/js/**/*.js` (soit `app.js` + `offline.js`), 2 blocs se neutralisent avec `/* eslint-disable */`. Grosseurs : `_page_editor_scripts` 2517, `local_workspace` 2030, `agent_panel` 1805, `base` 1520, `_database_table_scripts` 1323, `settings` 1093, `library` 1039. *Fix : extraire les gros partials vers `/static/js/*.js` (ils ne sont pas Jinja-interpolés) + ajouter les templates à eslint.* — **phase 1 faite 2026-10-01** : **7 templates sans Jinja → 9 fichiers `static/js/*.js` (4 243 lignes, -30 % du JS inline)** : agent_panel_1/2 (4 243… dont le 1 788 L livré sur CHAQUE page), library 1039, gitea_workspace 626, _icon_picker_1/2, _ctx_menu, import, workspaces — **un fichier par bloc** (ordre/timing identique, pas de defer, attributs conservés, `?v={{ asset_version }}`), `node --check` vert sur les 9, 0 script inline restant dans les cibles, suite 1089/1089. **Lint** : eslint **installé globalement** (`npm i -g eslint`, la config était déjà flat v9 sans dépendances) → `eslint static/js` = **0 erreur / 120 warnings** (no-unused-vars 69, no-empty 36, no-undef 15 — baseline à nettoyer opportunistiquement). **Reste A27** : les blocs **interpolés Jinja** (page_editor 2517, local_workspace 2031, base 1523, database_table 1323, settings 1093, realtime 531… ≈ 9 661 L) → extraction en 2 temps (config JSON injectée + script statique). Effort : **L**. +- [x] **A27 — 13 900 lignes de JS inline — PARTIEL 2026-10-01 (4 243 extraites) dans 37 blocs**, ~3 800 livrées sur **chaque** page (`base.html` 1520 + `agent_panel` 1805 + `_icon_picker` 297 + `_header` 124 + `_notification_bell` 69), et **0 linté** : `eslint.config.mjs:50` ne couvre que `static/js/**/*.js` (soit `app.js` + `offline.js`), 2 blocs se neutralisent avec `/* eslint-disable */`. Grosseurs : `_page_editor_scripts` 2517, `local_workspace` 2030, `agent_panel` 1805, `base` 1520, `_database_table_scripts` 1323, `settings` 1093, `library` 1039. *Fix : extraire les gros partials vers `/static/js/*.js` (ils ne sont pas Jinja-interpolés) + ajouter les templates à eslint.* — **phase 1 faite 2026-10-01** : **7 templates sans Jinja → 9 fichiers `static/js/*.js` (4 243 lignes, -30 % du JS inline)** : agent_panel_1/2 (4 243… dont le 1 788 L livré sur CHAQUE page), library 1039, gitea_workspace 626, _icon_picker_1/2, _ctx_menu, import, workspaces — **un fichier par bloc** (ordre/timing identique, pas de defer, attributs conservés, `?v={{ asset_version }}`), `node --check` vert sur les 9, 0 script inline restant dans les cibles, suite 1089/1089. **Lint** : eslint **installé globalement** (`npm i -g eslint`, la config était déjà flat v9 sans dépendances) → `eslint static/js` = **0 erreur / 120 warnings** (no-unused-vars 69, no-empty 36, no-undef 15 — baseline à nettoyer opportunistiquement). **Phase 2a faite 2026-10-01 (l'éditeur, le plus gros bloc interpolé)** : `_page_editor_scripts` **2 516 L extraites** vers `static/js/page_editor_scripts.js` — recette « config JSON » : les 8 interpolations Jinja lisent `PD` = `JSON.parse(#page-data)` (le bloc JSON **existait déjà** juste avant le script) ; la route enrichit `page_data` de `updated_at`, `created_at`, `user_id`, `is_shared` (**dérivé hoisté : une seule expression pour le ctx ET le JSON**) et `clip_icon` (macro `fd_icon` rendue côté serveur) ; `workspace_key` reste vide comme avant (jamais défini dans ce ctx — parité). **Cumul A27 : 6 759 L extraites** (13 904 → 7 145 inline). **8 tests adaptés** (ils lisaient le template source → lisent maintenant `static/js/page_editor_scripts.js` ; `pageIsShared:true` → parsing du JSON `#page-data` → `is_shared is True`). **Reste A27** : local_workspace 2031, base 1523 (structurel : {% for %}/{% block %}), database_table 1323 (if/else), settings 1093, realtime 531, board 146 ≈ 6 653 L + nettoyage des 120 warnings eslint. Effort : **L**. - [ ] **A28 — Dette de découpe (god files)** : `api_v2.py` 115 routes / 131 Ko, `dashboard.py` 63 / 116 Ko (27 pages HTMLResponse + 50 JSON + I/O fichiers, 16 `Environment(...)` locaux), `collections.py` 53 / 112 Ko, `board.py` 53 / 93 Ko (page CRUD + `zipfile` + sync Gitea). *Fix : scinder par **concern** (`pages_html`, `files`, sous-modules `api_v2/*`) — mécanique, 0 changement d'URL. Effort : **L**.* - [x] **A29 — Endpoints dupliqués 2-3×** : publish/unpublish existe en 3 endroits (`sharing.py:304/345`, `board.py:1020/1039`, `api_v2.py:1743/1761`) avec slug et auth **différents** ; listing collections ×3 (`/api/v1/collections`, `/db/api`, `/api/v2/collections`) ; `/api/users/me` ×2. *Fix : un `services/publish.py` partagé, les routers déléguent.* — **fait 2026-10-01** : `services/publish.py` (slugify unique, 404 partout, événements) ; les 3 paires publish/unpublish déléguent (sharing + board + v2), board gagne `_require_auth`, les bonus divergents (`share_mode='anyone'` / `is_shared=1`) supprimés — le share dialog reste propriétaire de ces drapeaux ; **byproduct sécurité** : `GET /api/users/me` (v1) et le contexte de `/accounts` faisaient `SELECT *` → `password_hash` exposé → colonnes whitelistées. **Décision** : `/api/users/me` ×2 et listing collections ×3 **restent** — contrats versionnés distincts (session+guest vs Bearer+scope, formes différentes). Effort : **M**. - [x] **A30 — 16 fonctions top-level jamais référencées**, dont `require_scope` (`api_v2_helpers.py:213`, la factory FastAPI qui doit faire les scopes — les handlers font `has_scope(...)` à la main), `validate_upload`, `_get_user_or_redirect`, `_require_user_gitea`, `unsync_block`, `find_referring`… *Fix : câbler `validate_upload` (A22) + `require_scope`, supprimer le reste. Effort : **S**.* @@ -1181,4 +1181,4 @@ Quality DB views, Agent IA Palette → Realtime + E → Puis **A3–A8** (le bloc « fallback admin ») d'un seul tenant, puis **A10** (autoescape) qui débloque A18/A20. *Audit produit le 2026-09-30 · 43 items · aucun code modifié ( ROADMAP seul ).* -→ **A1–A9 corrigés le 2026-09-30** : deps réinstallées (`pyotp`/`webauthn`/`cbor2`), rebinding de `settings` supprimé dans `test_v54.py` → **suite 1016/1016 verts**, cycle committé (`1706ad1`) + tag `v7.3.0` poussé, `.db`/fichiers de test désindexés, `APP_SECRET_KEY` roté dans `.env` (sessions révoquées) · **A3–A8 : 401 sans session sur les routes de compte (mdp actuel exigé), tokens `/api/v1` + `/api/user` sans session → 401, CRUD membres d'espace sous session+role admin, `_require_view`/`_require_edit` sans session → 404/401, création/lecture de page sous session, `/board/api/pages` + `/api/user` sortis du CSRF exempt, seed admin sans mdp en dur (aléatoire ou `FLOWDECK_ADMIN_PASSWORD`). Tests : client connecte par defaut (`_TestSessionAuth`), helper `anon()` sur les 40 tests d'anonymat → suite 1016/1016 + ruff OK, commit `d125eb3` · **A10 : `app/templating.py` (ENV partagé + autoescape `select_autoescape(["html"])`) remplace les 29 instantiations, `|safe` retriés (corps d'issue/commentaires echappes, `sidebar_config` en `|tojson`) → suite 1016/1016, version 7.3.1 · **A11 (traversal avatar) + A18 (vue publique : 404 restricted/private + html.escape)** : `tests/test_audit_p0_fixes.py`, suite 1019/1019, version 7.3.2 · **A12/A13/A15/A17/A22/A23/A24** : garde SSRF par hop, routers automations + /api sous session/Bearer, webhooks admin+URL publique, uploads validés, N+1 en GROUP BY/executemany, 2 doublons de routes supprimés → suite 1025/1025, version 7.3.3 · **A16** : exports + pièces jointes sous session + `can_view_page` → suite 1026/1026, version 7.3.4 · **A19 (partiel)** : 12 préfixes CSRF retirés après scan front (12 préfixes = tous ceux dont les appels portent déjà le header), reste 5 préfixes / 49 fetchs → suite 1026/1026, version 7.3.5 · **A19 terminé** : 46 call sites front équipés, plus aucun préfixe cookie-auth exempté → suite 1026/1026, version 7.3.6 · **A14** : fallback `admin` supprimé de `_current_user_id`/`_current_admin` (401 sans session), `api_base` validé scheme http(s) sans identifiants (hôtes privés acceptés = Ollama localhost, commentaire `ponytail:`) → suite 1027/1027, version 7.3.7 · **A25** : 84 `except Exception: pass` remplacés par `logger.exception(fn)` (19 fichiers, +`logger` manquants), try supprimé sur `materialize_properties` dans `create_collection_v2` et `apply_db_template_v2` (rollback au lieu d'un commit sans schéma), test de rollback · **A21 (partiel)** : `busy_timeout=5000` dans `get_conn()` → suite 1028/1028, version 7.3.8 · **A26/A33/A34/A35/A36/A43** : secret par défaut refusé au boot, rate limit (préfixes + settings + XFF + épurage), `_spawn()` pour les 10 schedulers, OpenAPI 511 chemins + README, 4 deps mortes purgées, 15 `utcnow()` → `now(UTC)` naïf → suite 1028/1028, version 7.3.9. · **A30/A37/A39/A40/A41** : `require_scope` câblé sur 69 sites + 12 fonctions mortes supprimées, CORS sans `*` (origines de `app_base_url` + regex dev/extensions), assets versionnés depuis `VERSION` (source unique), `app.css` -10,2 Ko de règles mortes, htmx = décision « rien » documentée → suite 1031/1031, version 7.4.0. · **A29/A42** : `services/publish.py` partagé (3 routers déléguent, 404 partout, board sous session), fuite `password_hash` corrigée sur `GET /api/users/me` v1 + contexte `/accounts`, `settings.data_dir` remplace les 9 copies d'env, cache Gitea évacue les expirés ; `/users/me` ×2 + collections ×3 = contrats versionnés, on garde ; reste A42 = client httpx partagé → suite 1034/1034, version 7.5.0. · **A31** : transaction par migration (`_apply_one`, rollback tout-ou-rien du DDL) + helper `columns()` remplaçant 25 copies de `PRAGMA table_info` (1 helper au lieu de 3 — les 2 autres seraient mort-nés) → suite 1036/1036, version 7.6.0. · **A20 (partiel)** : CSP nonce par requête — `unsafe-inline` retiré de `script-src`, 38 scripts templates + login constant + 3 scripts Python noncés, meta `htmx-config` pour htmx, `script-src-attr` pour les 74 `onclick=`, CDN chart/leaflet débloqués (déjà cassés avant) → suite 1037/1037, version 7.7.0. · **A21 phase 1** : 352 routes `async def` sans `await` → `def` (threadpool FastAPI, SQLite hors loop, zéro changement de logique — scan corps par corps) ; reste phase 2 = 311 routes avec `await` → `anyio.to_thread.run_sync` par bloc DB → suite 1037/1037, version 7.8.0. · **A21 phase 2a** : `body` JSON → paramètre `Body(default={})` sur les 36 routes api_v2 dont c'était le seul `await` → conversion en `def` → api_v2 à 96/115 hors loop (19 async restantes : fire_event/form/gitea) → suite 1037/1037, version 7.9.0. · **A21 phase 2b** : `run_event_sync()` (asyncio.run dans le worker, événement attendu = déterministe) + les 15 routes json/événements en `def` → api_v2 bouclé à 111/115 (4 async = vrais awaits réseau) ; repo-wide 403 sync / 260 async → suite 1037/1037, version 7.10.0. · **A32 phase 1** : les 4 routers à 0 test couverts (webhooks/notes/sidebar_config/github_routes = 10 smokes, 0 réseau réel, échappement notes vérifié) ; reste quasi nuls library/api/dashboard/api_v2 → suite 1047/1047, version 7.11.0. · **A32 phase 2a** : library 1/10 → 8 routes couvertes ; découverte = 2 routes lisant la table fantôme `local_workspace_items` (500 systématique, 0 ref front) supprimées + `_format_size` mort → suite 1053/1053, version 7.12.0. · **A32 phase 2b** : api.py 3 → 16/22 routes couvertes (board-config, col-mapping, card, collaborators stubbé, frontend-error dédup, mutations checklist vérifiées en base) ; reste 6 routes gitea + dashboard/api_v2 → suite 1059/1059, version 7.13.0. · **A32 phase 2c** : api_v2 scan strict → 5 routes à 0 ref couvertes (formula, rollup, audit-logs avec portail vérifié, webhooks/events, verify-signature signé) → suite 1064/1064, version 7.14.0. · **A32 phase 2d** : dashboard scan strict = 44 routes à 0 ref, 10 couvertes (tags CRUD, page content→rename→trash vérifiés en base, tree HTML, avatar-color restauré, members) → suite 1069/1069, version 7.15.0. · **A32 phase 2e** : +9 routes dashboard (pages comptes sans hash, profile/password A3 403, token fd_+64hex, forge, settings/account 400, select cookie, breadcrumb) → suite 1075/1075, version 7.16.0. · **A32 phase 2f** : +7 routes dashboard centrées A16 (files traversal 403, download/file-content sans fuite, avatar 302 sans réseau, table-data + création de ligne) → suite 1079/1079, version 7.17.0. · **A32 phase 2g** : +13 routes dashboard (gitea-workspace HTML, projects CRUD, cycle items 5 routes, cycle tags d'item 5 routes avec ws dédié) → suite 1083/1083, version 7.18.0. · **A32 phase 2h → dashboard bloqué** : members (cycle complet + quirk tuple), upload-folder (validations seules), convert-to-database (vérifié en base, ordre FK) ; les 44 routes à 0 ref sont toutes exercées (faux positifs f-string rapprochés) → suite 1086/1086, version 7.19.0. · **A32 TERMINÉ** : 6 routes Gitea stubbées (canevas mutable, carte board, HTML `?format=html`, 404) + **bug prod `card_detail.html`/`fd_icon` corrigé** (500 garanti avant) → suite 1089/1089, version 7.20.0. · **A27 phase 1** : 7 templates sans Jinja → 9 fichiers static/js (4 243 L, -30 % du inline, node --check vert), eslint installé globalement → `eslint static/js` 0 erreur/120 warnings ; reste = blocs interpolés Jinja (~9 661 L) → suite 1089/1089, version 7.21.0. +→ **A1–A9 corrigés le 2026-09-30** : deps réinstallées (`pyotp`/`webauthn`/`cbor2`), rebinding de `settings` supprimé dans `test_v54.py` → **suite 1016/1016 verts**, cycle committé (`1706ad1`) + tag `v7.3.0` poussé, `.db`/fichiers de test désindexés, `APP_SECRET_KEY` roté dans `.env` (sessions révoquées) · **A3–A8 : 401 sans session sur les routes de compte (mdp actuel exigé), tokens `/api/v1` + `/api/user` sans session → 401, CRUD membres d'espace sous session+role admin, `_require_view`/`_require_edit` sans session → 404/401, création/lecture de page sous session, `/board/api/pages` + `/api/user` sortis du CSRF exempt, seed admin sans mdp en dur (aléatoire ou `FLOWDECK_ADMIN_PASSWORD`). Tests : client connecte par defaut (`_TestSessionAuth`), helper `anon()` sur les 40 tests d'anonymat → suite 1016/1016 + ruff OK, commit `d125eb3` · **A10 : `app/templating.py` (ENV partagé + autoescape `select_autoescape(["html"])`) remplace les 29 instantiations, `|safe` retriés (corps d'issue/commentaires echappes, `sidebar_config` en `|tojson`) → suite 1016/1016, version 7.3.1 · **A11 (traversal avatar) + A18 (vue publique : 404 restricted/private + html.escape)** : `tests/test_audit_p0_fixes.py`, suite 1019/1019, version 7.3.2 · **A12/A13/A15/A17/A22/A23/A24** : garde SSRF par hop, routers automations + /api sous session/Bearer, webhooks admin+URL publique, uploads validés, N+1 en GROUP BY/executemany, 2 doublons de routes supprimés → suite 1025/1025, version 7.3.3 · **A16** : exports + pièces jointes sous session + `can_view_page` → suite 1026/1026, version 7.3.4 · **A19 (partiel)** : 12 préfixes CSRF retirés après scan front (12 préfixes = tous ceux dont les appels portent déjà le header), reste 5 préfixes / 49 fetchs → suite 1026/1026, version 7.3.5 · **A19 terminé** : 46 call sites front équipés, plus aucun préfixe cookie-auth exempté → suite 1026/1026, version 7.3.6 · **A14** : fallback `admin` supprimé de `_current_user_id`/`_current_admin` (401 sans session), `api_base` validé scheme http(s) sans identifiants (hôtes privés acceptés = Ollama localhost, commentaire `ponytail:`) → suite 1027/1027, version 7.3.7 · **A25** : 84 `except Exception: pass` remplacés par `logger.exception(fn)` (19 fichiers, +`logger` manquants), try supprimé sur `materialize_properties` dans `create_collection_v2` et `apply_db_template_v2` (rollback au lieu d'un commit sans schéma), test de rollback · **A21 (partiel)** : `busy_timeout=5000` dans `get_conn()` → suite 1028/1028, version 7.3.8 · **A26/A33/A34/A35/A36/A43** : secret par défaut refusé au boot, rate limit (préfixes + settings + XFF + épurage), `_spawn()` pour les 10 schedulers, OpenAPI 511 chemins + README, 4 deps mortes purgées, 15 `utcnow()` → `now(UTC)` naïf → suite 1028/1028, version 7.3.9. · **A30/A37/A39/A40/A41** : `require_scope` câblé sur 69 sites + 12 fonctions mortes supprimées, CORS sans `*` (origines de `app_base_url` + regex dev/extensions), assets versionnés depuis `VERSION` (source unique), `app.css` -10,2 Ko de règles mortes, htmx = décision « rien » documentée → suite 1031/1031, version 7.4.0. · **A29/A42** : `services/publish.py` partagé (3 routers déléguent, 404 partout, board sous session), fuite `password_hash` corrigée sur `GET /api/users/me` v1 + contexte `/accounts`, `settings.data_dir` remplace les 9 copies d'env, cache Gitea évacue les expirés ; `/users/me` ×2 + collections ×3 = contrats versionnés, on garde ; reste A42 = client httpx partagé → suite 1034/1034, version 7.5.0. · **A31** : transaction par migration (`_apply_one`, rollback tout-ou-rien du DDL) + helper `columns()` remplaçant 25 copies de `PRAGMA table_info` (1 helper au lieu de 3 — les 2 autres seraient mort-nés) → suite 1036/1036, version 7.6.0. · **A20 (partiel)** : CSP nonce par requête — `unsafe-inline` retiré de `script-src`, 38 scripts templates + login constant + 3 scripts Python noncés, meta `htmx-config` pour htmx, `script-src-attr` pour les 74 `onclick=`, CDN chart/leaflet débloqués (déjà cassés avant) → suite 1037/1037, version 7.7.0. · **A21 phase 1** : 352 routes `async def` sans `await` → `def` (threadpool FastAPI, SQLite hors loop, zéro changement de logique — scan corps par corps) ; reste phase 2 = 311 routes avec `await` → `anyio.to_thread.run_sync` par bloc DB → suite 1037/1037, version 7.8.0. · **A21 phase 2a** : `body` JSON → paramètre `Body(default={})` sur les 36 routes api_v2 dont c'était le seul `await` → conversion en `def` → api_v2 à 96/115 hors loop (19 async restantes : fire_event/form/gitea) → suite 1037/1037, version 7.9.0. · **A21 phase 2b** : `run_event_sync()` (asyncio.run dans le worker, événement attendu = déterministe) + les 15 routes json/événements en `def` → api_v2 bouclé à 111/115 (4 async = vrais awaits réseau) ; repo-wide 403 sync / 260 async → suite 1037/1037, version 7.10.0. · **A32 phase 1** : les 4 routers à 0 test couverts (webhooks/notes/sidebar_config/github_routes = 10 smokes, 0 réseau réel, échappement notes vérifié) ; reste quasi nuls library/api/dashboard/api_v2 → suite 1047/1047, version 7.11.0. · **A32 phase 2a** : library 1/10 → 8 routes couvertes ; découverte = 2 routes lisant la table fantôme `local_workspace_items` (500 systématique, 0 ref front) supprimées + `_format_size` mort → suite 1053/1053, version 7.12.0. · **A32 phase 2b** : api.py 3 → 16/22 routes couvertes (board-config, col-mapping, card, collaborators stubbé, frontend-error dédup, mutations checklist vérifiées en base) ; reste 6 routes gitea + dashboard/api_v2 → suite 1059/1059, version 7.13.0. · **A32 phase 2c** : api_v2 scan strict → 5 routes à 0 ref couvertes (formula, rollup, audit-logs avec portail vérifié, webhooks/events, verify-signature signé) → suite 1064/1064, version 7.14.0. · **A32 phase 2d** : dashboard scan strict = 44 routes à 0 ref, 10 couvertes (tags CRUD, page content→rename→trash vérifiés en base, tree HTML, avatar-color restauré, members) → suite 1069/1069, version 7.15.0. · **A32 phase 2e** : +9 routes dashboard (pages comptes sans hash, profile/password A3 403, token fd_+64hex, forge, settings/account 400, select cookie, breadcrumb) → suite 1075/1075, version 7.16.0. · **A32 phase 2f** : +7 routes dashboard centrées A16 (files traversal 403, download/file-content sans fuite, avatar 302 sans réseau, table-data + création de ligne) → suite 1079/1079, version 7.17.0. · **A32 phase 2g** : +13 routes dashboard (gitea-workspace HTML, projects CRUD, cycle items 5 routes, cycle tags d'item 5 routes avec ws dédié) → suite 1083/1083, version 7.18.0. · **A32 phase 2h → dashboard bloqué** : members (cycle complet + quirk tuple), upload-folder (validations seules), convert-to-database (vérifié en base, ordre FK) ; les 44 routes à 0 ref sont toutes exercées (faux positifs f-string rapprochés) → suite 1086/1086, version 7.19.0. · **A32 TERMINÉ** : 6 routes Gitea stubbées (canevas mutable, carte board, HTML `?format=html`, 404) + **bug prod `card_detail.html`/`fd_icon` corrigé** (500 garanti avant) → suite 1089/1089, version 7.20.0. · **A27 phase 1** : 7 templates sans Jinja → 9 fichiers static/js (4 243 L, -30 % du inline, node --check vert), eslint installé globalement → `eslint static/js` 0 erreur/120 warnings ; reste = blocs interpolés Jinja (~9 661 L) → suite 1089/1089, version 7.21.0. · **A27 phase 2a** : éditeur 2 516 L extrait via `#page-data` (PD.*), dérivé `is_shared` hoisté, 8 tests adaptés (source → static js / parsing JSON) → suite 1089/1089, version 7.22.0. diff --git a/VERSION b/VERSION index a29ba3d..696eaac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.21.0 +7.22.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index 2f1c930..5dcad62 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.21.0 (audit — A27 partiel : 4 243 L extraites, 0 erreur eslint) | **Statut**: EN COURS 🔄 +> **Début**: 2026-07-08 | **Version**: v7.22.0 (audit — A27 phase 2 : éditeur 2 516 L via page-data JSON) | **Statut**: EN COURS 🔄 > **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0` ## Avancement Global diff --git a/app/main.py b/app/main.py index 9078ab0..7116076 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.21.0", + version="7.22.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index a59bc77..d4b2f2b 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -413,17 +413,36 @@ def view_page_root(request: Request, page_id: int): with get_conn() as conn: nav_crumbs = _nav_breadcrumb(conn, page_id) + # dérivé calculé UNE fois : ctx ET page_data (JSON) l'utilisent (A27) + page_is_shared = ( + bool(page.get("is_shared", 0)) + or page.get("share_mode", "private") != "private" + or bool(page.get("published", 0)) + ) ctx = {**sidebar, "page": page, "sub_pages": [dict(s) for s in subs], "page_favorited": fav is not None, "page_share_mode": page.get("share_mode", "private"), "page_published": bool(page.get("published", 0)), - "page_is_shared": bool(page.get("is_shared", 0)) or page.get("share_mode", "private") != "private" or bool(page.get("published", 0)), + "page_is_shared": page_is_shared, "page_data": page_data, "collection_data": collection_data, "breadcrumb_items": nav_crumbs, "nav_workspace_id": page.get("workspace_id") or 0, "nav_page_id": page_id, "embed_mode": embed} + # A27 phase 2 : le JS de l'éditeur lit ces valeurs dans page-data (JSON) + # au lieu des interpolations Jinja — une seule source, même calculs que le + # ctx ci-dessus. + from app.templating import ENV as _ENV27 + page_data.update( + updated_at=page.get("updated_at", ""), + created_at=page.get("created_at", ""), + user_id=_uid or 0, + is_shared=page_is_shared, + clip_icon=_ENV27.from_string( + "{% from '_icons.html' import fd_icon %}{{ fd_icon('paperclip', 14) }}" + ).render(), + ) # Select template: collection pages use database table view if page.get("content_format") == "collection" and not embed: template = env.get_template("page_editor_collection.html") diff --git a/app/templates/_page_editor_scripts.html b/app/templates/_page_editor_scripts.html index 0712784..63d2cdf 100644 --- a/app/templates/_page_editor_scripts.html +++ b/app/templates/_page_editor_scripts.html @@ -1,2519 +1,4 @@ - + diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 101a0aa..bb693df 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.21.0" + "version": "7.22.0" }, "paths": { "/auth/register": { diff --git a/static/js/page_editor_scripts.js b/static/js/page_editor_scripts.js new file mode 100644 index 0000000..b20c1cd --- /dev/null +++ b/static/js/page_editor_scripts.js @@ -0,0 +1,2516 @@ + +/* eslint-disable */ +if (!window.__fdEditorScriptsLoaded) { +window.__fdEditorScriptsLoaded = true; +const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.parse(el.textContent):{}}catch(e){return {}}})(); + const CMDS=[{name:'BASIC',items:[ + {id:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'}, + {id:'heading_3',name:'Heading 3',icon:'H3'},{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},{id:'numbered_list',name:'Numbered list',icon:'1.'}, + {id:'to_do',name:'To-do',icon:'☐'},{id:'toggle',name:'Toggle',icon:'▶'},{id:'quote',name:'Quote',icon:'❝'}, + {id:'callout',name:'Callout',icon:'💡'},{id:'table_of_contents',name:'Table of contents',icon:'☰'},{id:'math',name:'Math equation',icon:'∑'}, + {id:'columns',name:'Column list',icon:'▮'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'}, + {id:'table',name:'Table',icon:'⊞'},{id:'button',name:'Button / Automation',icon:'⚡'}, + ]},{name:'AI',items:[ + {id:'ai_write',name:'Write with AI',icon:'✨'},{id:'ai_summarize',name:'Summarize',icon:'📝'}, + {id:'ai_translate',name:'Translate',icon:'🌐'},{id:'ai_continue',name:'Continue writing',icon:'➡️'}, + ]}, + {name:'MEDIA',items:[ + {id:'image',name:'Image',icon:'🖼️'},{id:'embed',name:'Embed',icon:'🔗'}, + {id:'bookmark',name:'Bookmark',icon:'🔖'},{id:'video',name:'Video',icon:'🎬'}, + {id:'audio',name:'Audio',icon:'🎵'}, + ]}, + {name:'DATA',items:[{id:'database',name:'Database / Inline database',icon:'📊'}]}, + {name:'SYNCED',items:[{id:'synced',name:'Synced block',icon:'🔗'}]}, + {name:'ACTIONS',items:[{id:'duplicate',name:'Duplicate',icon:'▣'},{id:'turn_into',name:'Turn into…',icon:'↩'},{id:'copy_link',name:'Copy link to block',icon:'🔗'},{id:'delete',name:'Delete block',icon:'✕'}]}]; + + let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);} + // v5.13.1: guarantee every block (and nested child) has a stable id. + // Template blocks may be persisted without ids; leaving them undefined + // makes `data-bid="undefined"` collide and corrupts editing. + function ensureBlockIds(list){if(!Array.isArray(list))return list;list.forEach(function(b){if(b&&typeof b==='object'){if(!b.id)b.id=genId();if(Array.isArray(b.children))ensureBlockIds(b.children);}});return list;} + window.ensureBlockIds=ensureBlockIds; + function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;} + function gt(e){return (e&&e.innerText!==undefined)?e.innerText:(e?e.textContent:'');} + // v5.11.0: read a contenteditable block back to plain text, converting + // wiki chip nodes (contenteditable=false spans) back into their raw + // [[fdpage:ID]] / [[fddate:YYYY-MM-DD]] tokens. + function gtTok(e){ + if(!e) return ''; + if(!e.querySelector||!e.querySelector('.fd-wiki-chip,.fd-wiki-date')) return gt(e); + let out=''; + (function walk(node){ + for(const n of node.childNodes){ + if(n.nodeType===3){out+=n.nodeValue;continue;} + if(n.nodeType===1){ + if(n.classList&&n.classList.contains('fd-wiki-chip')){out+=n.getAttribute('data-token')||'';continue;} + if(n.classList&&n.classList.contains('fd-wiki-date')){out+=n.getAttribute('data-token')||'';continue;} + if(n.tagName==='BR'){out+='\n';continue;} + walk(n); + } + } + })(e); + return out; + } + window.gtTok=gtTok; + function splitCaret(e){ + const s=window.getSelection(); if(!s.rangeCount||!s.rangeCount)return null; const sel=s.getRangeAt(0); + const MK='\uF000FD'; const mark=document.createElement('span'); mark.textContent=MK; + sel.insertNode(mark); + const full=gtTok(e)||''; + const parts=full.split(MK); + mark.remove(); e.normalize(); + return {before:parts[0]||'',after:(parts.length>1?parts[1]:'')||'',content:full.split(MK).join('')}; + } + function ce(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(false);window.getSelection().removeAllRanges();window.getSelection().addRange(r);} + function cs(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(true);window.getSelection().removeAllRanges();window.getSelection().addRange(r);} + function esc(s){return s.replace(/&/g,'&').replace(//g,'>');} + // Escape then render inline markdown (bold/italic/code/links/strikethrough) + // so agent-generated content displays without raw `**`/backtick characters. + function mdEsc(s){ + if(!s) return ''; + return String(s).replace(/&/g,'&').replace(//g,'>') + .replace(/`([^`]+)`/g,'$1') + .replace(/\*\*([^*]+)\*\*/g,'$1') + .replace(/\*([^*]+)\*/g,'$1') + .replace(/~~([^~]+)~~/g,'$1') + .replace(/\[\[fdpage:(\d+)\]\]/g,function(m,pid){ + return '…';}) + .replace(/\[\[fddate:([0-9]{4}-[0-9]{2}-[0-9]{2})(?:T[0-9:]{5,8})?\]\]/g,function(m,iso){ + return ''+fdDateLabel(iso)+'';}) + .replace(/\[([^\]]+)\]\((https?:[^\)\s]+)\)/g,'$1') + .replace(/\n/g,'
'); + } + // v5.11.0: labels of [[fdpage:ID]] chips are resolved from the live DB + // (rename propagation — tokens store only the id). + function fdDateLabel(iso){ + try{ + const p=iso.split('-'); const d=new Date(+p[0],+p[1]-1,+p[2]); + const DOW=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; + const MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + return DOW[d.getDay()]+' '+(+p[2])+' '+MON[+p[1]-1]+' '+p[0]; + }catch(e){return iso;} + } + function _wikiResolve(root){ + const ct=root||document.getElementById('_blocksCt'); if(!ct)return; + const pids={}; + ct.querySelectorAll('.fd-wiki-chip[data-pid]').forEach(function(a){ + const lab=a.querySelector('.fd-wiki-label'); + if(!lab||lab.getAttribute('data-done'))return; + lab.setAttribute('data-done','1'); + pids[a.dataset.pid]=true; + }); + const ids=Object.keys(pids); if(!ids.length)return; + fetch('/board/api/wiki/titles?ids='+encodeURIComponent(ids.join(',')),{credentials:'same-origin'}) + .then(function(r){return r.json();}) + .then(function(d){ + const titles=d.titles||{}; + ct.querySelectorAll('.fd-wiki-chip[data-pid]').forEach(function(a){ + const t=titles[a.dataset.pid]; + const lab=a.querySelector('.fd-wiki-label'); + if(lab) lab.textContent = t||'Deleted page'; + }); + }).catch(function(){}); + } + window._wikiResolve=_wikiResolve; + + const CALLOUT_ICONS=['💡','💡','📌','⚠️','ℹ️','✅','❌','🚀','🔔','📣','⭐','❤️','🔥','💬','👀','🧠','🎯','🔑','📅','💰','💡','📚','🧪','🌍']; + + /* ═══════════ Simple Table block (v4.8.0) ═══════════ */ + function _tblSplitLine(s){s=(s||'').trim();if(s.charAt(0)==='|')s=s.slice(1);if(s.charAt(s.length-1)==='|')s=s.slice(0,-1);return s.split('|').map(function(x){return x.trim();});} + function _tblIsSep(s){s=(s||'').trim();if(!s||s.charAt(0)!=='|')return false;var cells=_tblSplitLine(s);if(!cells.length)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});} + function _tblIsData(s){s=(s||'').trim();return /^\|/.test(s)&&s.indexOf('|',1)>=0;} + function _tblNew(rows,cols){var out=[];for(var i=0;iw)index=w;b.rows.forEach(function(r){r.splice(index,0,'');});} + function _tblAddRow(b,index){_tblClone(b);var w=b.rows.length&&b.rows[0]?b.rows[0].length:1;if(index<0)index=0;if(index>b.rows.length)index=b.rows.length;b.rows.splice(index,0,_tblNew(1,w)[0]);} + function _tblDupCol(b,index){_tblClone(b);b.rows.forEach(function(r){var v=(indexr.length?r.length:index+1;r.splice(at,0,v);});} + function _tblDupRow(b,index){_tblClone(b);if(index<0||index>b.rows.length)return;b.rows.splice(index+1,0,(b.rows[index]||[]).slice());} + function _tblClearCol(b,index){_tblClone(b);b.rows.forEach(function(r){if(r&&index=0&&index=0&&index=0&&index';} + function _tblDefault(){return {rows:_tblNew(3,3)};} + function _tblFocus(bid,r,c){var sel=document.querySelector('.ftable-editor[data-bid="'+bid+'"]');if(!sel)return;var q='[data-c="'+c+'"]';var cell=sel.querySelector(q);if(!cell){var first=sel.querySelector('.ftable-cell[data-r]');if(first){first.focus();ce(first);}return;}cell.focus();if(r===undefined)cs(cell);else ce(cell);} + function renderTableBlock(b,idx){ + var rows=(b&&Array.isArray(b.rows)&&b.rows.length)?b.rows:_tblNew(2,2); + var w=0;rows.forEach(function(r){if(r&&r.length>w)w=r.length;});if(w<1)w=2; + var widths=(b&&Array.isArray(b.colsW))?b.colsW:[]; + var bid=b.id;var h=''; + var hasHeader=(b.has_header!==false);var firstCol=(!!b.first_col_header); + h+='
'; + h+='
'; + for(var c=0;c'+_tblGrip()+'
'; + } + h+='
'; + h+=''; + for(var ri=0;ri0&&firstCol);var tag='td'; + if(isHead){tag='th';} + h+=''; + for(var ci=0;ci0)?(' style="width:'+widths[ci]+'px"'):''; + h+='<'+tag+' class="ftable-cell'+(rcH?' head':'')+'" contenteditable="true" data-r="'+ri+'" data-c="'+ci+'"'+wpx+' spellcheck="false">'+esc(v); + if(ri===0&&hasHeader)h+=''; + h+=''; + } + h+=''; + } + h+='
'; + h+='
'; + h+='
'; + return h; + } + function _tblBlockById(bid){var e=window.E;if(!e||!e.blocks)return null;for(var i=0;i'+it[1]+''; + }); + d.innerHTML=html; + document.body.appendChild(d); + var r=d.getBoundingClientRect(); + x=Math.min(Math.max(8,x),window.innerWidth-r.width-8); + y=Math.min(Math.max(8,y),window.innerHeight-r.height-8); + d.style.left=x+'px';d.style.top=y+'px'; + window._tblCur={bid:bid,kind:kind,index:index}; + } + function _tblDo(action,cur){ + if(!cur)return; + var bid=cur.bid,index=cur.index,b=_tblBlockById(bid),e=window.E; + if(!b||!e)return; + e.sync(); + if(action!=='tbl-del')e.pushHistory(); + if(!(b.rows&&b.rows.length))b.rows=_tblNew(2,2); + var focusR=1,focusC=0; + if(action==='col-insert-left'){_tblAddCol(b,index);focusC=index;} + else if(action==='col-insert-right'){_tblAddCol(b,index+1);focusC=index+1;} + else if(action==='col-dup'){_tblDupCol(b,index);focusC=index+1;} + else if(action==='col-clear'){_tblClearCol(b,index);focusC=index;} + else if(action==='col-del'){_tblDelCol(b,index);focusC=Math.min(index,b.rows[0].length-1);} + else if(action==='row-insert-above'){_tblAddRow(b,index);focusR=index;} + else if(action==='row-insert-below'){_tblAddRow(b,index+1);focusR=index+1;} + else if(action==='row-dup'){_tblDupRow(b,index);focusR=index+1;} + else if(action==='row-clear'){_tblClearRow(b,index);focusR=index;} + else if(action==='row-del'){_tblDelRow(b,index);focusR=Math.min(index,b.rows.length-1);if(focusR<1)focusR=1;} + else if(action==='tbl-del'){_tblMenuClose();var bi=_tblIdxByBid(bid);if(bi>=0)e.removeBlock(bi);return;} + e.render();e.dirty=true;e.autoSave(); + _tblFocus(bid,focusR,focusC); + } + function _tblResizeStart(ev){ + var sp=ev.target&&ev.target.closest?ev.target.closest('.ftable-resize'):null;if(!sp)return; + var th=sp.parentElement;if(!th)return; + var wrap=th.closest('.ftable-editor');if(!wrap)return; + var bid=wrap.getAttribute('data-bid');var col=parseInt(sp.getAttribute('data-col')||'0',10); + var b=_tblBlockById(bid);if(!b)return; + ev.preventDefault();ev.stopPropagation(); + var E=window.E;if(E&&E.sync)E.sync(); + var widths=(b.colsW&&Array.isArray(b.colsW))?b.colsW.slice():[]; + while(widths.length<=col)widths.push(null); + var startX=ev.clientX;var base=widths[col]||th.getBoundingClientRect().width; + document.body.style.cursor='col-resize';document.body.style.userSelect='none'; + function move(me){var dx=me.clientX-startX;var nw=Math.max(48,Math.min(760,base+dx));th.style.width=Math.round(nw)+'px';widths[col]=Math.round(nw);} + function up(me){ + document.removeEventListener('mousemove',move);document.removeEventListener('mouseup',up); + document.body.style.cursor='';document.body.style.userSelect=''; + move(me); + b.colsW=widths; + if(E){E.dirty=true;E.autoSave();} + } + document.addEventListener('mousemove',move); + document.addEventListener('mouseup',up); + } + function _tblInitDelegate(){ + if(window._tblDelegate)return;window._tblDelegate=true; + document.addEventListener('click',function(ev){ + if(ev.target.closest&&ev.target.closest('#_tblMenu')){var it=ev.target.closest('.ftable-menu-item');if(it){var act=it.getAttribute('data-tbl-act');_tblDo(act,window._tblCur);}return;} + var g=ev.target.closest?ev.target.closest('[data-tbl]'):null; + if(g){var act=g.getAttribute('data-tbl'),bid=g.getAttribute('data-bid'),col=parseInt(g.getAttribute('data-col')||'0',10); + var gr=g.getBoundingClientRect?g.getBoundingClientRect():null; + if(act==='colmenu'){_tblMenuOpen(gr?gr.left:ev.clientX,(gr?gr.bottom:ev.clientY)+4,bid,'col',col);ev.preventDefault();} + else if(act==='colplus'){var b=_tblBlockById(bid);if(b){window.E.sync();_tblAddCol(b,col+1);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,col+1);}} + else if(act==='rowadd'){var b2=_tblBlockById(bid);if(b2){window.E.sync();var n=b2.rows.length;_tblAddRow(b2,n);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,n,0);}} + else if(act==='coladdright'){var b4=_tblBlockById(bid);if(b4){window.E.sync();var cw=(b4.rows&&b4.rows[0])?b4.rows[0].length:1;_tblAddCol(b4,cw);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,cw);}} + else if(act==='header'||act==='firstcol'){var b5=_tblBlockById(bid);if(b5){window.E.sync();window.E.pushHistory();if(act==='header')b5.has_header=g.checked?true:false;else b5.first_col_header=g.checked?true:false;window.E.render();window.E.dirty=true;window.E.autoSave();_rtSync();}} + return; + } + if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('[data-tbl]')))_tblMenuClose(); + }); + document.addEventListener('contextmenu',function(ev){ + var cell=ev.target.closest?ev.target.closest('.ftable-cell'):null; + if(!cell)return; + var w=cell.closest('.ftable-editor');if(!w)return; + var bid=w.getAttribute('data-bid'),r=parseInt(cell.getAttribute('data-r')||'0',10),c=parseInt(cell.getAttribute('data-c')||'0',10); + ev.preventDefault(); + if(r===0)_tblMenuOpen(ev.clientX,ev.clientY,bid,'col',c); + else _tblMenuOpen(ev.clientX,ev.clientY,bid,'row',r); + }); + document.addEventListener('mousedown',function(ev){ + if(ev.target&&ev.target.closest&&ev.target.closest('.ftable-resize')){_tblResizeStart(ev);return;} + if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('#_tblMenu')))_tblMenuClose(); + }); + } + + // ── Block type shortcuts (shown on the right of each slash item) ── + const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'•',numbered_list:'1.',to_do:'[ ]',toggle:'▸',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'———',code:'<>',table:'|',image:'[img]',embed:'[🔗]',bookmark:'[🔖]',video:'[🎬]',audio:'[🎵]',database:'DB',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'}; + const _GROUP_LABEL={BASIC:'Basic blocks',AI:'AI writing',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions'}; + // ── Hover preview card (matches Notion's visual hint) ── + function _previewHtml(blockId){ + const p=function(inner,cls){return '
'+inner+'
';}; + switch(blockId){ + case 'heading_1': return p('
Big section heading
'); + case 'heading_2': return p('
Our Values
• Ownership
• Altruism
• Integrity
Medium section heading
'); + case 'heading_3': return p('
Small section heading
Body text goes here…
'); + case 'heading_4': return p('
Tiny section heading
'); + case 'bulleted_list': return p('
• List item
• Another item
• Third item
'); + case 'numbered_list': return p('
1. First
2. Second
3. Third
'); + case 'to_do': return p('
☐ Todo item
☑ Checked todo
☐ Another
'); + case 'toggle': return p('
▸ Toggle content
'); + case 'quote': return p('
A blockquote to highlight a quotation.
'); + case 'callout': return p('
💡 A callout draws attention.
'); + case 'table_of_contents': return p('
On this page
· Heading 1
· Heading 2
· Heading 3
'); + case 'math': return p('
E = mc²
'); + case 'columns': return p('
col 1
col 2
'); + case 'divider': return p('
'); + case 'code': return p('
const x = 1;
'); + case 'table': return p('
AB
12
'); + case 'image': return p('
[Image]
'); + case 'embed': return p('
🔗 Embed (YouTube, Figma, Maps…)
'); + case 'bookmark': return p('
🔖 Bookmark card with preview
'); + case 'video': return p('
🎬 Inline video player
'); + case 'audio': return p('
🎵 Inline audio player
'); + case 'database': return p('
📊 Inline database table
'); + default: return p('
Text block
'); + } + } + + const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],_pv:null, + init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML=''; + const list=document.createElement('div');list.className='sm-list';list.style.cssText='overflow-y:auto;flex:1;min-height:120px;max-height:360px;padding:6px'; + list.addEventListener('click',ev=>{const it=ev.target.closest('.slash-item');if(it){ev.preventDefault();SM.sel(it.dataset.sid);}}); + list.addEventListener('mouseover',ev=>{const it=ev.target.closest('.slash-item');if(it)SM._hover(it);}); + const footer=document.createElement('div');footer.className='sm-footer';footer.style.cssText='display:flex;align-items:center;justify-content:space-between;padding:7px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--text-dim);cursor:pointer'; + footer.innerHTML='⌘ Close menuesc'; + footer.onclick=()=>SM.close(); + const inp=document.createElement('input');inp.type='text';inp.placeholder='Type to search'; + inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box'; + inp.oninput=()=>SM.flt(); + e.appendChild(list);e.appendChild(footer);e.appendChild(inp); + SM._e=e;SM._in=inp;SM._li=list;SM._f=footer; + document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target))SM.close();}); + if(!SM._doc){ + SM._doc=true; + document.addEventListener('keydown',function(ev){ + if(!SM._o)return; + if(ev.metaKey||ev.ctrlKey||ev.altKey)return; + if(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'){ + ev.preventDefault();ev.stopPropagation();SM._docKd(ev); + } + }); + } + SM._docKd=function(ev){ + if(SM._o&&(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'))SM._kd(ev); + }; + }, + _hover(it){ if(!it) return; const id=it.dataset.sid; if(!id) return; + if(!SM._pv){ const d=document.createElement('div'); d.className='sm-preview'; d.style.cssText='position:fixed;z-index:1000;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);padding:12px;width:220px;pointer-events:none;'; document.body.appendChild(d); SM._pv=d; } + SM._pv.innerHTML=_previewHtml(id); + const r=SM._e.getBoundingClientRect(); const ir=it.getBoundingClientRect(); + let left = r.right + 8; let top = ir.top - 12; + if(left+220 > window.innerWidth-8) left = r.left - 228; + top = Math.max(8, Math.min(top, window.innerHeight - SM._pv.offsetHeight - 8)); + SM._pv.style.left=left+'px'; SM._pv.style.top=top+'px'; SM._pv.style.display='block'; + }, + open(i,el){if(!SM._e)SM.init();SM.close();SM._i=i;SM._s=0;SM._in.value='';SM.flt(); + const r=el.getBoundingClientRect();SM._e.style.display='flex'; + SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-360)?r.top-364:r.bottom+4)+'px'; + SM._e.style.left=Math.min(r.left,window.innerWidth-340)+'px'; + SM._rs();setTimeout(()=>SM._in.focus(),10);SM._o=true;}, + close(){if(SM._e)SM._e.style.display='none';SM._o=false;SM._in&&(SM._in.value='');if(SM._pv)SM._pv.style.display='none';}, + flt(){if(!SM._li)return;SM._it=[];const q=(SM._in.value||'').toLowerCase();SM._q=q;let h=''; + for(const g of CMDS){let items=g.items;if(q)items=items.filter(c=>c.name.toLowerCase().includes(q)||c.id.includes(q));if(!items.length)continue; + h+=`
${_GROUP_LABEL[g.name]||g.name}
`; + items.forEach(c=>{SM._it.push(c); + h+=`
+ ${c.icon} + ${c.name} + ${_SHORTCUT[c.id]||''}
`;});} + SM._li.innerHTML=h;SM._rs();}, + _kd(e){if(e.key==='ArrowDown'){e.preventDefault();SM._s=Math.min(SM._s+1,SM._it.length-1);SM._rs();} + else if(e.key==='ArrowUp'){e.preventDefault();SM._s=Math.max(SM._s-1,0);SM._rs();} + else if(e.key==='Enter'){e.preventDefault();const c=SM._it[SM._s];if(c)SM.sel(c.id);} + else if(e.key==='Escape'){e.preventDefault();SM.close();}}, + _rs(){const its=SM._li.querySelectorAll('.slash-item');its.forEach((el,i)=>{el.classList.toggle('selected',i===SM._s);});const cur=its[SM._s];if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'});}, + sel(id){if(window.E)window.E.applySlash(id);SM.close();}, + get isOpen(){return SM._o;}}; + + // ═══════════ v5.11.0 Wiki-link picker ([[ → page search) ═══════════ + const WM={_o:false,_idx:-1,_s:0,_it:[],_root:null,_in:null,_li:null,_pv:null, + mk(){ if(this._root)return this._root; + const d=document.createElement('div'); d.id='_wikiMenu'; + d.style.cssText='position:fixed;z-index:1250;display:none;width:340px;max-height:320px;flex-direction:column;background:var(--bg-modal,#1f1f1f);border:1px solid var(--border,#333);border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden'; + const list=document.createElement('div'); list.style.cssText='overflow-y:auto;flex:1;min-height:60px;max-height:250px;padding:6px'; + const inp=document.createElement('input'); inp.type='text'; inp.placeholder='Search pages…'; + inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box'; + d.appendChild(list); d.appendChild(inp); document.body.appendChild(d); + this._root=d; this._li=list; this._in=inp; + inp.oninput=()=>{clearTimeout(this._t);this._t=setTimeout(()=>this.load(),180);}; + list.addEventListener('click',ev=>{const it=ev.target.closest('.wm-item');if(it){ev.preventDefault();WM.sel(parseInt(it.dataset.wpid,10));}}); + document.addEventListener('mousedown',ev=>{if(!d.contains(ev.target))WM.close();}); + document.addEventListener('keydown',function(ev){ + if(!WM._o)return; + if(ev.metaKey||ev.ctrlKey||ev.altKey)return; + if(ev.key==='Escape'){ev.preventDefault();ev.stopPropagation();WM.close();} + else if(ev.key==='ArrowDown'){ev.preventDefault();ev.stopPropagation();WM._mv(1);} + else if(ev.key==='ArrowUp'){ev.preventDefault();ev.stopPropagation();WM._mv(-1);} + else if(ev.key==='Enter'){ev.preventDefault();ev.stopPropagation();const c=WM._it[WM._s];if(c)WM.sel(c.id);} + },true); + return d; }, + open(i,el){ this.mk(); const r=el.getBoundingClientRect(); + this._root.style.display='flex'; + this._root.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px'; + this._root.style.left=Math.min(r.left,window.innerWidth-350)+'px'; + this._idx=i; this._s=0; this._in.value=''; this._o=true; + this.load(); setTimeout(()=>this._in.focus(),10); }, + close(){ if(this._root)this._root.style.display='none'; this._o=false; }, + load(){ const self=this; const q=(this._in.value||'').toLowerCase(); + fetch('/board/api/wiki/pages?q='+encodeURIComponent(q),{credentials:'same-origin'}) + .then(r=>r.json()).then(d=>{ + if(!self._o)return; + self._it=d.pages||[]; self._s=0; + let h='
Pages
'; + self._it.forEach(function(p,idx){ + h+='
' + +''+esc(p.icon||'📄')+'' + +''+esc(p.title)+'' + +''+esc(p.workspace||'')+'
'; + }); + if(!self._it.length)h+='
No page found
'; + self._li.innerHTML=h; + }).catch(()=>{}); }, + _mv(dir){ if(!this._it.length)return; + this._s=(this._s+dir+this._it.length)%this._it.length; + this._li.querySelectorAll('.wm-item').forEach((el,i)=>{el.style.background=i===this._s?'rgba(35,131,226,.15)':'';}); + const cur=this._li.querySelectorAll('.wm-item')[this._s]; if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'}); }, + sel(pid){ const E=window.E; if(!E||this._idx<0)return; + const b=E.blocks[this._idx]; if(!b)return; + // Drop the typed "[[" from the live DOM text, append the token chip. + const el=E.getEl(b.id); let text=(el?gtTok(el):b.content)||''; + if(text.endsWith('[[')) text=text.slice(0,-2); + else text=text.replace(/\[\[[^\]]*$/,''); + E._commitBlockText(b, text+'[[fdpage:'+pid+']] '); + this.close(); }, + get isOpen(){return this._o;}}; + + // ═══════════ Inline AI composer (Notion AI-style) ═══════════ + // Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…" + // → framed result → "Insert below" → applyAIBlocks. + const AIC={_open:false,_idx:-1,_stage:'',_html:'', + ROOT:null, + mk(){ if(this.ROOT) return this.ROOT; + const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none'; + document.body.appendChild(d); this.ROOT=d; return d; }, + open(idx){ const self=this; this._idx=idx; this._stage='input'; this.mk(); + const r=this._anchorRect(idx); + const w=Math.max(r.width||0,280); this.ROOT.style.width=Math.min(w,1200)+'px'; + this.ROOT.style.left=Math.max(8,Math.min(r.left,window.innerWidth-r.width-16))+ 'px'; + this.ROOT.style.top=Math.max(8,(r.bottom+6>window.innerHeight-260)?r.top-180:r.bottom+6)+'px'; + this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest(); + document.addEventListener('mousedown',function onDoc(ev){ if(!self.ROOT.contains(ev.target)){ self.close(); document.removeEventListener('mousedown',onDoc); } }); + }, + close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; if(window.E)window.E.dirty=true; this._stage=''; }, + _anchorRect(idx){ const E=window.E; if(!E||!E.blocks)return{left:Math.max(24,window.innerWidth/2-210),top:window.innerHeight/2,bottom:window.innerHeight/2,width:420}; + const b=E.blocks[idx]; const el=E.getEl(b?b.id:null); if(el){const r=el.getBoundingClientRect();return r;} return {left:24,top:120,bottom:180,width:420}; }, + _focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} }, + _escTag(tag,s){return '<'+tag+'>'+s+'';}, + _mdToHtml(md){ // compact block render (headings, lists, bold, paragraphs) + const lines=String(md||'').split(/\r?\n/); let out=[]; let i=0; + const inline=function(t){return esc(t).replace(/`([^`]+)`/g,'$1').replace(/\*\*([^*]+)\*\*/g,'$1').replace(/\*([^*]+)\*/g,'$1').replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'$1');}; + while(i'+inline(s.replace(/^#+\s+/,''))+''); i++; continue; } + if(/^[-*+] /.test(s)){ const items=[]; while(i'+inline(lines[i].trim().replace(/^[-*+] /,''))+''); i++;} out.push('
    '+items.join('')+'
'); continue; } + if(/^\d+\. /.test(s)){ const items=[]; while(i'+inline(lines[i].trim().replace(/^\d+\. /,''))+''); i++;} out.push('
    '+items.join('')+'
'); continue; } + if(s.startsWith('> ')){ out.push('
'+inline(s.replace(/^> /,''))+'
'); i++; continue; } + out.push('

'+inline(s)+'

'); i++; + } + return out.join(''); }, + paint(){ const self=this; const h=this.ROOT; + if(this._stage==='input'){ + h.innerHTML='
' + +'✨' + +'' + +'' + +'
'; + const inp=h.querySelector('.aici-input'); if(inp){inp.focus(); const go=function(){const v=inp.value.trim(); if(v){self._idx=self._idx>=0?self._idx:0; self._run(v);} }; inp.onkeydown=function(ev){ if(ev.key==='Enter'){ev.preventDefault();go();} else if(ev.key==='Escape'){ev.preventDefault();self.close();} }; h.querySelector('.aici-send').onclick=go; } + } else if(this._stage==='brewing'){ + h.innerHTML='
' + +'✨Brewing…' + +'
'; + h.querySelector('.aici-stop').onclick=function(){ self.close(); if(window.fdAgent&&window.fdAgent.cancel)window.fdAgent.cancel(); }; + } else if(this._stage==='result'){ + h.innerHTML='
' + +'
'+this._html+'
' + +'
' + +'' + +'' + +'' + +'' + +'
'; + const self2=this; h.querySelector('.aici-insert').onclick=function(){ self2.insert(); }; h.querySelectorAll('.aici-fb').forEach(function(b){ b.onclick=function(){ self2.toast(b.dataset.fb==='up'?'👍 Merci':'👎 Enregistré'); }; }); + } + }, + _run(prompt){ const self=this; this._stage='brewing'; this.paint(); + const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):''; + if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; } + window.fdAgent.generate(prompt, ctx).then(function(text){ + self._html=self._mdToHtml(text||''); self._stage='result'; self.paint(); + }).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); }); + }, + _err(msg){ this._html='

⚠ '+esc(msg)+'

'; this._stage='result'; this.paint(); }, + insert(){ const E=window.E; if(!E||typeof E.applyAIBlocks!=='function')return; E.sync(); + const result=this._resultText(); if(!result)return; E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); }, + _resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result'); if(!el)return ''; const r=document.createRange();r.selectNodeContents(el);return r.toString(); }, + toast(m){ if(window.showToast)window.showToast(m,'info'); } + }; + window.AIC=AIC; + + // ═══════════ AI Autocomplete (v5.9.0) ═══════════ + // After a short idle while typing in a text block, ask the AI for a short + // continuation and surface it as a "Tab to accept" pill anchored to the + // block. Tab inserts the suggestion; Escape dismisses it. + const AIAC={_timer:null,_sug:'',_bid:null,_busy:false,_enabled:true,_req:0, + ROOT:null, + _allowed:function(b){ if(!b)return false; return ['paragraph','heading_1','heading_2','heading_3','heading_4','bulleted_list','numbered_list','quote','callout','to_do'].indexOf(b.type)>=0; }, + mk(){ if(this.ROOT)return this.ROOT; const d=document.createElement('div'); d.id='_aiAC'; d.style.cssText='position:fixed;z-index:1150;display:none;max-width:min(520px,90vw)'; document.body.appendChild(d); this.ROOT=d; + document.addEventListener('mousedown',function(ev){ if(AIAC.ROOT&&!AIAC.ROOT.contains(ev.target))AIAC.clear(); }); + return d; }, + schedule(bid){ if(!this._enabled)return; const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear(); + clearTimeout(this._timer); const self=this; this._timer=setTimeout(function(){ self.fetch(bid); },900); }, + fetch(bid){ const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear(); + const el=E.getEl(bid); const prefix=((el&&el.textContent)||b.content||'').trim(); if(prefix.length<8)return this.clear(); + const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):''; + const req=++this._req; this._busy=true; const self=this; const csrf=(E.csrfTok?E.csrfTok():''); + fetch('/api/agent/writing',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({action:'autocomplete',prefix:prefix,context:ctx})}) + .then(function(r){return r.json();}) + .then(function(d){ self._busy=false; if(req!==self._req)return; if(d&&d.ok&&d.text){ self.show(bid,d.text); } else { self.clear(); } }) + .catch(function(){ self._busy=false; if(req===self._req)self.clear(); }); + }, + show(bid,text){ text=(text||'').trim(); if(!text){return this.clear();} const E=window.E; const el=E&&E.getEl(bid); if(!el)return; + this._sug=text; this._bid=bid; this.mk(); const r=el.getBoundingClientRect(); + this.ROOT.innerHTML='
' + +'✨'+esc(text)+'' + +'Tab' + +'
'; + const btn=this.ROOT.querySelector('.aiac-x'); if(btn)btn.onclick=function(){ AIAC.clear(); }; + let left=Math.min(Math.max(8,r.left+24),window.innerWidth-360); let top=r.bottom+4; + if(top+40>window.innerHeight)top=r.top-36; + this.ROOT.style.left=left+'px'; this.ROOT.style.top=Math.max(8,top)+'px'; this.ROOT.style.display='block'; + }, + accept(){ if(!this._sug)return false; const E=window.E; const idx=E&&E.getIdx(this._bid); if(idx<0){ this.clear(); return false; } + const b=E.blocks[idx]; const el=E.getEl(this._bid); E.sync(); E.pushHistory(); + const cur=((el&&el.textContent)||b.content||''); const next=(cur+this._sug).replace(/\s+$/,' '); + b.content=next; if(el){ el.textContent=next; } this.clear(); E.dirty=true; E.autoSave(); if(window.__fdRT&&window.__fdRT.syncNow)window.__fdRT.syncNow(); + if(el){ el.focus(); ce(el); } return true; + }, + clear(){ this._sug=''; this._bid=null; this._req++; if(this.ROOT){ this.ROOT.style.display='none'; this.ROOT.innerHTML=''; } }, + get active(){ return !!this._sug; } + }; + window.AIAC=AIAC; + + function renderBlock(b,idx){ + if(b.type==='meeting')return renderMeetingBlock(b,idx); + if(b.type==='table')return renderTableBlock(b,idx); + if(b.type==='divider')return `

`; + if(b.type==='button')return renderAutomationButton(b,idx); + if(b.type==='image')return `
${esc(b.alt||'')}
`; + if(b.type==='video'){ + const src=b.src||'';return src?`
`:`
🎬
Click to add video URL
`; + } + if(b.type==='audio'){ + const src=b.src||'';return src?`
`:`
🎵
Click to add audio URL
`; + } + if(b.type==='bookmark'){ + var url=b.url||b.src||'';var title=esc(b.title||url);var desc=esc(b.description||'');var img=b.image||'';var site=esc(b.site_name||''); + var imgHtml=img?'':''; + var descHtml=desc?'
'+desc+'
':''; + var siteHtml=site?'
'+site+'
':''; + var safeUrl=url.replace(/"/g,'"'); + return url?``:`
🔖
Click to add bookmark URL
`; + } + if(b.type==='embed'){ + if(b.embed_type==='inline_dbs')return `
📊 Inline Databases
`; + if(b.embed_type==='collection')return `
Loading database...
<`+`script>setTimeout(function(){var el=document.getElementById('inline-db-${b.collection_id}');if(el&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,${b.collection_id},null);},50);<`+`/script>
`; + if(b.embed_type==='pdf')return `

Failed to load PDF

Open in new tab →
`; + if(b.embed_type==='download'){ + var sz=b.file_size||0;var szStr=sz<1024?sz+' B':sz<1048576?(sz/1024).toFixed(1)+' KB':(sz/1048576).toFixed(1)+' MB'; + return `
${PD.clip_icon||''}
${esc(b.file_name||'File')}
${szStr} · ${esc(b.file_mime||'')}
⬇ Download
`; + } + var url=b.src||'';if(url){ + var src=b.embed_src||url; + var height=b.height||520; + return `
`; + } + return `
🔗
Click to add embed URL
`; + } + if(b.type==='synced'){ + if(b._synced_deleted){ + // v6.5.0: source deleted → clear state instead of stale cache. + return `
+
🔗 Deleted synced block
+
The original synced block was deleted — this copy no longer updates.
+
`; + } + var syncedTitle=b._synced_title||'Synced block'; + var syncedContent=b._synced_content||[]; + var sid=b.synced_id||''; + var syncedInner=syncedContent.map(function(ch){return'
'+mdEsc(typeof ch==='object'?ch.content||'':ch)+'
';}).join(''); + return `
+
🔗 Synced — ${esc(syncedTitle)}
+
${syncedInner||'
Loading synced content…
'}
+
`; + } + const ph=b.type==='heading_1'?'Heading 1':b.type==='heading_2'?'Heading 2':b.type==='heading_3'?'Heading 3':b.type==='heading_4'?'Heading 4':b.type==='bulleted_list'?'List':b.type==='numbered_list'?'List':b.type==='to_do'?'To-do':b.type==='toggle'?'Toggle list':b.type==='quote'?'Empty quote':b.type==='code'?'Type code...':b.type==='callout'?'Type something...':(b.type==='paragraph'&&!b.content?'Press \'space\' for AI or \'/\' for commands':'Press \'/\' for commands...'); + let inner=''; + if(b.type==='to_do')inner=`
${mdEsc(b.content)}
`; + else if(b.type==='toggle')inner=`
${mdEsc(b.content)}
${(b.children||[]).map((ch)=>renderNestedBlock(ch)).join('')}
`; + else if(b.type==='code')inner=`
${b.language||'Plain Text'}
${mdEsc(b.content)}
`; + else if(b.type==='callout')inner=`${b.icon||'💡'}
${mdEsc(b.content)}
`; + else inner=`
${mdEsc(b.content)}
`; + const tag=b.type.startsWith('heading_')?'h'+b.type.slice(-1):b.type==='quote'?'blockquote':'div'; + const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`; + const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:''); + const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:''; + const actions=idx>=0?`
`:''; + return `
+
+ ${actions} + <${tag}${style}${bidAttr} class="${cls}">${inner} +
`; + } + + function renderTOC(b){ + var out=[]; + var blocks=window.E?window.E.blocks:[]; + var headings=blocks.filter(function(b){return /^heading_[1-4]$/.test(b.type)&&(b.content||'').trim();}); + if(headings.length===0){ + out.push(`
Add headings to generate a table of contents
`); + }else{ + out.push(`
On this page
`); + headings.forEach(function(h){ + var lvl=parseInt(h.type.slice(-1),10); + var id='h-'+ (h.id||'').replace(/[^a-zA-Z0-9]/g,''); + out.push(`
${esc(h.content)}
`); + }); + out.push(`
`); + } + if(blocks.length===0){} + return `
${out.join('')}
`; + } + + function renderMathBlock(b){ + var html=`
`; + html+=``; + return html; + } + + function renderAutomationButton(b,idx){ + var label=(b.content&&b.content.trim())?b.content:((b.automation_name||'Run automation')); + var bound=b.automation_id>0; + var badge=bound?'⚡ '+esc(b.automation_name||'')+'':'Choose…'; + var actions=idx>=0?`
`:''; + return `
+
+ ${actions} +
+
`; + } + + function renderColumnsBlock(b){ + var cols=(b.children&&b.children.length)?b.children:[{id:genId(),type:'paragraph',content:''}]; + var cells=cols.map(function(c){ + return `
${renderChildColumn(c,b.id)}
`; + }).join(''); + return `
${cells}
`; + } + + function renderChildColumn(c,parentId){ + return `
+
+
${esc(c.content)}
+
`; + } + + function renderNestedBlock(c){ + var ph=c.type==='heading_1'?'Heading 1':c.type==='heading_2'?'Heading 2':c.type==='heading_3'?'Heading 3':c.type==='heading_4'?'Heading 4':c.type==='bulleted_list'?'List':c.type==='numbered_list'?'List':c.type==='to_do'?'To-do':'Type...'; + return `
${esc(c.content)}
`; + } + + // ═══════════ AI Meeting Note — widget de réunion (v4.12.0) ═══════════ + // Bloc interactif « Notion-like » : consentement, transcription live (Web + // Speech API quand disponible), transcript horodaté + notes, puis résumé + // généré par le FlowDeck Agent. + function renderMeetingBlock(b, idx){ + var actions=idx>=0?`
`:''; + return `
+
+ ${actions} +
Préparation de la réunion…
+
`; + } + + function _fmFind(bid){var e=window.E;if(e&&e.blocks){for(var i=0;i${_fmEsc(st)}`:''; + var head=`
+ 🎙️ + + 📅 ${_fmEsc(m.date)} + ${pill} +
`; + + var body=''; + if(!m.started){ + body+=`
+
1 · Consentement des participants
+ + +
${audioOk?'Notion AI est prêt : la réunion sera transcrite en direct. Autorisez le micro lorsque le navigateur le demande.':'Ce navigateur ne supporte pas la transcription audio : vous pourrez saisir ou coller la transcription manuellement.'}
+ +
`; + }else{ + var tabs=`
+ + + +
`; + var ctrl=''; + if(m.status==='running'){ + ctrl=`
•••|ı|ı|ı|ı•••
+
+ + +
`; + }else if(m.status==='paused'){ + ctrl=`
+ + +
`; + }else if(m.status==='manual'){ + ctrl=`
`; + }else if(m.status==='done'){ + ctrl=`
`; + } + var panel=''; + if(m.lastTab==='notes'){ + panel=``; + }else if(m.lastTab==='transcript'){ + var lines=(m.lines||[]).map(function(l){return `
${_fmEsc(l.t||'')}${_fmEsc(l.who||'Vous')}${_fmEsc(l.text)}
`;}).join(''); + if(m.status==='running'){lines+=`
·Vous${_fmEsc(m.interim||'')}${(m.interim||'')?'':'…'}
`;} + var manual=m.status!=='running'?`
Transcript saisi ou collé (si aucune capture audio)
`:''; + panel=`
${lines||'
La transcription apparaîtra ici en direct. Enregistrez la réunion pour générer un transcript.
'}
${manual}
`; + }else{ + var summaryHtml=''; + if(m.summaryLoading){ + summaryHtml=`
Génération du résumé…
`; + }else if(m.summary){ + summaryHtml=` +
`; + }else if(m.summaryError){ + summaryHtml=`
${_fmEsc(m.summaryError)}
+ `; + }else{ + var has=!!(_fmTranscriptText(b).trim()); + if(m.status==='done'&&has){ + summaryHtml=` +
L'agent analyse le transcript et produit un aperçu structuré (sujets, décisions, actions).
`; + }else{ + summaryHtml=`
Arrêtez l'enregistrement (ou ajoutez un transcript) puis générez le résumé ici.
`; + } + } + panel=summaryHtml; + } + body=`
${ctrl}${tabs}${panel}
`; + } + return `
${head}${body}
`; + }, + onInput(bid,el){ + var b=_fmEnsure(bid);if(!b||!el)return;var m=b.meeting;var k=el.getAttribute&&el.getAttribute('data-fm'); + if(!k)return; + if(k==='title'){m.title=el.value||'';var tEl=document.getElementById('_titleEl');if(tEl&&!((tEl.textContent||'').trim())){tEl.textContent=m.title;if(window.E)window.E.pageTitle=m.title;}} + else if(k==='notes'){m.notes=el.value;} + else if(k==='manual'){m.manualTranscript=el.value;} + else if(k==='summary'){m.summary=el.value;} + else if(k==='consent'){m.consent=el.value;} + else return; + _fmSave(); + }, + act(bid,act,el){ + var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var self=this; + if(act==='start'){ + if(m.status==='done'||m.status==='idle'||m.status==='paused'){ + if(m.status==='done'){m.lines=[];m.summary='';m.summaryError='';m.manualTranscript='';} + m.started=true;m.lastTab='transcript'; + if(!self._audio()){m.status='manual';m.interim='';_fmSave();self.paint(bid);return;} + if(m.consent==='auto'&&window.speechSynthesis){ + var u=new SpeechSynthesisUtterance('Cette réunion est enregistrée et transcrite par FlowDeck. Merci de votre attention.'); + try{window.speechSynthesis.speak(u);}catch(e){} + } + setTimeout(function(){self._startRec(bid);},m.consent==='auto'?1300:0); + } + }else if(act==='pause'){ + this._stopRec(bid,false);m.status='paused';m.interim='';_fmSave();this.paint(bid); + }else if(act==='stop'){ + this._stopRec(bid,true);m.status='done';m.lastTab='transcript';m.interim='';_fmSave();this.paint(bid); + }else if(act==='new'){ + m.lines=[];m.summary='';m.summaryError='';m.status='idle';m.started=false;m.lastTab='transcript';_fmSave();this.paint(bid); + }else if(act==='tab'){ + m.lastTab=(el&&el.getAttribute&&el.getAttribute('data-tab'))||'transcript';_fmSave();this.paint(bid); + }else if(act==='generate'){ + this._generate(bid); + }else if(act==='copy'){ + var txt=(m.summary||'').trim(); + if(!txt)return; + if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt).catch(function(){});} + } + }, + _startRec(bid){ + var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var s=this._rt[bid]||(this._rt[bid]={}); + var SR=window.SpeechRecognition||window.webkitSpeechRecognition; + if(!SR){m.status='manual';_fmSave();this.paint(bid);return;} + if(s.rec){try{s.rec.stop();}catch(e){}} + var rec=new SR();s.rec=rec;s.offset=0;s.stopped=false; + rec.lang='fr-FR';rec.continuous=true;rec.interimResults=true;rec.maxAlternatives=1; + var self=this; + rec.onresult=function(ev){ + var final='',interim=''; + for(var i=(s.offset||0);i${_fmEsc(l.t||'')}${_fmEsc(l.who||'Vous')}${_fmEsc(l.text)}`;}).join('');return lines||'
La transcription apparaîtra ici en direct…
';}, + _generate(bid){ + var b=_fmEnsure(bid);if(!b)return;var m=b.meeting; + var content=_fmTranscriptText(b); + if(!content.trim()){m.summaryError='Aucun contenu de réunion à résumer.';_fmSave();this.paint(bid);return;} + m.summaryLoading=true;m.summaryError='';_fmSave();this.paint(bid); + var self=this; + var prompt='Rédige le résumé structuré de cette réunion (format AI Meeting Note) : une ligne d\'en-tête avec la date du '+_fmEsc(m.date)+', puis les sections « Résumé » (Overview), « Décisions » et « Action items » avec des puces concrètes. Si le contenu est trop court ou fragmentaire, indique « No meaningful content could be extracted ». Ne précède pas la réponse de commentaires.'; + if(!(window.fdAgent&&window.fdAgent.generate)){ + m.summaryLoading=false;m.summaryError='Agent IA indisponible — configurez-le dans Paramètres → Agent & IA.';_fmSave();this.paint(bid);return; + } + window.fdAgent.generate(prompt, content).then(function(text){ + m.summary=(text||'').trim();m.summaryLoading=false;m.summaryError='';_fmSave();self.paint(bid); + }).catch(function(err){ + m.summaryLoading=false;m.summaryError=(err&&err.message)?err.message:'Échec de génération du résumé';_fmSave();self.paint(bid); + }); + } + }; + window.FDMeeting=FDMeeting; + + // ═══════════ v5.10.0 — Interactions de bloc : sélection multiple, menu ⋮⋮, undo/redo ═══════════ + let _selSet=new Set(); + function _selHas(bid){return _selSet.has(bid);} + function _selToggle(bid){if(!bid)return;if(_selSet.has(bid))_selSet.delete(bid);else _selSet.add(bid); + document.querySelectorAll('.block-wrapper[data-id="'+bid+'"]').forEach(function(w){w.classList.toggle('selected',_selSet.has(bid));});} + function _selClear(){_selSet.clear();document.querySelectorAll('.block-wrapper.selected').forEach(function(w){w.classList.remove('selected');});} + function _selApply(){document.querySelectorAll('.block-wrapper[data-id]').forEach(function(w){var id=w.getAttribute('data-id');w.classList.toggle('selected',!!(_selSet.has(id)));});} + function _selIndicesSorted(){var E=window.E;var out=[];if(!E)return out;E.blocks.forEach(function(b,i){if(_selSet.has(b.id))out.push(i);});return out;} + function _rtSync(){try{var rt=window.__fdRT;if(rt&&rt.syncNow)rt.syncNow();}catch(e){}} + + const _BLOCK_TURN_TYPES=[ + {id:'paragraph',name:'Text'},{id:'heading_1',name:'Heading 1'},{id:'heading_2',name:'Heading 2'}, + {id:'heading_3',name:'Heading 3'},{id:'heading_4',name:'Heading 4'},{id:'bulleted_list',name:'Bulleted list'}, + {id:'numbered_list',name:'Numbered list'},{id:'to_do',name:'To-do'},{id:'toggle',name:'Toggle'}, + {id:'quote',name:'Quote'},{id:'callout',name:'Callout'},{id:'table',name:'Table'}, + {id:'math',name:'Math equation'},{id:'columns',name:'Column list'},{id:'button',name:'Button / Automation'}, + {id:'code',name:'Code block'} + ]; + const _BLOCK_COLORS_TEXT=['default','#E5484D','#F76B15','#FFC53D','#46A758','#3E63DD','#8E4EC6','#12A594','#94A3B8']; + const _BLOCK_COLORS_BG=['default','rgba(229,72,77,.15)','rgba(247,107,21,.15)','rgba(255,197,61,.15)','rgba(70,154,88,.15)','rgba(62,99,221,.15)','rgba(142,78,198,.15)','rgba(18,165,148,.15)','rgba(148,163,184,.2)']; + function _blockMenuClose(){var m=document.getElementById('blockMenu');if(m)m.remove();m=document.getElementById('blockMenuSub');if(m)m.remove();} + function _blockMenuClick(ev){ + var t=ev.target.closest&&ev.target.closest('[data-bm-act]');if(!t)return; + var act=t.getAttribute('data-bm-act');var bid=t.getAttribute('data-bid'); + if(!act)return; + ev.preventDefault();ev.stopPropagation(); + var E=window.E;if(!E)return; + var idx=bid?E.getIdx(bid):-1; + if(act==='turn'){var m=document.getElementById('blockMenu');if(m){var r=m.getBoundingClientRect();_blockTurnMenuOpen(r.right+4,r.top,bid||'');}return;} + if(act==='duplicate'){E.duplicateBlock(idx);} + else if(act==='copy-link'){E.copyBlockLink(bid||'');} + else if(act==='copy-blocks'){E.copySelectedBlocks();} + else if(act==='cut-blocks'){E.cutSelectedBlocks();} + else if(act==='paste-blocks'){E.pasteBlocks();} + else if(act==='move-to'){_blockMoveToOpen(idx);} + else if(act==='delete'){E.deleteSelected(idx);} + else if(act==='color'){var key=t.getAttribute('data-color-key'),val=t.getAttribute('data-color-val');E.setBlockColor(idx,key,val);} + else if(act==='turn-type'){E.turnInto(idx,t.getAttribute('data-type'));_blockMenuClose();} + _blockMenuClose(); + } + function blockMenuOpen(x,y,bid){ + _blockMenuClose(); + var E=window.E;if(!E)return; + var idx=E.getIdx(bid);if(idx<0)return; + var b=E.blocks[idx];if(!b)return; + var d=document.createElement('div');d.id='blockMenu';d.className='block-menu';d.style.cssText='position:fixed;z-index:4000;'; + var colorsText='',colorsBg=''; + _BLOCK_COLORS_TEXT.forEach(function(c){colorsText+='';}); + _BLOCK_COLORS_BG.forEach(function(c){colorsBg+='';}); + d.innerHTML='
↩Turn into▸
' + +'
' + +'
▣Duplicate '+(_rtMac()?'⌘D':'Ctrl+D')+'
' + +'
🔗Copy link to block
' + +'
' + +'
⎘Copy blocks '+(_rtMac()?'⌘C':'Ctrl+C')+'
' + +'
✂Cut blocks '+(_rtMac()?'⌘X':'Ctrl+X')+'
' + +'
⎘Paste blocks '+(_rtMac()?'⌘V':'Ctrl+V')+'
' + +'
' + +'
↪Move to…
' + +'
' + +'
Text
'+colorsText+'
' + +'
Background
'+colorsBg+'
' + +'
' + +'
✕Delete ⌫
'; + d.addEventListener('click',_blockMenuClick); + d.addEventListener('contextmenu',function(ev){ev.preventDefault();_blockMenuClose();}); + document.body.appendChild(d); + var r=d.getBoundingClientRect(); + x=Math.min(Math.max(8,x),window.innerWidth-r.width-8); + y=Math.min(Math.max(8,y),window.innerHeight-r.height-8); + d.style.left=x+'px';d.style.top=y+'px'; + } + function _blockTurnMenuOpen(x,y,bid){ + var m=document.getElementById('blockMenuSub');if(m)m.remove(); + var d=document.createElement('div');d.id='blockMenuSub';d.className='block-menu block-menu-sub';d.style.cssText='position:fixed;z-index:4001;'; + var html=''; + _BLOCK_TURN_TYPES.forEach(function(t){html+='
'+t.name+'
';}); + d.innerHTML=html; + d.addEventListener('click',_blockMenuClick); + document.body.appendChild(d); + var r=d.getBoundingClientRect(); + x=Math.min(Math.max(8,x),window.innerWidth-r.width-8); + y=Math.min(Math.max(8,y),window.innerHeight-r.height-8); + d.style.left=x+'px';d.style.top=y+'px'; + } + function _rtMac(){return /Mac|iPhone|iPad/.test(navigator.userAgent);} + function _blockMoveToOpen(idx){ + var E=window.E;if(!E)return; + var pages=[];var seen={}; + document.querySelectorAll('.sidebar-item.ws-tree-item a.page-name[href^="/pages/"]').forEach(function(a){ + var m=(a.getAttribute('href')||'').match(/\/pages\/(\d+)/);if(!m)return; + var id=parseInt(m[1],10);var name=(a.textContent||'').trim()||('Page '+id); + if(id===E.pid||seen[id])return;seen[id]=1;pages.push({id:id,name:name}); + }); + var ov=document.createElement('div');ov.id='fdMoveOverlay';ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;'; + var box=document.createElement('div');box.style.cssText='background:var(--bg-secondary,#1f1f1f);border:1px solid var(--border,#333);border-radius:12px;padding:16px;width:420px;max-width:92vw;max-height:80vh;overflow:auto;'; + var listHtml=pages.length?pages.map(function(p){return '';}).join(''):'
Aucune autre page dans l\u2019arbre du workspace.
Ouvrez la page cible puis réessayez.
'; + box.innerHTML='
Move block to…
' + +'' + +'
'+listHtml+'
' + +''; + box.addEventListener('click',function(ev){ + var p=ev.target.closest('[data-pid]'); + if(p){var pid=parseInt(p.getAttribute('data-pid'),10);ov.remove();E.moveBlockToPage(idx,pid);return;} + if(ev.target.closest('[data-cancel]')){ov.remove();} + }); + box.addEventListener('input',function(ev){ + if(ev.target.classList&&ev.target.classList.contains('bm-search')){ + var q=(ev.target.value||'').toLowerCase(); + box.querySelectorAll('.bm-page').forEach(function(b2){b2.style.display=(q&&b2.textContent.toLowerCase().indexOf(q)<0)?'none':'';}); + } + }); + ov.appendChild(box); + document.body.appendChild(ov); + } + function _blockDocDown(ev){ + if(!(ev.target&&ev.target.closest))return; + if(ev.target.closest('#blockMenu')||ev.target.closest('#blockMenuSub')||ev.target.closest('#fdMoveOverlay'))return; + if(ev.target.closest('.block-handle'))return; + if(_selSet.size)_selClear(); + _blockMenuClose(); + } + + window.E=null; + function editorState(){ + return { + pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null, + updatedAt:PD.updated_at||'', + fmt:{open:false,top:0,left:0,idx:-1}, + shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,exportOpen:false, + aiBusy:false, + moveOpen:false,moveWorkspaces:[],moveLoading:false, + activityOpen:false,pageCreated:PD.created_at||'', + shareTab:'share',favorited:false, + pagePublished:!!PD.published, + pageIsShared:!!PD.is_shared, + generalAccess:PD.share_mode||'private',publicPerm:'viewer', + pubAllowEdit:false,pubAllowComments:true, + pageUrl:window.location.href,publishedUrl:'', + inviteEmail:'',invitePermission:'edit',inviteUserId:null,inviteGroupId:null,inviteGroups:[],inviteUsers:[],inviteSuggestOpen:false,inviteSel:-1,accessList:[], + toastVisible:false,toastMsg:'', + // ── v4.9.0: Collaboration — comments & mentions ── + commentsOpen:false,commentDraft:'',comments:[],commentCount:0, + currentUserId:PD.user_id||0, + _commentSel:{range:null,bid:null,start:0,end:0}, + _notifiedMentions:{}, + undoStack:[],redoStack:[],_histCap:100,_histLock:false, + // ── Cover & icon (v5.10+) ── + coverUrl:'', + coverOpen:false, + coverLoading:false, + iconOpen:false, + iconValue:'', + // ── Import ── + importOpen:false,importText:'',importFile:null, + // ── v5.11.0 wiki-links & v5.12.0 page options ── + isLocked:false,lockedBy:null,canEdit:true,fullWidth:false,fontSmall:false, + tplSaving:false, + // ── DB Templates ── + dbTplOpen:false,dbTplLoading:false,dbTemplates:[], + // ── Version history ── + versionsOpen:false,versionsLoading:false,versionsList:[], + // ── Backlinks ── + backlinksOpen:false,backlinksLoading:false,backlinksList:[], + get timeAgo() { + if (!this.updatedAt) return 'just now'; + var diff = Math.floor((Date.now() - new Date(this.updatedAt + 'Z').getTime()) / 1000); + if (diff < 60) return 'just now'; + if (diff < 3600) return Math.floor(diff / 60) + 'm ago'; + if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'; + return Math.floor(diff / 86400) + 'd ago'; + }, + formatDate(d) { + if (!d) return ''; + var dt = new Date(d + 'Z'); + return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + + ' at ' + dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); + }, + init(){ + _bid=Math.floor(Math.random()*10000); + const dataEl=document.getElementById('page-data'); + if(dataEl){try{const data=JSON.parse(dataEl.textContent);this.pid=data.id;this.pageTitle=data.title||'';this.favorited=data.favorited||false;const fmt=data.content_format||'blocks';this.contentFormat=fmt;const raw=data.content||''; + if(fmt==='file'){this.fileData=data;this.fileUrl=data.file_url||'';this.loadFileContent(data);} + else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);ensureBlockIds(this.blocks);}catch(e){this.blocks=[];}} + else if(raw&&raw.trim())this.blocks=this.md2b(raw); + // v5.12.0: page lock + layout options + this.isLocked=!!data.is_locked;this.lockedBy=data.locked_by||null; + this.canEdit=data.can_edit!==false;this.fullWidth=!!data.full_width;this.fontSmall=!!data.font_small;}catch(e){}} + if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')]; + this.loadInlineDBs(); + window.E=this; + this.insertTable=function(){this.sync();this.pushHistory();var nb={id:genId(),type:'table',content:'',rows:_tblNew(3,3)};this.blocks.push(nb);this.dirty=true;this.autoSave();this.render();_rtSync();setTimeout(function(){_tblFocus(nb.id,0,0);},60);}; + SM.init();_tblInitDelegate();this.render(); + this._applyLockMode();this._applyLayout();_wikiResolve(); + const tEl=document.getElementById('_titleEl'); + if(tEl){const syncT=()=>{tEl.classList.toggle('empty',!(tEl.textContent||'').trim());};syncT();tEl.addEventListener('input',syncT);tEl.addEventListener('blur',syncT);} + if(this.contentFormat==='blocks'&&window.__fdRT&&window.__fdRT.start){setTimeout(()=>{try{window.__fdRT.start(this);}catch(e){}},150);} + setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return; + ct.addEventListener('keydown',e=>this.onKd(e)); + ct.addEventListener('paste',e=>this.handleSmartPaste(e)); + if(typeof Sortable!=='undefined')Sortable.create(ct,{draggable:'.block-wrapper:not(.nested-block):not(.block-column-wrapper)',handle:'.block-handle',animation:150,ghostClass:'sortable-ghost',dragClass:'sortable-drag',onEnd:evt=>{this._dragEnd(evt);}}); + ct.addEventListener('click',e=>{const h=e.target&&e.target.closest?e.target.closest('.block-handle'):null;if(!h)return;const w=h.closest('.block-wrapper');if(!w)return;const wid=w.getAttribute('data-id');if(!wid)return; + if(e.shiftKey){_selToggle(wid);return;} + _selClear();blockMenuOpen(e.clientX,e.clientY,wid);}); + ct.addEventListener('contextmenu',e=>{if(!(e.target&&e.target.closest))return;if(e.target.closest('.ftable-cell')||e.target.closest('#_tblMenu'))return;const c=e.target.closest('.block-content, .block-wrapper, .block-column-wrapper');const el0=e.target.closest('[data-bid]');if(!c)return;const w=e.target.closest('.block-wrapper');const wid=w?w.getAttribute('data-id'):null;const bid=wid||(el0?el0.getAttribute('data-bid'):null);if(!bid)return;e.preventDefault();_selClear();blockMenuOpen(e.clientX,e.clientY,bid);}); + document.addEventListener('mousedown',_blockDocDown); + if(window.location.hash&&window.location.hash.indexOf('#fdblk-')===0){const hb=window.location.hash.slice(7);setTimeout(()=>{const selEl=document.querySelector(`[data-bid="${hb}"]`);if(selEl&&selEl.scrollIntoView)selEl.scrollIntoView({behavior:'smooth',block:'center'});if(selEl){selEl.focus();ce(selEl);}},500);} + },100); + }, + // ── v5.12.0: lock + layout helpers ── + _applyLockMode(){ + document.body.classList.toggle('page-locked',!!this.isLocked); + let banner=document.getElementById('_lockBanner'); + if(this.isLocked){ + if(!banner){ + banner=document.createElement('div');banner.id='_lockBanner'; + banner.className='fd-lock-banner'; + banner.innerHTML='🔒This page is locked (read-only)' + +''; + const wrap=document.querySelector('.page-editor-wrapper'); + if(wrap)wrap.insertBefore(banner,wrap.firstChild); + } + const ub=banner.querySelector('#_lockUnlockBtn'); + if(ub)ub.style.display=this.canEdit?'':'none'; + }else if(banner){banner.remove();} + const ct=document.getElementById('_blocksCt'); + if(ct){ct.querySelectorAll('[data-bid]').forEach(el=>el.setAttribute('contenteditable',this.canEdit?'true':'false'));} + const t=document.getElementById('_titleEl'); + if(t)t.setAttribute('contenteditable',this.canEdit?'true':'false'); + }, + _applyLayout(){ + const wrap=document.querySelector('.page-editor-wrapper'); + if(!wrap)return; + wrap.classList.toggle('full-width',!!this.fullWidth); + wrap.classList.toggle('small-text',!!this.fontSmall); + }, + async toggleLock(){ + if(!this.canEdit){this.showToast&&this.showToast('Only the person who locked this page (or an admin) can unlock it','error');return;} + const next=!this.isLocked; + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/lock',{method:'POST',credentials:'same-origin', + headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()}, + body:JSON.stringify({locked:next})}); + if(!r.ok){const d=await r.json().catch(()=>({}));this.showToast((d&&d.detail)||'Lock failed','error');return;} + this.isLocked=next;this._applyLockMode(); + this.showToast(next?'🔒 Page locked':'🔓 Page unlocked'); + }catch(e){this.showToast('Lock failed','error');} + }, + async setPageOption(key,val){ + try{ + const body={};body[key]=val; + const r=await fetch('/board/api/pages/'+this.pid+'/options',{method:'POST',credentials:'same-origin', + headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify(body)}); + if(r.ok){this[key]=val;this._applyLayout();} + }catch(e){} + }, + async saveAsTemplate(){ + if(!this.pageTitle||!(this.pageTitle||'').trim()){this.showToast('Give the page a title first','error');return;} + const name=(this.pageTitle||'').trim()+' template'; + this.tplSaving=true;this.sync(); + const blocks=this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','children','rows'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;}); + try{ + const r=await fetch('/board/api/page-templates',{method:'POST',credentials:'same-origin', + headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()}, + body:JSON.stringify({name:name,blocks:blocks})}); + if(r.ok){this.moreOpen=false;this.showToast('📑 Saved as template: '+name);} + else{const d=await r.json().catch(()=>({}));this.showToast((d&&d.detail)||'Save failed','error');} + }catch(e){this.showToast('Save failed','error');} + this.tplSaving=false; + }, + fileData:null, + fileUrl:'', + async loadFileContent(data){ + const url=data.file_url||'';const mime=data.file_mime||'';const name=data.file_name||'';const self=this; + if(!url){this.blocks=[this.mkB('paragraph','File not available')];this.render();return;} + const ext=name.toLowerCase().split('.').pop(); + if(mime.startsWith('image/')||['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext)){ + this.blocks=[this.mkB('image','',{src:url,alt:name})];this.render();return; + } + if(mime==='application/pdf'||ext==='pdf'){ + this.blocks=[this.mkB('embed','',{src:url,embed_type:'pdf'})];this.render();return; + } + const codeExts=['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log']; + const langMap={'py':'python','js':'javascript','ts':'typescript','jsx':'javascript','tsx':'typescript','html':'html','htm':'html','css':'css','json':'json','xml':'xml','yaml':'yaml','yml':'yaml','md':'markdown','markdown':'markdown','sql':'sql','sh':'bash','bash':'bash','ps1':'powershell','bat':'plaintext','cmd':'plaintext','rb':'ruby','go':'go','rs':'rust','java':'java','c':'c','cpp':'cpp','h':'c','hpp':'cpp','php':'php','swift':'swift','kt':'kotlin','scala':'scala','r':'r','toml':'toml','ini':'ini','cfg':'ini','conf':'ini','env':'ini','txt':'plaintext','log':'plaintext'}; + if(mime.startsWith('text/')||codeExts.includes(ext)){ + try{const r=await fetch(url);const text=await r.text(); + if(ext==='md'||ext==='markdown'){this.blocks=this.md2b(text);} + else{this.blocks=[this.mkB('code',text,{language:langMap[ext]||'Plain Text'})];} + this.render(); + }catch(e){this.blocks=[this.mkB('paragraph','Error loading file: '+e.message)];this.render();} + return; + } + this.blocks=[this.mkB('embed','',{src:url,embed_type:'download',file_name:name,file_size:data.file_size||0,file_mime:mime})];this.render(); + }, + mkB(type,content,extras){const b={id:genId(),type,content:content||'',...(extras||{})};if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='toggle')b.children=[];if(type==='button'){b.automation_id=0;b.automation_name='';}return b;}, + // v5.11.0: rewrite one block's text (e.g. after inserting a wiki token) + // and re-render in place, caret at end. + _commitBlockText(b,text){ + this.sync();this.pushHistory(); + b.content=text; + this.dirty=true;this.autoSave();this.render();_rtSync(); + const el=this.getEl(b.id); + if(el){el.focus();const r=document.createRange();r.selectNodeContents(el);r.collapse(false);const s=window.getSelection();s.removeAllRanges();s.addRange(r);} + if(window.__fdRT&&window.__fdRT.syncNow)window.__fdRT.syncNow(); + }, + getEl(bid){const ct=document.getElementById('_blocksCt');return ct?ct.querySelector(`[data-bid="${bid}"]`):null;}, + getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);}, + getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};}, + _dragEnd(evt){ + const draggedId=evt.item&&evt.item.getAttribute?evt.item.getAttribute('data-id'):null; + const selected=_selIndicesSorted(); + const group=_selSet.has(draggedId)&&selected.length>1; + if(evt.oldIndex===evt.newIndex&&!group)return; + this.sync();this.pushHistory(); + const byId={};this.blocks.forEach(b=>{byId[b.id]=b;}); + if(group){ + const groupIds={};selected.forEach(i=>{const bb=this.blocks[i];if(bb)groupIds[bb.id]=1;}); + const groupBlocks=selected.map(i=>this.blocks[i]).filter(Boolean); + const ct=document.getElementById('_blocksCt'); + const domIds=[];ct.querySelectorAll('.block-wrapper[data-id]').forEach(w=>{const id=w.getAttribute('data-id');if(byId[id])domIds.push(id);}); + let inserted=false;const out=[]; + for(let i=0;iout.push(gb));inserted=true;}continue;} + out.push(byId[id]); + } + if(!inserted)groupBlocks.forEach(gb=>out.push(gb)); + if(out.length)this.blocks=out; + }else{ + const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item); + } + this.dirty=true;this.autoSave();this.render();_rtSync(); + }, + focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}}, + onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];if(b&&(b.type==='table'||b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='button')){this.addAt(this.blocks.length,'paragraph');}else{const el=b&&this.getEl(b.id);if(el){el.focus();ce(el);}}}}}, + sync(){const ct=document.getElementById('_blocksCt');if(!ct)return;for(const b of this.blocks){if(b.type==='table'){const wrap=ct.querySelector(`[data-bid="${b.id}"].ftable-editor`);if(wrap){const rows=[];wrap.querySelectorAll('tr.ftable-row').forEach(function(tr){const cells=tr.querySelectorAll('th.ftable-cell,td.ftable-cell');if(!cells.length)return;const rowarr=[];cells.forEach(function(cell){rowarr.push(cell.textContent||'');});rows.push(rowarr);});if(rows.length)b.rows=rows;}continue;} + if(b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='table_of_contents'||b.type==='button')continue; + if(b.type==='math'){const te=ct.querySelector(`[data-math-bid="${b.id}"]`);if(te)b.content=te.value||'';continue;} + const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el&&!el.classList.contains('toggle-title'))b.content=gtTok(el)||'';else if(el)b.content=gtTok(el)||''; + if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtTok(che)||'';}); + if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtTok(che)||'';}); + }}, + render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i{ct.querySelectorAll('.block-embed-collection').forEach(el=>{const cid=parseInt(el.id.replace('inline-db-',''));if(cid&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,cid,null);});},50); + // Initialize AI Meeting Note widgets (block type 'meeting') + setTimeout(()=>{ct.querySelectorAll('[data-meeting-bid]').forEach(function(el){if(window.FDMeeting)window.FDMeeting.mount(el.getAttribute('data-meeting-bid'),el);});},50); + ct.querySelectorAll('[data-bid]').forEach(el=>{el.addEventListener('input',()=>{self.dirty=true;self.autoSave();if(el.hasAttribute&&el.hasAttribute('data-placeholder'))el.classList.toggle('empty',!(el.textContent||'').trim());if(window.AIAC)window.AIAC.schedule(el.getAttribute('data-bid'));})}); + ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());}); + _selApply(); + var self2=this; + setTimeout(function(){ + if(window._wikiResolve)_wikiResolve(); + if(!self2.canEdit)self2._applyLockMode(); + const ub=document.getElementById('_lockUnlockBtn'); + if(ub&&!ub._wired){ub._wired=true;ub.onclick=function(){window.E.toggleLock();};} + },30); + }, + _renderKatex(el,tex){ + if(!el)return;if(!tex||!tex.trim()){el.innerHTML='Empty math block';return;} + if(window.katex){try{el.innerHTML=window.katex.renderToString(tex,{displayMode:true,throwOnError:false});}catch(e){el.innerHTML='Invalid LaTeX';}} + else{el.innerHTML=''+esc(tex)+'';} + }, +replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pushHistory();b.type=type;b.content='';if(type==='toggle'){b.expanded=true;b.children=[];}if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='math'){}if(type==='button'){b.automation_id=0;b.automation_name='';}if(type==='embed'){b.embed_type='';b.src='';b.height=520;}if(type==='bookmark'){b.url='';b.title='';b.description='';b.image='';b.site_name='';}if(type==='video'){b.src='';b.title='';}if(type==='audio'){b.src='';b.title='';}this.render();this.dirty=true;this.autoSave();_rtSync();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);}, + addAt(idx,type,content){this.sync();this.pushHistory();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();_rtSync();setTimeout(()=>{const el=this.getEl(b.id);if(el&&type!=='divider'){el.focus();content&&ce(el);}},60);return b;}, + addAfter(idx,type){return this.addAt(idx+1,type);}, + removeBlock(idx){this.sync();this.pushHistory();if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}const fi=Math.min(idx,this.blocks.length-1);this.render();this.dirty=true;this.autoSave();this._rtSync();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);}, + + // ── v5.4.0/v5.5.0: Media block URL setters (click empty block → prompt) ── + _mediaPrompt(bid, field, label){ + const idx=this.getIdx(bid); if(idx<0)return; + const b=this.blocks[idx]; if(!b)return; + const cur=b[field]||''; const u=prompt(label+' (URL):', cur); + if(u===null)return; + if(u.trim()===cur.trim())return; + this.sync();this.pushHistory();b[field]=u.trim();this.dirty=true;this.autoSave();this.render();_rtSync(); + }, + async setEmbedUrl(bid){ + const idx=this.getIdx(bid); if(idx<0)return; + const b=this.blocks[idx]; if(!b)return; + const cur=b.src||''; const u=prompt('Embed URL (YouTube, Vimeo, Figma, Maps…):', cur); + if(u===null)return; + if(u.trim()===cur.trim())return; + this.sync();this.pushHistory();b.src=u.trim();b.embed_type='';this.dirty=true; + try{ + const r=await fetch('/board/api/embed/resolve',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({url:u.trim()})}); + const d=await r.json(); + if(d&&d.ok&&d.src){b.embed_src=d.src;b.embed_provider=d.provider||'';} + }catch(e){} + this.render();this.autoSave();_rtSync(); + }, + setBookmarkUrl(bid){ + const idx=this.getIdx(bid); if(idx<0)return; + const b=this.blocks[idx]; if(!b)return; + const cur=b.url||''; const u=prompt('Bookmark URL:', cur); + if(u===null)return; + if(u.trim()===cur.trim())return; + this.sync();this.pushHistory();b.url=u.trim(); + this._fetchBookmarkPreview(bid, u.trim()); + }, + async _fetchBookmarkPreview(bid, url){ + try{ + const r=await fetch('/board/api/og/metadata',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({url})}); + const d=await r.json(); + if(d&&d.ok){ + const idx=this.getIdx(bid); if(idx<0)return; + const b=this.blocks[idx]; + if(d.title)b.title=d.title; + if(d.description)b.description=d.description; + if(d.image)b.image=d.image; + if(d.site_name)b.site_name=d.site_name; + this.render();this.dirty=true;this.autoSave();_rtSync(); + } + }catch(e){} + }, + setVideoSrc(bid){this._mediaPrompt(bid,'src','Video');}, + setAudioSrc(bid){this._mediaPrompt(bid,'src','Audio');}, + + // ── v5.5.0: Image lightbox (fullscreen + keyboard navigation) ── + _openLightbox(imgEl){ + const full=imgEl.dataset.full||imgEl.src; + if(!full)return; + const imgs=Array.from(document.querySelectorAll('.block-image img, .fd-img')).filter(i=>i.dataset.full||i.src); + let idx=imgs.indexOf(imgEl); + if(idx<0)idx=0; + const overlay=document.createElement('div');overlay.className='fd-lightbox';overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.95);z-index:5000;display:flex;align-items:center;justify-content:center;flex-direction:column;'; + overlay.innerHTML='' + +'' + +'' + +'
'; + document.body.appendChild(overlay); + const img=overlay.querySelector('.fd-lightbox-img'); + const hint=overlay.querySelector('.fd-lightbox-hint'); + const show=()=>{const cur=imgs[idx];img.src=cur.dataset.full||cur.src;hint.textContent=(imgs.length>1?(idx+1)+' / '+imgs.length+' — ':'')+'← → to navigate · Esc to close';}; + const step=(d)=>{if(imgs.length<2)return;idx=(idx+d+imgs.length)%imgs.length;show();}; + show(); + const close=()=>{overlay.remove();document.removeEventListener('keydown',kd);}; + const kd=(e)=>{if(e.key==='Escape')close();else if(e.key==='ArrowRight')step(1);else if(e.key==='ArrowLeft')step(-1);}; + document.addEventListener('keydown',kd); + overlay.querySelectorAll('[data-nav]').forEach(btn=>btn.addEventListener('click',e=>{e.stopPropagation();step(btn.dataset.nav==='next'?1:-1);})); + overlay.addEventListener('click',e=>{if(e.target===overlay)close();}); + }, + + // ── v5.4.0: Version history UI ── + versionsOpen:false,versionsLoading:false,versionsList:[], + async toggleVersions(){ + this.versionsOpen=!this.versionsOpen; + if(this.versionsOpen){await this.loadVersions();} + }, + async loadVersions(){ + this.versionsLoading=true; + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/versions'); + const d=await r.json(); + this.versionsList=d.versions||[]; + }catch(e){this.versionsList=[];} + this.versionsLoading=false; + }, + async restoreVersion(vid){ + if(!confirm('Restore this version? Current content will be replaced.'))return; + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/versions/'+vid+'/restore',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()}}); + const d=await r.json(); + if(d.status==='ok'){ + this.showToast('Version restored');this.versionsOpen=false; + setTimeout(()=>window.location.reload(),300); + } + }catch(e){this.showToast('Restore failed','error');} + }, + + // ── v5.4.0: Backlinks ── + backlinksOpen:false,backlinksLoading:false,backlinksList:[], + async toggleBacklinks(){ + this.backlinksOpen=!this.backlinksOpen; + if(this.backlinksOpen){await this.loadBacklinks();} + }, + async loadBacklinks(){ + this.backlinksLoading=true; + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/backlinks'); + const d=await r.json(); + this.backlinksList=d.backlinks||[]; + }catch(e){this.backlinksList=[];} + this.backlinksLoading=false; + }, + + // ── v5.5.0: Cover & Icon ── + async loadCoverIcon(){ + try{ + const r=await fetch('/board/api/pages/'+this.pid); + const d=await r.json(); + this.coverUrl=d.cover_url||''; + this.iconValue=d.page_icon||''; + }catch(e){} + }, + toggleCover(){this.coverOpen=!this.coverOpen;if(this.coverOpen&&!this.coverUrl){this.loadCoverIcon();}}, + async setCoverUrl(){ + const u=prompt('Cover image URL:', this.coverUrl); + if(u===null)return; + this.coverLoading=true; + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({cover_url:u})}); + const d=await r.json(); + if(d.status==='ok'){this.coverUrl=d.cover_url;this.coverOpen=false;this.showToast('Cover updated');} + }catch(e){this.showToast('Cover update failed','error');} + this.coverLoading=false; + }, + async uploadCover(e){ + const f=e.target.files?.[0];if(!f)return; + this.coverLoading=true; + const fd=new FormData();fd.append('file',f); + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()},body:fd}); + const d=await r.json(); + if(d.status==='ok'){this.coverUrl=d.cover_url;this.coverOpen=false;this.showToast('Cover uploaded');} + }catch(e){this.showToast('Cover upload failed','error');} + this.coverLoading=false; + e.target.value=''; + }, + async removeCover(){ + if(!confirm('Remove cover?'))return; + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'DELETE',headers:{'X-CSRF-Token':this.csrfTok()}}); + const d=await r.json(); + if(d.status==='ok'){this.coverUrl='';this.showToast('Cover removed');} + }catch(e){this.showToast('Remove failed','error');} + }, + toggleIcon(){this.iconOpen=!this.iconOpen;}, + iconHtml(){ + var v=this.iconValue; + if(!v)v=(this.contentFormat==='file')?'paperclip':'file'; + return window.fdIconHtml?window.fdIconHtml(v,36):v; + }, + openIconPicker(){ + var self=this; + if(window.FDIconPicker)window.FDIconPicker.openFor({ + x:120,y:160, + onPick:function(v){self.setIcon(v);}, + onRemove:function(){self.setIcon('');} + }); + }, + async setIcon(icon){ + this.iconValue=icon; + try{ + const r=await fetch('/board/api/pages/'+this.pid+'/icon',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({icon})}); + const d=await r.json(); + if(d.status==='ok'){this.iconOpen=false;this.showToast(icon?'Icon updated':'Icon removed');} + }catch(e){this.showToast('Icon update failed','error');} + }, + + // ── v5.4.0: Import ── + importOpen:false,importFile:null,importText:'', + toggleImport(){this.importOpen=!this.importOpen;}, + handleImportFile(e){ + const f=e.target.files?.[0];if(!f)return; + this.importFile=f; + const reader=new FileReader(); + reader.onload=()=>{this.importText=reader.result;}; + reader.readAsText(f); + }, + async doImport(){ + if(!this.importText.trim()&&!this.importFile){this.showToast('Nothing to import');return;} + if(this.importFile){ + const fd=new FormData();fd.append('file',this.importFile); + try{ + const r=await fetch('/board/api/pages/import/file',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()},body:fd}); + const d=await r.json(); + if(d.status==='ok'){this.showToast(d.count+' page(s) imported');this.importOpen=false;this.importFile=null;this.importText='';} + }catch(e){this.showToast('Import failed','error');} + }else{ + try{ + const r=await fetch('/board/api/pages/import',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({markdown:this.importText,title:this.pageTitle||'Import'})}); + const d=await r.json(); + if(d.status==='ok'){this.showToast('Page imported');this.importOpen=false;this.importText='';setTimeout(()=>{window.location.href='/pages/'+d.id;},400);} + }catch(e){this.showToast('Import failed','error');} + } + }, + + // ── v5.10.0: Undo / Redo ── + pushHistory(){if(this._histLock)return;this.undoStack.push(JSON.parse(JSON.stringify(this.blocks)));if(this.undoStack.length>this._histCap)this.undoStack.shift();this.redoStack.length=0;}, + undo(){if(!this.undoStack.length){this.showToast('Rien à annuler');return;}const fid=this._focusedBid();this.redoStack.push(JSON.parse(JSON.stringify(this.blocks)));this.applySnapshot(this.undoStack.pop());this._focusBid(fid);}, + redo(){if(!this.redoStack.length){this.showToast('Rien à rétablir');return;}const fid=this._focusedBid();this.undoStack.push(JSON.parse(JSON.stringify(this.blocks)));this.applySnapshot(this.redoStack.pop());this._focusBid(fid);}, + applySnapshot(snap){this._histLock=true;this.blocks=snap||[];if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];this.dirty=true;this.autoSave();this.render();this._histLock=false;_rtSync();}, + _focusedBid(){const a=this.getActiveBlock();return a?a.bid:null;}, + _focusBid(bid){if(bid)setTimeout(()=>{const el=this.getEl(bid);if(el){el.focus();ce(el);}},60);}, + _rtSync(){_rtSync();}, + + // ── v5.10.0: Duplicate ── + _deepCloneBlock(b){const c=JSON.parse(JSON.stringify(b));c.id=genId();if(Array.isArray(c.children)){c.children=c.children.map(ch=>{const n=JSON.parse(JSON.stringify(ch));n.id=genId();return n;});}return c;}, + duplicateBlock(idx){if(idx<0||idx>=this.blocks.length)return;const sel=_selIndicesSorted();if(sel.length>1&&sel.includes(idx)){this.duplicateSelected();return;}this.sync();this.pushHistory();const copy=this._deepCloneBlock(this.blocks[idx]);this.blocks.splice(idx+1,0,copy);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(copy.id);_selClear();return copy;}, + duplicateSelected(){const indices=_selIndicesSorted();if(!indices.length)return;this.sync();this.pushHistory();const copies=indices.map(i=>this._deepCloneBlock(this.blocks[i]));const pos=indices[indices.length-1]+1;this.blocks.splice(pos,0,...copies);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(copies[0].id);_selClear();}, + + // ── v5.10.0: Turn into / couleurs / liens / déplacement ── + _blockTargets(idx){const sel=_selIndicesSorted();return(sel.includes(idx))?sel:[idx];}, + turnInto(idx,type){if(idx<0||idx>=this.blocks.length)return;if(_BLOCK_TURN_TYPES.indexOf(type)<0)return;const targets=this._blockTargets(idx);const focusId=this.blocks[idx].id;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(b.type===type)return;const keep=(b.content||'').trim();b.type=type;if(type!=='to_do'&&type!=='toggle'&&keep&&['image','embed','meeting','divider','table_of_contents','database','button','columns'].indexOf(b.type)<0){b.content=keep;}else{b.content='';}if(type==='toggle'){b.expanded=true;if(!Array.isArray(b.children))b.children=[];}if(type==='to_do')b.checked=false;if(type==='table'&&!Array.isArray(b.rows))b.rows=_tblNew(3,3);if(type==='code'&&!b.language)b.language='Plain Text';if(type==='columns'&&(!Array.isArray(b.children)||!b.children.length))b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='button'){b.automation_id=0;b.automation_name='';}}); + this.dirty=true;this.autoSave();this.render();_rtSync();_selClear(); + setTimeout(()=>{const bid=this.blocks[idx]?this.blocks[idx].id:focusId;if(type==='table'){_tblFocus(bid,0,0);}else{const el=this.getEl(bid);if(el){el.focus();ce(el);}}},60);}, + setBlockColor(idx,key,val){if(idx<0||idx>=this.blocks.length)return;const targets=this._blockTargets(idx);const v=(!val||val==='default')?null:val;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(!b.style)b.style={};if(v==null)delete b.style[key];else b.style[key]=v;});this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();}, + copyBlockLink(bid){const url=window.location.origin+window.location.pathname+'#fdblk-'+bid;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(url).then(()=>this.showToast('Lien du bloc copié')).catch(()=>this.showToast('Copie impossible','error'));}else{this.showToast(url,'error');}}, + deleteSelected(idx){const indices=_selIndicesSorted();if(indices.length<=1)return this.removeBlock(idx);this.sync();this.pushHistory();const keep=this.blocks.filter((b,i)=>indices.indexOf(i)<0);this.blocks=keep.length?keep:[this.mkB('paragraph','')];const fi=Math.min(idx,this.blocks.length-1);this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);}, + // ── v5.11: Copy / Cut / Paste selected blocks ── + _blocksToMarkdown(blocks){return blocks.map(blocksToMarkdown).join('\n\n');}, + copySelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>this.showToast('Blocs copiés')).catch(()=>this.showToast('Copie impossible','error'));}else{this.showToast(md,'error');}}, + cutSelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;this.sync();this.pushHistory();const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>{});}this.blocks=this.blocks.filter((b,i)=>indices.indexOf(i)<0);if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Blocs coupés');}, + async pasteBlocks(){const idx=this._focusedIdx();if(idx<0)return;try{const text=await navigator.clipboard.readText();if(!text||!text.trim())return;const parsed=this.md2b(text);if(!parsed.length)return;this.sync();this.pushHistory();this.blocks.splice(idx+1,0,...parsed);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(parsed[parsed.length-1].id);_selClear();}catch(e){if(e.name==='NotAllowedError'){this.showToast('Permission presse-papier refusée');}else{this.showToast('Coller impossible','error');}}}, + _focusedIdx(){const a=this.getActiveBlock();return a?a.idx:-1;}, + async moveBlockToPage(idx,pageId){if(idx<0||idx>=this.blocks.length||pageId===this.pid)return;this.sync();this.pushHistory();const block=this.blocks[idx];const self=this;const csrf=document.cookie.match(/csrf_token=([^;]+)/);this.showToast('Déplacement du bloc…');try{const r=await fetch(`/board/api/pages/${pageId}`,{credentials:'same-origin'});const d=await r.json();let tblocks=[];if(d.content_format==='blocks'&&d.content){try{tblocks=JSON.parse(d.content);}catch(e){tblocks=[];}}tblocks.push(block);await fetch(`/board/api/pages/${pageId}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:d.title||'',blocks:tblocks})}); + if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Bloc déplacé'); + }catch(e){this.showToast('Échec du déplacement','error');}}, + + // ── v4.11.1: Inline AI — "Ask AI" & "AI meeting note" (Get started) ── + // Wire the contextual get-started actions into the FlowDeck Agent + // (Notion-style inline generation). The reply is streamed in the agent + // panel and inserted into this page via window.__fdInsertAI. + askAI(){ + const self=this; + const title=(this.pageTitle||'Sans titre').trim(); + if(!(window.fdAgent && window.fdAgent.generate)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; } + if(self.aiBusy) return; + self.aiBusy=true; + self.showToast('FlowDeck Agent : génération en cours…'); + let ctx=''; + try{ if(window.fdGetPageContext) ctx=String(window.fdGetPageContext()||''); }catch(e){} + const prompt='Rédige le contenu de la page FlowDeck intitulée « '+title+' ». Fournis un texte structuré et directement utilisable : paragraphes et, si utile, des listes à puces. Sépare chaque paragraphe par une ligne vide. Ne précède pas le texte de commentaires.'; + window.fdAgent.generate(prompt, ctx).then(function(text){ + self.aiBusy=false; + if(text && text.trim()){ self.applyAIBlocks(text); } + else { self.showToast('Aucun retour généré par l\u2019agent'); } + }).catch(function(err){ + self.aiBusy=false; + self.showToast((err && err.message) ? err.message : 'Erreur de génération'); + }); + }, + meetingNote(){ + // v4.12.0: crée une vraie « boîte de réunion » (bloc 'meeting') au lieu + // d'insérer du texte. La page est renommée « Meeting @date » si elle + // est vide, puis le widget est ajouté (ou remplace le bloc vide initial). + var self=this; + this.gsMoreOpen=false; + var today=new Date(); + var iso=today.toISOString().slice(0,10); + var title='Meeting @'+iso; + var tEl=document.getElementById('_titleEl'); + var curTitle=tEl?(tEl.textContent||'').trim():''; + var blank=!curTitle||curTitle==='Untitled'||curTitle==='New page'; + if(blank){ + this.pageTitle=title; + if(tEl)tEl.textContent=title; + } + var mb=this.mkB('meeting',''); + mb.meeting={ + title:title, + date:iso, + consent:'self', + started:false, + status:'idle', + lines:[], + interim:'', + notes:'', + summary:'', + summaryError:'', + lastTab:'transcript' + }; + this.sync();this.pushHistory(); + var emptyFirst=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim(); + if(emptyFirst){ this.blocks=[mb]; } else { this.blocks.push(mb); } + this.dirty=true; this.autoSave(); this.render(); + this.showToast('AI Meeting Note : bloc de réunion ajouté. Cliquez sur « Start transcribing ».'); + var bid=mb.id; + setTimeout(function(){ var el=document.querySelector('[data-meeting-bid="'+bid+'"]'); if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth',block:'center'}); if(el&&window.FDMeeting)window.FDMeeting.focus(bid); },120); + }, + aiRun(prompt){ + const self=this; + if(self.aiBusy) return; + if(!(window.fdAgent && window.fdAgent.ask)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; } + self.aiBusy=true; + window.__fdInsertAI = function(content){ window.__fdInsertAI=null; self.aiBusy=false; self.applyAIBlocks(content); }; + self.showToast('FlowDeck Agent : génération en cours…'); + window.fdAgent.ask(prompt); + }, +applyAIBlocks(text){ + // Convert agent markdown → real blocks (headings, lists, tables, code…) + // so it renders naturally (Notion-style) instead of showing raw `## …`. + const nb=this.md2b(text||''); + if(!nb.length) return; + this.sync(); + const startEmpty=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim(); + this.blocks = startEmpty ? nb : this.blocks.concat(nb); + this.dirty=true; this.autoSave(); this.render(); + }, + + toggleFavorite(){ + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + const m=this.favorited?'DELETE':'POST'; + fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf?csrf[1]:''}}) + .then(r=>r.json()).then(()=>{this.favorited=!this.favorited;this.showToast(this.favorited?'Added to favorites':'Removed from favorites'); + if(window.appState&&window.appState.refreshFavorites)window.appState.refreshFavorites(); + }).catch(()=>{this.showToast('Failed to toggle favorite');}); + }, + toggleActivityOpen(){ + if(!this.activityOpen){this.activityOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);} + else{this.activityOpen=false;} + }, + toggleShareOpen(){ + if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);this.loadShares();} + else{this.shareOpen=false;} + }, + toggleMoreOpen(){ + if(!this.moreOpen){this.moreOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);} + else{this.moreOpen=false;} + }, + _justOpened:false, + _syncIsShared(){this.pageIsShared=!!(this.pagePublished||this.generalAccess==='anyone'||(this.accessList||[]).length>0);}, + togglePublish(){this.pagePublished=!this.pagePublished;this._syncIsShared();this.saveShare();}, + saveShare(){ + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).then(()=>this._syncIsShared()).catch(()=>{}); + }, + async copyPageLink(){ + console.log('copyPageLink invoked'); + const url = this.pageUrl; + const fallback = () => { + const ta = document.createElement('textarea'); + ta.value = url; ta.style.position='fixed'; ta.style.opacity='0'; + document.body.appendChild(ta); ta.select(); + document.execCommand('copy'); document.body.removeChild(ta); + }; + try { + await navigator.clipboard.writeText(url); + this.showToast('Link copied to clipboard'); + } catch(e) { fallback(); this.showToast('Link copied to clipboard'); } + }, + showToast(msg){ + this.toastMsg = msg; this.toastVisible = true; + clearTimeout(this._toastTimer); + this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500); + }, + publishPage() { + var self = this; + var csrf = document.cookie.match(/csrf_token=([^;]+)/); + fetch('/api/pages/' + this.pid + '/publish', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' } + }).then(function(r){ return r.json(); }).then(function(d){ + if (d.is_published) { + self.pagePublished = true; + self.publishedUrl = window.location.origin + '/p/' + d.publish_slug; + self.showToast('Published to web — ' + self.publishedUrl); + } else { + self.showToast(d.detail || 'Publish failed'); + } + }).catch(function(){ self.showToast('Publish failed'); }); + }, + unpublishPage() { + var self = this; + var csrf = document.cookie.match(/csrf_token=([^;]+)/); + fetch('/api/pages/' + this.pid + '/publish', { + method: 'DELETE', + headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' } + }).then(function(r){ return r.json(); }).then(function(d){ + if (!d.is_published) { + self.pagePublished = false; + self.publishedUrl = ''; + self.showToast('Unpublished'); + } else { + self.showToast(d.detail || 'Unpublish failed'); + } + }).catch(function(){ self.showToast('Unpublish failed'); }); + }, + shareInvite() { + var self = this; + if (this.inviteGroupId) { + var csrf = document.cookie.match(/csrf_token=([^;]+)/); + fetch('/api/pages/' + this.pid + '/share', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' }, + body: JSON.stringify({ group_id: this.inviteGroupId, permission: this.invitePermission }) + }).then(function(r){ return r.json(); }).then(function(d){ + if (d.status === 'shared') { + self.inviteEmail = ''; self.inviteUserId = null; self.inviteGroupId = null; + self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false; + self.showToast('Group shared'); self.loadShares(); + } else { self.showToast(d.detail || 'Share failed'); } + }).catch(function(){ self.showToast('Share failed'); }); + return; + } + if (!this.inviteEmail.trim()) return; + var csrf2 = document.cookie.match(/csrf_token=([^;]+)/); + fetch('/api/pages/' + this.pid + '/share', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf2 ? csrf2[1] : '' }, + body: JSON.stringify({ user_id: this.inviteUserId, email: this.inviteEmail.trim(), permission: this.invitePermission }) + }).then(function(r){ return r.json(); }).then(function(d){ + if (d.status === 'shared') { + self.inviteEmail = ''; self.inviteUserId = null; self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false; + self.showToast('Invitation sent'); self.loadShares(); + } else { self.showToast(d.detail || 'Share failed'); } + }).catch(function(){ self.showToast('Share failed'); }); + }, + inviteInput(){ + var self = this; + var q = (this.inviteEmail || '').trim(); + if (!q) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteUserId = null; this.inviteGroupId = null; this.inviteSel = -1; return; } + clearTimeout(this._inviteTimer); + this._inviteTimer = setTimeout(function(){ self._loadInviteUsers(q); }, 150); + }, + async _loadInviteUsers(q){ + try { + const r = await fetch('/api/notifications/users/search?q=' + encodeURIComponent(q), { credentials: 'same-origin' }); + const d = await r.json(); + var me = this.currentUserId; + var shared = {}; + (this.accessList || []).forEach(function(a){ if (a.user_login) shared[a.user_login.toLowerCase()] = true; }); + this.inviteUsers = (d.users || []).filter(function(u){ + return u.id !== me && !shared[(u.login || '').toLowerCase()]; + }); + // Groups: charger les groupes du workspace et filtrer + try { + const gr = await fetch('/api/v2/groups', { credentials: 'same-origin' }); + const gd = await gr.json(); + var sharedGroups = {}; + (this.accessList || []).forEach(function(a){ if (a.shared_with_group_id) sharedGroups[a.shared_with_group_id] = true; }); + var ql = (q || '').toLowerCase(); + this.inviteGroups = (gd.groups || []).filter(function(g){ + return !sharedGroups[g.id] && (!ql || (g.name || '').toLowerCase().indexOf(ql) >= 0); + }).slice(0, 8); + } catch(e2) { this.inviteGroups = []; } + this.inviteSel = this.inviteUsers.length ? 0 : -1; + this.inviteSuggestOpen = true; + } catch(e) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; } + }, + inviteMove(dir){ + var el = this.$refs.inviteSuggest; + var n = this.inviteUsers.length; + if (!n) return; + this.inviteSel = (this.inviteSel + dir + n) % n; + if (el) { + var item = el.querySelectorAll('.sd-user-item')[this.inviteSel]; + if (item) item.scrollIntoView({ block: 'nearest' }); + } + }, + inviteEnter(){ + if (this.inviteSuggestOpen && this.inviteSel >= 0 && this.inviteUsers[this.inviteSel]) { + this.pickInviteUser(this.inviteUsers[this.inviteSel]); + } else { + this.inviteSuggestOpen = false; + this.shareInvite(); + } + }, + pickInviteUser(u){ + this.inviteUserId = u.id; + this.inviteGroupId = null; + this.inviteEmail = u.login || u.full_name || ''; + this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1; + var el = this.$refs.inviteInput; + if (el) el.focus(); + }, + pickInviteGroup(g){ + this.inviteGroupId = g.id; + this.inviteUserId = null; + this.inviteEmail = g.name || ''; + this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1; + var el = this.$refs.inviteInput; + if (el) el.focus(); + }, + updateShare(sid, perm) { + var self = this; + var csrf = document.cookie.match(/csrf_token=([^;]+)/); + fetch('/api/pages/' + this.pid + '/share/' + sid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' }, + body: JSON.stringify({ permission: perm }) + }).then(function(r){ return r.json(); }).then(function(d){ + self.showToast(d.status === 'updated' ? 'Permission updated' : (d.detail || 'Update failed')); + self.loadShares(); + }).catch(function(){ self.showToast('Update failed'); self.loadShares(); }); + }, + loadShares() { + var self = this; + fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){ + self.accessList = d.shares || []; + self._syncIsShared(); + }).catch(function(){ self.accessList = []; self._syncIsShared(); }); + }, + removeShare(sid) { + var self = this; + var csrf = document.cookie.match(/csrf_token=([^;]+)/); + fetch('/api/pages/' + this.pid + '/share/' + sid, { + method: 'DELETE', + headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' } + }).then(function(r){ return r.json(); }).then(function(d){ + if (d.status === 'removed') { self.showToast('Share removed'); self.loadShares(); } + else { self.showToast(d.detail || 'Remove failed'); } + }).catch(function(){ self.showToast('Remove failed'); }); + }, + invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';}, + removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);}, + exportPage(fmt){ + this.moreOpen=false; + this.exportOpen=false; + // Ensure latest blocks are persisted before exporting any format + var csrf = document.cookie.match(/csrf_token=([^;]+)/); + var self = this; + var doDownload = function(){ + var title = encodeURIComponent((self.pageTitle || 'Untitled').trim() || 'Untitled'); + var a = document.createElement('a'); + a.href = '/api/export/' + (fmt==='markdown'?'markdown':fmt==='html'?'html':fmt==='pdf'?'pdf':'site') + '/' + self.pid; + a.download = ''; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + var label = {markdown:'Markdown',html:'HTML',pdf:'PDF',site:'Site'}[fmt]||''; + self.showToast('Exported as ' + label); + }; + if (this.dirty) { + this.save(function(){ doDownload(); }); + } else { + doDownload(); + } + }, + duplicatePage(){this.moreOpen=false;const csrf=document.cookie.match(/csrf_token=([^;]+)/);const t=(this.pageTitle||'').trim()||'New Page';fetch(`/board/api/pages?title=${encodeURIComponent(t+' copy')}§ion=Private&project=${encodeURIComponent(PD.workspace_key||'')}`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(r=>r.json()).then(d=>{window.location.href=`/pages/${d.id}`;}).catch(()=>{window.showToast('Duplicate failed','error');});}, + movePage(){ + this.moreOpen=false; + this.moveOpen = true; + this.moveLoading = true; + var self = this; + fetch('/api/workspaces') + .then(function(r){ return r.json(); }) + .then(function(d){ + self.moveWorkspaces = d.workspaces || []; + self.moveLoading = false; + }) + .catch(function(){ self.moveLoading = false; }); + }, + doMove(wsId) { + this.moveOpen = false; + var csrf = document.cookie.match(/csrf_token=([^;]+)/); + var self = this; + fetch('/board/api/pages/' + this.pid + '/move', { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' }, + body: JSON.stringify({ workspace_id: wsId }) + }).then(function(r){ return r.json(); }) + .then(function(d){ + if (d.status === 'ok') { + self.showToast('Moved to workspace'); + setTimeout(function(){ window.location.href = '/local-workspace'; }, 800); + } else { + self.showToast(d.detail || 'Move failed'); + } + }) + .catch(function(){ self.showToast('Move failed'); }); + }, + deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});}, + downloadFile(){this.moreOpen=false;if(!this.fileUrl)return;var a=document.createElement('a');a.href=this.fileUrl;a.download='';document.body.appendChild(a);a.click();document.body.removeChild(a);this.showToast('Download started','success');}, + async copyFileContent(){this.moreOpen=false;if(!this.fileUrl)return;try{var r=await fetch('/api/pages/'+this.pid+'/file-content');var d=await r.json();if(d.ok&&d.content!==undefined){await navigator.clipboard.writeText(d.content);this.showToast('Content copied to clipboard','success');}else{this.showToast('Not a text file','error');}}catch(e){this.showToast('Copy failed','error');}}, + + // ── v4.6.0 / v5.3.0: Get Started actions — Database (template picker) ── + dbTplOpen:false, dbTplLoading:false, dbTemplates:[], dbTplTargetIdx:-1, + async createDatabase(){ + this.openDbTemplatePicker(-1); + }, + async openDbTemplatePicker(targetIdx){ + this.gsMoreOpen=false; + this.dbTplOpen=true; this.dbTplTargetIdx=targetIdx; this.dbTplLoading=true; + var self=this; + try{ + const r=await fetch('/workspace/templates/database'); + const d=await r.json(); + self.dbTemplates=d.templates||[]; + }catch(e){ self.dbTemplates=[]; } + self.dbTplLoading=false; + self.$nextTick(()=>{ const f=document.getElementById('fd-dbtpl'); if(f) setTimeout(()=>{f.style.opacity='1';},10); }); + }, + closeDbTemplatePicker(){ this.dbTplOpen=false; }, + async createDbFromTemplate(tplName){ + const self=this; + var name = tplName ? (tplName+' — '+new Date().toLocaleDateString('fr-FR')) : 'Database'; + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + try{ + const body={name:name, parent_page_id:this.pid}; + if(tplName) body.template=tplName; + const r=await fetch('/db/inline/api',{ + method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''}, + body:JSON.stringify(body) + }); + const d=await r.json(); + if(d.id){ + const nb=this.mkB('embed','',{embed_type:'collection',collection_id:d.id}); + this.sync(); + const idx=this.dbTplTargetIdx>=0?this.dbTplTargetIdx:(this.blocks.length); + this.blocks.splice(idx,0,nb); + this.dbTplOpen=false; + this.dirty=true; this.autoSave(); this.render(); + this.showToast(tplName?('Database created from template — '+tplName):'Database created'); + }else{ + this.showToast(d.detail||'Failed to create database'); + } + }catch(e){ this.showToast('Error: '+e.message); } + }, + async createForm(){ + const name=prompt('Form name:','New Form'); + if(!name||!name.trim())return; + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + try{ + const r=await fetch('/db/inline/api',{ + method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''}, + body:JSON.stringify({name:name.trim(),parent_page_id:this.pid}) + }); + const d=await r.json(); + if(d.id){ + this.inlineDBs.push({id:d.id,name:d.name,view_type:'form'}); + this.showToast('Form created — '+d.name); + this.saveDBsToPage(); + this.renderInlineDBs(); + }else{ + this.showToast('Failed to create form'); + } + }catch(e){this.showToast('Error: '+e.message);} + }, + saveDBsToPage(){ + // Store inline DB refs in the page's blocks as a special embed block + if(this.inlineDBs.length===0)return; + const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs'); + if(existing.length===0){ + this.blocks.push(this.mkB('embed','',{embed_type:'inline_dbs',dbs:this.inlineDBs})); + }else{ + existing.forEach(b=>{b.dbs=this.inlineDBs;}); + } + this.render(); + this.dirty=true;this.autoSave(); + }, + renderInlineDBs(){ + // Re-render with inline DB blocks + this.render(); + }, + loadInlineDBs(){ + const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs'); + if(existing.length>0){ + this.inlineDBs=existing[0].dbs||[]; + } + }, + + async onKd(e){ + const ab=this.getActiveBlock();if(!ab)return; + const{el,bid,idx}=ab;const block=this.blocks[idx];if(!block)return; + if(SM.isOpen){ + if(e.key==='Escape'){e.preventDefault();SM.close();return;} + if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;} + if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;} + return; + } + if(window.AIAC&&window.AIAC.active){ + if(e.key==='Tab'){e.preventDefault();e.stopPropagation();window.AIAC.accept();return;} + if(e.key==='Escape'){e.preventDefault();window.AIAC.clear();return;} + if(e.key==='Enter'||e.key==='Backspace'){window.AIAC.clear();} + } + if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen&&!WM.isOpen)SM.open(i2,e2); + if(e2&&!SM.isOpen&&!WM.isOpen){const t=gtTok(e2)||'';const mm=t.match(/\[\[$/);if(mm){WM.open(i2,e2);}}},15);} + if(e.key===' '){ + if(block.type==='paragraph'&&!(block.content||'').trim()&&!(el.textContent||'').trim()&&!SM.isOpen&&window.AIC){ + e.preventDefault(); window.AIC.open(idx); return; + } + const i2=idx;setTimeout(()=>this.checkMd(i2),15); + } + if(e.key==='Enter'){ + if(e.shiftKey){this.dirty=true;this.autoSave();return;} + e.preventDefault(); + const sp=splitCaret(el); + const before=sp?sp.before:gtTok(el)||'',after=sp?sp.after:''; + if(!before.trim()&&!after.trim()){ + const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph'; + this.sync();this.pushHistory();this.blocks.splice(idx+1,0,this.mkB(nt,''));this.render();_rtSync(); + setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();ce(ne);}},60); + this.dirty=true;this.autoSave();return; + } + const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph'; + this.sync();this.pushHistory();block.content=before;this.blocks.splice(idx+1,0,this.mkB(nt,after));this.render();_rtSync(); + setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();cs(ne);}},60); + this.dirty=true;this.autoSave();return; + } + if(e.key==='ArrowUp'){const _sp=splitCaret(el);if(_sp&&!_sp.before.trim()&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;} + if(e.key==='ArrowDown'){const _sp=splitCaret(el);if(_sp&&!_sp.after.trim()&&idx{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;} + if(e.key===' '){const b2=bid;setTimeout(()=>this.checkMd(b2),15);} + if((e.ctrlKey||e.metaKey)&&!e.altKey){ + if(e.key.toLowerCase()==='z'){e.preventDefault();if(e.shiftKey)this.redo();else this.undo();return;} + if(e.key.toLowerCase()==='y'){e.preventDefault();this.redo();return;} + if(e.key.toLowerCase()==='d'){e.preventDefault();const ab2=this.getActiveBlock();if(ab2)this.duplicateBlock(ab2.idx);return;} + if(e.key.toLowerCase()==='c'){ + e.preventDefault(); + const sel=_selIndicesSorted(); + if(sel.length>0){this.copySelectedBlocks();return;} + } + if(e.key.toLowerCase()==='x'){ + e.preventDefault(); + const sel=_selIndicesSorted(); + if(sel.length>0){this.cutSelectedBlocks();return;} + } + if(e.key.toLowerCase()==='v'){ + e.preventDefault(); + await this.pasteBlocks();return; + } + const m={b:'bold',i:'italic',u:'underline'};if(m[e.key]){e.preventDefault();document.execCommand(m[e.key]);this.dirty=true;this.autoSave();}} + if(e.key==='Tab'&&block.type==='code'){e.preventDefault();document.execCommand('insertText',false,' ');} + this.dirty=true;this.autoSave(); + }, + checkMd(idx){ + if(idx<0||idx>=this.blocks.length)return;const block=this.blocks[idx];const el=this.getEl(block.id);if(!el)return;const text=el.textContent||''; + const sc={'# ':'heading_1','## ':'heading_2','### ':'heading_3','#### ':'heading_4','- ':'bulleted_list','* ':'bulleted_list','+ ':'bulleted_list','1. ':'numbered_list','[] ':'to_do','[ ] ':'to_do','> ':'quote'}; + this.sync(); + for(const[pfx,type] of Object.entries(sc)){if(text===pfx||text.startsWith(pfx)){this.pushHistory();block.type=type;block.content='';if(type==='toggle')block.expanded=true;if(type==='to_do')block.checked=false;this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne&&type!=='divider')ne.focus();},60);return;}} + if(text==='---'){this.pushHistory();block.type='divider';block.content='';this.addAfter(idx,'paragraph');} + if(text==='```'){this.pushHistory();block.type='code';block.content='';block.language='Plain Text';this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);} + }, + applySlash(id){ + const idx=SM._i;if(idx<0||idx>=this.blocks.length)return; + if(id==='database'){ this.openDbTemplatePicker(idx); return; } + if(id==='synced'){ + const block=this.blocks[idx];this.sync();this.pushHistory(); + block.type='synced';block.synced_id=0;block._synced_content=[];block._synced_title=''; + this.render();this.dirty=true;this.autoSave();_rtSync(); + // Open synced block picker + this._pickSyncedBlock(idx); + return; + } + if(id.indexOf('ai_')===0){ this.aiSlash(id,idx); return; } + if(id==='duplicate'){this.duplicateBlock(idx);return;} + if(id==='delete'){this.removeBlock(idx);return;} + if(id==='copy_link'){const blk=this.blocks[idx];if(blk)this.copyBlockLink(blk.id);return;} + if(id==='turn_into'){const blk=this.blocks[idx];if(!blk)return;const el=this.getEl(blk.id);const rk=el?el.getBoundingClientRect():null;blockMenuOpen(rk?rk.left:window.innerWidth/2,rk?rk.bottom:window.innerHeight/2,blk.id);return;} + const block=this.blocks[idx];this.sync();this.pushHistory();block.content=''; + if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');} + else if(id==='embed'){block.type='embed';block.embed_type='';block.src='';block.height=520;} + else if(id==='bookmark'){block.type='bookmark';block.url='';block.title='';block.description='';block.image='';block.site_name='';} + else if(id==='video'){block.type='video';block.src='';block.title='';} + else if(id==='audio'){block.type='audio';block.src='';block.title='';} + else{block.type=id;if(id==='toggle'){block.expanded=true;block.children=[];}if(id==='to_do')block.checked=false;if(id==='columns')block.children=[{id:genId(),type:'paragraph',content:''}];if(id==='code')block.language='Plain Text';if(id==='table'){block.rows=_tblNew(3,3);block.content='';}if(id==='button'){block.automation_id=0;block.automation_name='';}} + this.render();this.dirty=true;this.autoSave();_rtSync(); + if(id==='table'){setTimeout(()=>_tblFocus(block.id,1,0),60);} + else if(id==='math'){setTimeout(()=>{const ne=document.querySelector(`[data-math-bid="${block.id}"]`);if(ne)ne.focus();},60);} + else if(id==='columns'){setTimeout(()=>{const ne=this.getEl(block.children[0].id);if(ne)ne.focus();},60);} + else{setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);} + }, + // ── v5.9.0: AI writing slash commands ── + aiSlash(id,idx){ + const action=id.slice(3); const block=this.blocks[idx]; + if(block){ this.sync(); this.pushHistory(); block.content=''; } + if(action==='write'){ this.render(); this.dirty=true; this.autoSave(); if(window.AIC)window.AIC.open(idx); return; } + if(action==='translate'){ this.render(); this.dirty=true; this.autoSave(); + const lang=window.prompt('Traduire en…','English'); if(!lang){return;} + this._aiWritingRun('translate',idx,{target_language:lang},'replace'); return; } + this.render(); this.dirty=true; this.autoSave(); + this._aiWritingRun(action,idx,{},'insert'); + }, + _aiWritingRun(action,idx,extra,mode){ + const self=this; this.sync(); + const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):''; + const payload={action:action,context:ctx}; + if(action==='continue'){ let parts=[]; for(let i=0;i{ const el=this.getEl(this.blocks[Math.min(idx,this.blocks.length-1)]?.id); if(el){el.focus();ce(el);} },60); + }, + _doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].checked=v;this.dirty=true;this.autoSave();_rtSync();}}, + _doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].expanded=!this.blocks[idx].expanded;this.render();this.dirty=true;this.autoSave();_rtSync();}}, + _addColumn(parentId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];this.pushHistory();b.children.push({id:genId(),type:'paragraph',content:''});this.render();this.dirty=true;this.autoSave();_rtSync();setTimeout(()=>{const ne=this.getEl(b.children[b.children.length-1].id);if(ne)ne.focus();},60);}, + _removeColumn(parentId,childId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];this.pushHistory();b.children=b.children.filter(function(c){return c.id!==childId;});if(b.children.length===0)b.children=[{id:genId(),type:'paragraph',content:''}];this.render();this.dirty=true;this.autoSave();_rtSync();}, + _toggleCalloutIcon(bid){const picker=document.getElementById('callout-picker-'+bid);if(picker)picker.style.display=picker.style.display==='none'?'flex':'none';}, + _setCalloutIcon(bid,icon){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].icon=icon;this.dirty=true;this.autoSave();this.render();_rtSync();}}, + // ── v5.14.0: Synced blocks picker ── + async _pickSyncedBlock(idx){ + const block=this.blocks[idx];if(!block)return; + try{ + const r=await fetch('/api/synced-blocks');const d=await r.json(); + const items=d.synced_blocks||[]; + if(!items.length){this.showToast('No synced blocks yet. Create one first in Settings → Synced Blocks.');return;} + const names=items.map(function(s){return (s.title||'Untitled')+' (id:'+s.id+')';}); + const choice=window.prompt('Choose a synced block:\n\n'+names.join('\n')); + if(!choice)return; + const match=choice.match(/id:(\d+)/); + if(!match)return; + const sid=parseInt(match[1]); + if(!sid){this.showToast('Invalid synced block','error');return;} + const sr=await fetch('/api/synced-blocks/'+sid);const sd=await sr.json(); + if(!sd||!sd.content){this.showToast('Synced block not found','error');return;} + block.synced_id=sid; + block._synced_content=sd.content||[]; + block._synced_title=sd.title||''; + this.render();this.dirty=true;this.autoSave();_rtSync(); + }catch(e){this.showToast('Failed to load synced blocks','error');} + }, + showFmt(idx){const s=window.getSelection();if(!s.rangeCount||s.isCollapsed){this.fmt.open=false;return;}const r=s.getRangeAt(0).getBoundingClientRect();this.fmt.open=true;this.fmt.idx=idx;this.fmt.top=Math.max(r.top-44,0);this.fmt.left=Math.min(r.left+r.width/2-120,window.innerWidth-260);this._sel={range:s.getRangeAt(0).cloneRange(),idx};}, + fmtApply(f){this.fmt.open=false;if(!this._sel)return;const s=window.getSelection();s.removeAllRanges();s.addRange(this._sel.range);document.execCommand(f==='strikeThrough'?'strikeThrough':f);this._sel=null;this.dirty=true;this.autoSave();}, + fmtLink(){this.fmt.open=false;const u=prompt('URL:');if(u){document.execCommand('createLink',false,u);this.dirty=true;this.autoSave();}}, + // ── v4.9.0: Collaboration — comments & mentions ── + toggleComments(){this.commentsOpen=!this.commentsOpen;if(this.commentsOpen)this.loadComments();}, + async loadComments(){try{const r=await fetch('/api/pages/'+this.pid+'/comments',{credentials:'same-origin'});const d=await r.json();this.comments=d.comments||[];this.commentCount=this.comments.length;}catch(e){this.comments=[];}}, + fmtTime(s){if(!s)return '';const t=new Date((String(s).includes('T')||String(s).includes('Z'))?s:(s+'Z'));if(isNaN(t.getTime()))t=new Date(s);const diff=Math.floor((Date.now()-t.getTime())/1000);if(diff<60)return 'just now';if(diff<3600)return Math.floor(diff/60)+'m ago';if(diff<86400)return Math.floor(diff/3600)+'h ago';return Math.floor(diff/86400)+'d ago';}, + csrfTok(){return (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'';}, + async addPageComment(){ + const text=(this.commentDraft||'').trim();if(!text)return; + const self=this;const sel=this._commentSel; + try{ + const r=await fetch('/api/pages/'+this.pid+'/comments',{method:'POST',credentials:'same-origin', + headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()}, + body:JSON.stringify({body:text,anchor_block_id:sel.bid||null,anchor_start:sel.bid?sel.start:null,anchor_end:sel.bid?sel.end:null})}); + const d=await r.json(); + if(d.status==='created'){this.commentDraft='';this._commentSel={range:null,bid:null,start:0,end:0};this.hideCommentBtn();await this.loadComments();this.showToast('Comment added');} + else{this.showToast(d.detail||'Failed to add comment');} + }catch(e){this.showToast('Failed to add comment');} + }, + async resolveComment(id){try{await fetch('/api/comments/'+id,{method:'PUT',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({resolved:true})});await this.loadComments();}catch(e){}}, + async deleteComment(id){if(!confirm('Delete this comment?'))return;try{await fetch('/api/comments/'+id,{method:'DELETE',credentials:'same-origin',headers:{'X-CSRF-Token':this.csrfTok()}});await this.loadComments();}catch(e){}}, + commentOnSelection(){ + const sel=this._commentSel;if(!sel.range)return; + const bid=sel.bid||'';this.commentsOpen=true;this.commentDraft=''; + const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none'; + this.$nextTick(()=>{const ta=document.querySelector('.comments-drawer textarea');if(ta)ta.focus();}); + }, + showCommentBtn(){ + const s=window.getSelection();if(!s||s.isCollapsed)return; + const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(s.anchorNode))return; + const bel=s.anchorNode.parentElement? s.anchorNode.parentElement.closest('[data-bid]'):null; + if(!bel)return; + const bid=bel.dataset.bid; + const range=s.getRangeAt(0).cloneRange();range.selectNodeContents(bel);range.setEnd(s.getRangeAt(0).startContainer,s.getRangeAt(0).startOffset); + const start=range.toString().length; + const end=start+s.toString().length; + if(end<=start)return; + this._commentSel={range:s.getRangeAt(0).cloneRange(),bid:bid,start:start,end:end}; + const r=s.getRangeAt(0).getBoundingClientRect(); + const btn=document.getElementById('_commentSelBtn');if(!btn)return; + btn.style.display='block'; + const bw=btn.offsetWidth||110,bh=btn.offsetHeight||34; + let x=r.left+r.width/2-bw/2,y=r.top-bh-8; + x=Math.min(Math.max(8,x),window.innerWidth-bw-8);if(y<8)y=r.bottom+8; + btn.style.left=x+'px';btn.style.top=y+'px'; + }, + hideCommentBtn(){const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';}, + // ── @mention autocomplete (v4.9.0 users + v5.11.0 pages & dates) ── + mentionOpen:false,mentionQuery:'',mentionResults:[],mentionPages:[],mentionEl:null, + openMention(el){ + const s=window.getSelection();if(!s.rangeCount)return; + const range=s.getRangeAt(0).cloneRange(); + range.selectNodeContents(el);range.setEnd(s.getRangeAt(0).startContainer,s.getRangeAt(0).startOffset); + const before=range.toString(); + const m=before.match(/@([\w\-\.]*)$/); + if(!m){return;} + this.mentionQuery=m[1];this.mentionEl=el; + const r=s.getRangeAt(0).getBoundingClientRect(); + const menu=document.getElementById('_mentionMenu'); + menu.style.display='block'; + menu.style.top=Math.max(4,(r.bottom+4>window.innerHeight-300)?r.top-304:r.bottom+4)+'px'; + menu.style.left=Math.min(r.left,window.innerWidth-260)+'px'; + this.mentionOpen=true;this.loadMentionResults(); + }, + async loadMentionResults(){ + const q=this.mentionQuery||''; + const self=this; + const [u,p]=await Promise.all([ + fetch('/api/notifications/users/search?q='+encodeURIComponent(q),{credentials:'same-origin'}).then(r=>r.ok?r.json():{users:[]}).catch(()=>({users:[]})), + fetch('/board/api/wiki/pages?q='+encodeURIComponent(q),{credentials:'same-origin'}).then(r=>r.ok?r.json():{pages:[]}).catch(()=>({pages:[]})) + ]); + if(!this.mentionOpen)return; + this.mentionResults=u.users||[];this.mentionPages=p.pages||[]; + this.renderMentionMenu(); + }, + _dateSuggestions(q){ + const out=[];const lq=(q||'').toLowerCase(); + const iso=s=>s.toISOString().slice(0,10); + const now=new Date(); + if(!lq||'today'.startsWith(lq))out.push({label:'Today',iso:iso(now)}); + if(!lq||'tomorrow'.startsWith(lq))out.push({label:'Tomorrow',iso:iso(new Date(now.getTime()+864e5))}); + if(!lq||'yesterday'.startsWith(lq))out.push({label:'Yesterday',iso:iso(new Date(now.getTime()-864e5))}); + const m=lq.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); + if(m)out.push({label:'Set date',iso:m[1]+'-'+m[2].padStart(2,'0')+'-'+m[3].padStart(2,'0')}); + return out; + }, + renderMentionMenu(){ + const menu=document.getElementById('_mentionMenu');if(!menu)return; + let h='';const self=this; + h+='
People
'; + (this.mentionResults||[]).slice(0,6).forEach(function(u){ + h+='
' + +'
' + +((u.avatar_url)?'':((u.full_name||u.login||'?').charAt(0).toUpperCase())) + +'
'+esc(u.login)+'
'+esc(u.full_name||'')+'
'; + }); + if((this.mentionPages||[]).length){ + h+='
Pages
'; + this.mentionPages.slice(0,6).forEach(function(p){ + h+='
' + +''+esc(p.icon||'📄')+'' + +''+esc(p.title)+'
'; + }); + } + const ds=this._dateSuggestions(this.mentionQuery); + if(ds.length){ + h+='
Date
'; + ds.forEach(function(d){ + h+='
' + +'📅' + +''+esc(d.label)+''+esc(d.iso)+'
'; + }); + } + if(this.mentionResults.length===0&&this.mentionPages.length===0&&ds.length===0)h='
No matches
'; + menu.innerHTML=h; + menu.querySelectorAll('.mention-item').forEach(function(it){ + it.addEventListener('click',function(){ + const kind=it.getAttribute('data-mkind');const val=it.getAttribute('data-mval'); + if(kind==='user')self.insertMention(val); + else if(kind==='page')self.insertMentionToken('[[fdpage:'+val+']] '); + else if(kind==='date')self.insertMentionToken('[[fddate:'+val+']] '); + }); + }); + }, + // Shared non-destructive insertion: replaces the typed "@query" right + // before the caret inside the token-reconstructed text (chips kept). + _replaceAtMention(replacement){ + const el=this.mentionEl;if(!el)return null; + const idx=this.getIdx(el.dataset.bid);if(idx<0)return null; + const b=this.blocks[idx]; + const text=gtTok(el)||''; + // robust: compute caret by walking with a marker + let caretPos=text.length; + try{ + const MK='\uF000AT';const mark=document.createElement('span');mark.textContent=MK; + const live=s.getRangeAt(0).cloneRange();live.collapse(true);live.insertNode(mark); + const full=gtTok(el)||'';const at=full.indexOf(MK); + if(at>=0)caretPos=at; + mark.remove();el.normalize(); + }catch(e){} + const before=text.slice(0,caretPos); + const m=before.match(/@([\w\-\.]*)$/); + if(!m)return null; + const start=caretPos-m[0].length; + const newText=text.slice(0,start)+replacement+text.slice(caretPos); + this._commitBlockText(b,newText); + return true; + }, + insertMentionToken(token){ + if(this._replaceAtMention(token)){this.closeMention();this.dirty=true;this.autoSave();} + }, + insertMention(login){ + if(this._replaceAtMention('@'+login+' ')){ + this.closeMention();this.dirty=true;this.autoSave(); + this.notifyNewMentions('@'+login); + } + }, + closeMention(){const menu=document.getElementById('_mentionMenu');if(menu)menu.style.display='none';this.mentionOpen=false;this.mentionEl=null;}, + notifyNewMentions(text){ + const self=this; + const handles=[];const re=/@([\w\-\.]+)/g;let mm; + while((mm=re.exec(text)))handles.push(mm[1].toLowerCase()); + const fresh=handles.filter(function(h){return !self._notifiedMentions[h];}); + if(!fresh.length)return; + fetch('/api/pages/'+this.pid+'/mentions',{method:'POST',credentials:'same-origin', + headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()}, + body:JSON.stringify({text:'@'+fresh.join(' @')})}).catch(function(){}); + }, + pastePlain(e){e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();}, + // ── Smart paste: analyze pasted text and split it into multiple blocks ── + paste2b(md){ + const out=[];let para=''; + const lines=String(md||'').replace(/\r\n/g,'\n').split('\n'); + const flush=()=>{ + if(!para.trim()){para='';return;} + const pa=para.split('\n').map(s=>s.trim()).filter(Boolean); + if(pa.length>1&&pa.every(l=>l.length<=55)){pa.forEach(l=>out.push(this.mkB('paragraph',l)));} + else out.push(this.mkB('paragraph',para.trim())); + para=''; + }; + let i=0; + for(;i{if(r.length>w)w=r.length;});if(w<1)w=1; + const rows2=rows.map(r=>{const a=r.slice();while(a.length '){flush();out.push(this.mkB('quote',t.substring(2)));continue;} + if(t==='---'||t==='***'){flush();out.push(this.mkB('divider',''));continue;} + para+=(para?'\n':'')+line.trim(); + } + flush(); + return out; + }, + handleSmartPaste(e){ + e.preventDefault(); + const text=(((e.clipboardData||window.clipboardData))||{}).getData('text/plain')||''; + const fallback=()=>{document.execCommand('insertText',false,text);this.dirty=true;this.autoSave();}; + const ab=this.getActiveBlock(); + if(!ab)return fallback(); + const bel=ab.el,idx=ab.idx,block=this.blocks[idx]; + if(!block)return fallback(); + const btype=block.type||'paragraph'; + if(['code','table','math','image','embed','meeting','divider','table_of_contents','columns'].indexOf(btype)>=0)return fallback(); + const parsed=this.paste2b(text); + if(parsed.length<=1)return fallback(); + const sp=splitCaret(bel); + let before='',after=''; + if(sp){before=sp.before;after=sp.after;}else{before=gt(bel)||'';} + this.sync();this.pushHistory(); + const beforeHasText=!!(before||'').trim(); + block.content=before; + const ins=[]; + parsed.forEach((pb,k)=>{ + if(k===0&&!beforeHasText){ + const nbf={id:block.id,type:pb.type,content:pb.content}; + for(const key in pb){if(key==='id'||key==='type'||key==='content')continue;nbf[key]=pb[key];} + this.blocks[idx]=nbf; + }else{ + const nb=this.mkB(pb.type,pb.content); + if(pb.language!==undefined)nb.language=pb.language; + if(pb.checked!==undefined)nb.checked=pb.checked; + if(pb.children)nb.children=pb.children; + ins.push(nb); + } + }); + if((after||'').trim())ins.push(this.mkB('paragraph',after.trim())); + this.blocks.splice(idx+1,0,...ins); + const focusId=ins.length?ins[ins.length-1].id:block.id; + this.render();this.dirty=true;this.autoSave(); + setTimeout(()=>{const fe=this.getEl(focusId);if(fe){fe.focus();ce(fe);}},60); + }, + autoSave(){if(this.fileData)return;clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);}, + // Reflect the page title in the left navigation, the header breadcrumb + // and the browser tab as soon as it changes. + _syncTitleUI(){ + const title=(this.pageTitle||'').trim()||'Untitled'; + document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=title;}); + document.querySelectorAll(`.sidebar-item.ws-tree-item[data-id="${this.pid}"]`).forEach(item=>{item.setAttribute('data-name',title);const n=item.querySelector('.page-name');if(n)n.textContent=title;}); + document.title='FlowDeck — '+title; + window.dispatchEvent(new CustomEvent('flowdeck:page-renamed',{detail:{id:this.pid,title:title}})); + if(window.parent&&window.parent!==window){try{window.parent.postMessage({type:'fd-page-renamed',id:this.pid,title:title},window.location.origin);}catch(e){}} + }, + save(cb){ + if(this.fileData){ + // File page: only the title is editable here. Sync the sidebar, + // header breadcrumb and browser tab live, and persist the title + // without rewriting the file's blocks. + const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||''; + this._syncTitleUI(); + if(this._fileTitleT)clearTimeout(this._fileTitleT); + this._fileTitleT=setTimeout(()=>{ + this._fileTitleT=null; + const title=(this.pageTitle||'').trim(); + if(!title)return; + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + fetch(`/board/api/pages/${this.pid}?title=${encodeURIComponent(title)}`,{method:'PUT',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}) + .then(()=>{this.dirty=false;}) + .catch(()=>{}); + },600); + if(cb)cb(); + return; + } + const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||''; + this._syncTitleUI(); + if(this.saving){ if(cb) cb(); return; } + this.sync(); + this.saving=true; + const blocksArr=this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','first_col_header','colsW','meeting','automation_id','automation_name','url','title','description','image','site_name'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;}); + // v6.0.0 PWA: hors ligne (ou échec réseau) → file IndexedDB rejouée au retour du réseau + const queueOffline=()=>{ + if(!window.FlowOffline){this.saving=false;return;} + window.FlowOffline.savePageOffline({id:this.pid,title:this.pageTitle,content:JSON.stringify(blocksArr),content_format:'blocks'}) + .then(()=>{this.saving=false;this.dirty=false;this.lastSaved='offline';if(cb)cb();}) + .catch(()=>{this.saving=false;}); + }; + if(!navigator.onLine){queueOffline();return;} + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:blocksArr})}) + .then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString(); + this._syncTitleUI(); + if(cb)cb(); + }).catch(()=>{this.saving=false;queueOffline();}); + }, + runAutomationButton(bid){ + const idx=this.getIdx(bid);if(idx<0)return; + const b=this.blocks[idx]; + if(!b.automation_id){this.pickAutomation(bid);return;} + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + const self=this; + this.showToast('⚡ '+ (b.automation_name||'Automation') + '…'); + fetch('/api/automations/'+b.automation_id+'/run',{method:'POST', + headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''}, + body:JSON.stringify({page_id:this.pid})}) + .then(r=>r.json()) + .then(d=>{ + if(d.status==='fired'){self.showToast('✅ Automation exécutée');} + else if(d.status==='skipped'){self.showToast('⏭️ Condition non remplie (skippée)');} + else{self.showToast((d.detail||'Erreur')+'','error');} + }) + .catch(()=>{self.showToast('Échec de l\u2019exécution','error');}); + }, + pickAutomation(bid){ + const self=this; + fetch('/workspace/automations',{credentials:'same-origin'}) + .then(r=>r.json()) + .then(d=>{ + const list=d.automations||[]; + if(!list.length){self.showToast('Aucune automation — créez-en dans Paramètres → Automations','error');return;} + const ov=document.createElement('div');ov.id='fdAutomationOverlay'; + ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;'; + const box=document.createElement('div');box.style.cssText='background:var(--bg-secondary,#1f1f1f);border:1px solid var(--border,#333);border-radius:12px;padding:20px;width:420px;max-width:92vw;max-height:80vh;overflow:auto;'; + box.innerHTML='
Choisir une automation
'+ + list.filter(a=>a.enabled).map(function(a){ + return ``; + }).join('')+ + ''; + ov.appendChild(box); + document.body.appendChild(ov); + box.addEventListener('click',function(ev){ + const btn=ev.target.closest('[data-id]');if(btn){ + const idx=self.getIdx(bid);if(idx>=0){self.blocks[idx].automation_id=parseInt(btn.getAttribute('data-id'),10);const a=list.find(function(x){return x.id===parseInt(btn.getAttribute('data-id'),10);});self.blocks[idx].automation_name=a?a.name:'';self.dirty=true;self.autoSave();self.render();} + ov.remove(); + }else if(ev.target.closest('[data-cancel]')){ov.remove();} + }); + }) + .catch(()=>{self.showToast('Erreur de chargement','error');}); + }, + md2b(md){const bl=[];const lines=String(md).split('\n');let i=0;for(;iw)w=r.length;});if(w<1)w=1;const rows2=rows.map(function(r){const a=r.slice();while(a.length '))bl.push(this.mkB('quote',t.substring(2))); + else bl.push(this.mkB('paragraph',t)); + }return bl;}, + }; + } + document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);d.showCommentBtn();}else{d.hideCommentBtn();}},80);}); + document.addEventListener('mousedown',()=>{setTimeout(()=>{const ed=document.querySelector('.page-editor-wrapper');if(ed?.__x)ed.__x.$data.hideCommentBtn();},10);}); + document.addEventListener('keyup',(e)=>{ + const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return; + const d=ed.__x.$data;if(!d.mentionOpen){if(e.key!=='@')return;} + const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return; + const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(el))return; + if(d.mentionOpen){ + if(e.key==='Escape'){d.closeMention();return;} + if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const items=document.querySelectorAll('#_mentionMenu .mention-item');if(!items.length)return;let idx=[].indexOf.call(items,document.querySelector('#_mentionMenu .mention-item.hover'));idx+= (e.key==='ArrowDown'?1:-1);if(idx<0)idx=items.length-1;if(idx>=items.length)idx=0;items.forEach(function(x){x.classList.remove('hover');});items[idx].classList.add('hover');return;} + if(e.key==='Enter'){e.preventDefault();const hover=document.querySelector('#_mentionMenu .mention-item.hover')||document.querySelector('#_mentionMenu .mention-item');if(hover)hover.click();return;} + } + d.openMention(el); + },true); + document.addEventListener('click',(e)=>{ + const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return; + const d=ed.__x.$data;if(!d.mentionOpen)return; + const menu=document.getElementById('_mentionMenu'); + if(menu&&!menu.contains(e.target))d.closeMention(); + }); + var fdStyle=document.createElement('style'); + fdStyle.textContent='#_mentionMenu .mention-item:hover,#_mentionMenu .mention-item.hover{background:rgba(35,131,226,.15);}'; + document.head.appendChild(fdStyle); + function blocksToMarkdown(b) { + var c = b.content || ''; + switch(b.type) { + case 'heading_1': return '# ' + c; + case 'heading_2': return '## ' + c; + case 'heading_3': return '### ' + c; + case 'heading_4': return '#### ' + c; + case 'bulleted_list': return '- ' + c; + case 'numbered_list': return '1. ' + c; + case 'to_do': return (b.checked ? '- [x] ' : '- [ ] ') + c; + case 'quote': return '> ' + c; + case 'divider': return '---'; + case 'code': return '```' + (b.language || '') + '\n' + c + '\n```'; + case 'toggle': return (b.children && b.children.length ? '## ' : '## ') + c + (b.children && b.children.length ? '\n\n' + b.children.map(blocksToMarkdown).join('\n\n') : ''); + case 'math': return '$$\n' + c + '\n$$'; + case 'table_of_contents': return '[TOC]'; + case 'columns': return (b.children || []).map(blocksToMarkdown).join('\n\n'); + case 'table': { var rows=b.rows||[]; if(!rows.length)return ''; var w=0; rows.forEach(function(r){if(r&&r.length>w)w=r.length;}); var rl=function(r){var cells=[];for(var k=0;k str: return fh.read() +def _read_js(name: str) -> str: + """A27 : le JS de l'éditeur vit dans static/js depuis l'extraction.""" + base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + with open(os.path.join(base, "static", "js", name), encoding="utf-8") as fh: + return fh.read() + + def test_editor_has_ai_slash_commands(client): - src = _read_template("_page_editor_scripts.html") + src = _read_js("page_editor_scripts.js") for cmd in ("ai_write", "ai_summarize", "ai_translate", "ai_continue"): assert cmd in src assert "AI writing" in src @@ -280,7 +287,7 @@ def test_editor_has_ai_slash_commands(client): def test_editor_has_autocomplete(client): - src = _read_template("_page_editor_scripts.html") + src = _read_js("page_editor_scripts.js") assert "AIAC" in src assert "autocomplete" in src assert "AIAC.schedule" in src diff --git a/tests/test_pwa_offline.py b/tests/test_pwa_offline.py index c9cf8ba..54efe7e 100644 --- a/tests/test_pwa_offline.py +++ b/tests/test_pwa_offline.py @@ -98,7 +98,7 @@ def test_workspace_offline_banner_shows_pending(client): def test_editor_save_has_offline_hook(): - src = (ROOT / "app" / "templates" / "_page_editor_scripts.html").read_text(encoding="utf-8") + src = (ROOT / "static" / "js" / "page_editor_scripts.js").read_text(encoding="utf-8") assert "FlowOffline" in src assert "savePageOffline" in src assert "navigator.onLine" in src diff --git a/tests/test_sharing.py b/tests/test_sharing.py index 73c8d84..9982aa0 100644 --- a/tests/test_sharing.py +++ b/tests/test_sharing.py @@ -295,7 +295,14 @@ def test_page_editor_renders_page_is_shared(client): r = client.get(f"/pages/{a}") assert r.status_code == 200 - assert "pageIsShared:true" in r.text + # A27 : la valeur n'est plus interpolée dans le JS inline mais exposée + # dans le JSON #page-data (le JS lit PD.is_shared) + import json as _json + import re as _re + + m = _re.search(r'id="page-data"[^>]*>(.*?)', r.text, _re.S) + assert m, "bloc #page-data absent de la page" + assert _json.loads(m.group(1))["is_shared"] is True def test_tree_is_shared_includes_link_shared_pages(client): diff --git a/tests/test_v511_v512_wiki_templates.py b/tests/test_v511_v512_wiki_templates.py index da87fed..522457b 100644 --- a/tests/test_v511_v512_wiki_templates.py +++ b/tests/test_v511_v512_wiki_templates.py @@ -391,7 +391,7 @@ def test_block_templates_service_shapes(): def test_editor_front_end_wired(client): import pathlib - src = pathlib.Path("app/templates/_page_editor_scripts.html").read_text(encoding="utf-8") + src = pathlib.Path("static/js/page_editor_scripts.js").read_text(encoding="utf-8") base = pathlib.Path("app/templates/base.html").read_text(encoding="utf-8") assert "gtTok" in src and "fd-wiki-chip" in src # chip render + reader assert "wiki/pages" in src and "wiki/titles" in src # picker + label resolution diff --git a/tests/test_v55.py b/tests/test_v55.py index cc6d59e..de193cb 100644 --- a/tests/test_v55.py +++ b/tests/test_v55.py @@ -316,7 +316,7 @@ class TestBookmarkPersistence: class TestLightbox: def test_editor_lightbox_supports_keyboard_navigation(self): - tpl = Path("app/templates/_page_editor_scripts.html").read_text(encoding="utf-8") + tpl = Path("static/js/page_editor_scripts.js").read_text(encoding="utf-8") assert "_openLightbox" in tpl assert "fd-lightbox-nav" in tpl assert "ArrowRight" in tpl and "ArrowLeft" in tpl @@ -343,7 +343,7 @@ class TestLightbox: class TestInlinePreviews: def test_editor_renders_video_audio_pdf(self): - tpl = Path("app/templates/_page_editor_scripts.html").read_text(encoding="utf-8") + tpl = Path("static/js/page_editor_scripts.js").read_text(encoding="utf-8") assert "