diff --git a/commit_snapshot.md b/commit_snapshot.md deleted file mode 100644 index 955482d..0000000 --- a/commit_snapshot.md +++ /dev/null @@ -1,248 +0,0 @@ -# Commit Snapshot (`commits.snapshot_json`) - Chuẩn Hiện Tại (FrontEndUser / UHM) - -Tài liệu này mô tả **snapshot_json** mà `FrontEndUser` (module UHM editor) tạo ra khi bấm **Commit** trong `/editor/[id]`, và gửi lên endpoint `POST /projects/{id}/commits`. - -Nguồn tham chiếu trong code (FrontEndUser): - -- Types: - - `src/uhm/types/sections.ts` (`EditorSnapshot`, `EntityWikiLinkSnapshot`) - - `src/uhm/types/geo.ts` (`FeatureCollection`, `GeometrySnapshot`, `GeometryEntitySnapshot`) - - `src/uhm/types/entities.ts` (`EntitySnapshot`) - - `src/uhm/types/wiki.ts` (`WikiSnapshot`) -- Build/normalize snapshot: - - `src/uhm/lib/editor/snapshot/editorSnapshot.ts` (`buildEditorSnapshot`, `normalizeEditorSnapshot`) - -## 1) Root Shape - -FE hiện tại không dùng `schema_version`. `snapshot_json` là một object có các phần sau: - -```ts -export type EditorSnapshot = { - editor_feature_collection?: FeatureCollection; - entities?: EntitySnapshot[]; - geometries?: GeometrySnapshot[]; - geometry_entity?: GeometryEntitySnapshot[]; - wikis?: WikiSnapshot[]; - entity_wiki?: EntityWikiLinkSnapshot[]; -}; -``` - -Lưu ý: - -- FE có thể **đọc** cả `entity_wiki` và legacy alias `entity_wikis` khi load snapshot (normalize), nhưng khi commit FE ghi `entity_wiki`. -- `editor_feature_collection` là nguồn để render editor/map. Các join table (`geometry_entity`, `entity_wiki`) là nguồn quan hệ. - -## 2) Types (TypeScript) - Đúng Theo FE Hiện Tại - -### 2.1 GeoJSON (editor_feature_collection) - -```ts -export type Geometry = - | { type: "Point"; coordinates: [number, number] } - | { type: "MultiPoint"; coordinates: [number, number][] } - | { type: "LineString"; coordinates: [number, number][] } - | { type: "MultiLineString"; coordinates: [number, number][][] } - | { type: "Polygon"; coordinates: [number, number][][] } - | { type: "MultiPolygon"; coordinates: [number, number][][][] }; - -export type FeatureId = string | number; - -export type FeatureProperties = { - id: FeatureId; - type?: string | null; - geometry_preset?: string | null; - time_start?: number | null; - time_end?: number | null; - binding?: string[]; - - // UI-only / legacy fields (FE sẽ strip khi persist snapshot): - entity_id?: string | null; - entity_ids?: string[]; - entity_name?: string | null; - entity_names?: string[]; - entity_type_id?: string | null; -}; - -export type Feature = { - type: "Feature"; - properties: FeatureProperties; - geometry: Geometry; -}; - -export type FeatureCollection = { - type: "FeatureCollection"; - features: Feature[]; -}; -``` - -### 2.2 Snapshot rows - -```ts -export type SnapshotSource = "inline" | "ref"; - -export type EntitySnapshotOperation = "create" | "update" | "delete" | "reference"; -export type GeometrySnapshotOperation = "create" | "update" | "delete" | "reference"; -export type WikiSnapshotOperation = "create" | "update" | "delete" | "reference"; - -export type EntitySnapshot = { - id: string; - source: SnapshotSource; - operation?: EntitySnapshotOperation; - name?: string; - slug?: string | null; - description?: string | null; - status?: number | null; - base_updated_at?: string; - base_hash?: string; -}; - -export type GeometrySnapshot = { - id: string; - source: SnapshotSource; - operation?: GeometrySnapshotOperation; - type?: string | null; - draw_geometry?: Geometry; - geometry?: Geometry; // legacy - binding?: string[]; - time_start?: number | null; - time_end?: number | null; - bbox?: { - min_lng: number; - min_lat: number; - max_lng: number; - max_lat: number; - } | null; - base_updated_at?: string; - base_hash?: string; -}; - -// FE stores wiki doc as a string (commonly HTML; in some flows it may be a JSON-stringified editor payload). -export type WikiDoc = string | null; - -export type WikiSnapshot = { - id: string; - source: SnapshotSource; - operation?: WikiSnapshotOperation; - title: string; - slug?: string | null; - doc: WikiDoc; - updated_at?: string; -}; -``` - -### 2.3 Join tables - -```ts -export type GeometryEntitySnapshot = { - geometry_id: string; - entity_id: string; - base_links_hash?: string; -}; - -export type EntityWikiLinkSnapshot = { - entity_id: string; - wiki_id: string; - operation?: "reference" | "binding" | "delete"; -}; -``` - -## 3) Quy Ước FE Khi Build Snapshot (buildEditorSnapshot) - -### 3.1 Feature.properties entity fields bị strip - -Khi persist snapshot, FE chủ động xoá các field denormalize trên feature properties: -`entity_id`, `entity_ids`, `entity_name`, `entity_names`, `entity_type_id`. - -Quan hệ geometry ↔ entity chỉ nằm ở `geometry_entity[]`. - -### 3.2 entities[] - -FE cố gắng đảm bảo mọi entity có `name` không rỗng (fallback sang `id`) và có `source`. - -`operation` được dùng như "delta" trong commit: - -- `"create"|"update"|"delete"`: thay đổi record entity -- `"reference"`: đưa entity vào context snapshot (pin/link) nhưng commit không sửa record entity - -### 3.3 geometries[] - -FE sinh 1 `GeometrySnapshot` cho mỗi feature đang tồn tại trong `editor_feature_collection.features[]`: - -- `id = String(feature.properties.id)` -- `source:"inline"` -- `draw_geometry = feature.geometry` -- `binding`, `time_start`, `time_end`, `bbox` (nếu tính được) -- `type`: FE hiện gửi **string code** (geo_type smallint) dưới dạng string -- `operation`: - - `"create"` nếu geometry mới - - `"update"` nếu geometry thay đổi - - `undefined` nếu geometry không đổi - -Nếu feature bị xoá khỏi draft, FE thêm 1 row: - -```json -{ "id": "…", "source": "ref", "operation": "delete" } -``` - -### 3.4 geometry_entity[] - -`geometry_entity` là danh sách quan hệ many-to-many geometry ↔ entity. Mỗi row là một cặp: - -```ts -{ geometry_id: string; entity_id: string } -``` - -### 3.5 wikis[] - -- Wiki `source:"ref"` (được add từ search): FE set `operation:"reference"` và `doc:null`. -- Wiki `source:"inline"` (được tạo/sửa trong editor): - - nếu UI set explicit `create|update|delete` thì giữ nguyên - - nếu không có operation: - - wiki mới: FE coi là `"create"` - - wiki cũ không đổi: FE gán `"reference"` - - wiki cũ có đổi nội dung: FE gán `"update"` - -### 3.6 entity_wiki[] - -Type trong FE cho UI state cho phép `"binding"` và `"delete"`. - -Khi build snapshot để commit, FE map link “đang bật” về `"reference"` để tương thích với backend (một số backend chỉ chấp nhận `"reference"|"delete"`). - -## 4) Ví Dụ snapshot_json (rút gọn) - -```json -{ - "editor_feature_collection": { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "properties": { "id": "019e…", "type": "country", "time_start": 1000, "time_end": 1500 }, - "geometry": { "type": "Polygon", "coordinates": [[[100, 10], [101, 10], [101, 11], [100, 10]]] } - } - ] - }, - "entities": [ - { "id": "019e…", "source": "inline", "operation": "reference", "name": "ent1", "description": null, "status": 1 } - ], - "geometries": [ - { "id": "019e…", "source": "inline", "operation": "update", "type": "9", "draw_geometry": { "type": "Polygon", "coordinates": [] }, "binding": [], "time_start": 1000, "time_end": 1500, "bbox": null } - ], - "geometry_entity": [ - { "geometry_id": "019e…", "entity_id": "019e…" } - ], - "wikis": [ - { "id": "019e…", "source": "ref", "operation": "reference", "title": "Existing wiki", "doc": null, "updated_at": "2026-05-08T00:00:00.000Z" } - ], - "entity_wiki": [ - { "entity_id": "019e…", "wiki_id": "019e…", "operation": "reference" } - ] -} -``` - -## 5) Compat Notes (khi load snapshot cũ) - -FE normalize khi load snapshot: - -- Nếu thấy `entity_wikis` (plural) sẽ đọc như `entity_wiki`. -- Nếu join link có `operation:"reference"` thì FE coi như link active (UI biểu diễn như “binding”). diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx index b3434c2..ed0cf3d 100644 --- a/src/app/editor/[id]/page.tsx +++ b/src/app/editor/[id]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction, type PointerEvent as ReactPointerEvent } from "react"; -import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import Map from "@/uhm/components/Map"; import Editor from "@/uhm/components/Editor"; import BackgroundLayersPanel from "@/uhm/components/editor/BackgroundLayersPanel"; @@ -70,11 +70,8 @@ const DEFAULT_EDITOR_USER_ID = "local-editor"; export default function Page() { const params = useParams(); const router = useRouter(); - const searchParams = useSearchParams(); const projectId = String(params.id || ""); const openedProjectIdRef = useRef(null); - const autoOpenWiki = searchParams.get("only") === "wiki"; - const wikiOnly = autoOpenWiki; const [blockedPendingSubmissionId, setBlockedPendingSubmissionId] = useState(null); const [searchKind, setSearchKind] = useState("entity"); const [searchQuery, setSearchQuery] = useState(""); @@ -423,8 +420,6 @@ export default function Page() { internalSetMode(m); }, [internalSetMode]); - const onSetMode = setMode; - const effectiveGeometryVisibility = useMemo(() => { const visibility: Record = { ...geometryVisibility }; @@ -1360,7 +1355,7 @@ export default function Page() { ) : null} - {!wikiOnly && !blockedPendingSubmissionId ? ( + {!blockedPendingSubmissionId ? (
{isBackgroundVisibilityReady ? ( )}
- ) : blockedPendingSubmissionId ? null : ( - // Wiki-only mode: avoid mounting Map/Timeline (WebGL + geometry fetching) to reduce lag. -
- )} + ) : null} {mode !== "replay" ? ( <> @@ -1676,7 +1668,6 @@ export default function Page() { projectId={projectId} wikis={snapshotWikis} setWikis={setSnapshotWikisUndoable} - autoOpen={autoOpenWiki} requestedActiveId={requestedActiveWikiId} /> @@ -1686,7 +1677,7 @@ export default function Page() { links={snapshotEntityWikiLinks} setLinks={setSnapshotEntityWikiLinksUndoable} /> - {!wikiOnly && selectedFeature ? ( + {selectedFeature ? ( router.push(`/editor/${id}`)}> Mo editor -
diff --git a/src/app/user/projects/page.tsx b/src/app/user/projects/page.tsx index e9f29d9..279c908 100644 --- a/src/app/user/projects/page.tsx +++ b/src/app/user/projects/page.tsx @@ -380,21 +380,6 @@ export default function ProjectsPage() { -
- - - Wiki Editor - -
))} @@ -491,4 +476,4 @@ export default function ProjectsPage() { ); -} \ No newline at end of file +} diff --git a/src/uhm/components/wiki/WikiSidebarPanel.tsx b/src/uhm/components/wiki/WikiSidebarPanel.tsx index ddb7dac..2ec2c16 100644 --- a/src/uhm/components/wiki/WikiSidebarPanel.tsx +++ b/src/uhm/components/wiki/WikiSidebarPanel.tsx @@ -48,7 +48,6 @@ type Props = { projectId: string; wikis: WikiSnapshot[]; setWikis: React.Dispatch>; - autoOpen?: boolean; requestedActiveId?: string | null; }; @@ -57,7 +56,7 @@ function clampTitle(title: string) { return t.length ? t.slice(0, 120) : "Untitled wiki"; } -export default function WikiSidebarPanel({ projectId, wikis, setWikis, autoOpen, requestedActiveId }: Props) { +export default function WikiSidebarPanel({ projectId, wikis, setWikis, requestedActiveId }: Props) { const [open, setOpen] = useState(false); const [activeId, setActiveId] = useState(null); const [collapsed, setCollapsed] = useState(false); @@ -93,12 +92,6 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, autoOpen, const globalWikiSearchRequestRef = useRef(0); const importFileInputRef = useRef(null); - useEffect(() => { - if (!autoOpen) return; - // open once on mount - setOpen(true); - }, [autoOpen]); - // Allow Quill to keep wiki links where href is a slug (no scheme). useEffect(() => { if (typeof window === "undefined") return; diff --git a/src/uhm/types/commit_snapshot.ts b/src/uhm/doc/commit_snapshot.ts similarity index 85% rename from src/uhm/types/commit_snapshot.ts rename to src/uhm/doc/commit_snapshot.ts index 2a731ac..ed0f13a 100644 --- a/src/uhm/types/commit_snapshot.ts +++ b/src/uhm/doc/commit_snapshot.ts @@ -1,3 +1,14 @@ +/** + * Tài liệu schema tham chiếu cho snapshot commit. + * + * Lưu ý: + * - Đây không phải "single source of truth" của runtime hiện tại; logic normalize/build thật nằm ở + * `src/uhm/lib/editor/snapshot/editorSnapshot.ts`. + * - Các phần như `replays` và nhóm `UIFunctionName` / `MapFunctionName` mô tả schema dự phòng hoặc tương lai. + * Editor route `/editor/[id]` hiện có mode `replay`, nhưng chưa thực thi hệ thống scripted replay đầy đủ theo file này. + * - Các field denormalized dùng cho UI như `entity_ids`, `entity_name`, `binding`, `time_start`, `time_end` + * có thể xuất hiện trong editor runtime, nhưng frontend sẽ strip hoặc tái sinh chúng khi build snapshot API. + */ // ---- Root request ---- diff --git a/src/uhm/doc/developer_guide.md b/src/uhm/doc/developer_guide.md new file mode 100644 index 0000000..cb7ca6b --- /dev/null +++ b/src/uhm/doc/developer_guide.md @@ -0,0 +1,191 @@ +# UHM Editor - developer guide thực dụng + +Tài liệu này dành cho người sửa editor hiện tại, không phải mô tả kiến trúc lý tưởng. + +## 1. Entry points quan trọng + +- `src/app/editor/[id]/page.tsx` + - orchestration chính của project editor +- `src/uhm/components/Map.tsx` + - container cho map và các hook map +- `src/uhm/lib/editor/state/useEditorState.ts` + - draft geometry + diff + undo +- `src/uhm/lib/editor/state/useEditorSessionState.ts` + - session/UI/project/wiki/entity state +- `src/uhm/lib/editor/snapshot/editorSnapshot.ts` + - normalize snapshot từ backend và build snapshot gửi ngược lại backend + +Nếu chưa đọc 5 file này, chưa nên sửa behavior lớn của editor. + +## 2. Cấu trúc thư mục nên ưu tiên hiểu + +- `src/uhm/components/editor/` + - panel UI bên trái/phải +- `src/uhm/components/wiki/` + - wiki editor và wiki viewer/sidebar +- `src/uhm/components/map/` + - hooks tích hợp MapLibre +- `src/uhm/lib/map/engines/` + - logic interaction theo mode +- `src/uhm/lib/editor/session/` + - các nhóm session state +- `src/uhm/lib/editor/draft/` + - draft diff và undo +- `src/uhm/lib/editor/snapshot/` + - schema conversion / snapshot semantics + +## 3. Cách editor thật sự vận hành + +Editor có 3 tầng dữ liệu: + +1. `baselineSnapshot` + - snapshot gốc của session +2. `initialData` + - `FeatureCollection` rehydrate từ snapshot đó +3. `draft` + - working copy để user sửa trên map + +Khi commit: + +- geometry đi từ `draft` +- entity/wiki/link đi từ snapshot collections +- `buildEditorSnapshot()` quyết định operation nào là `reference`, `binding`, `update`, `delete` + +Đừng tự build payload ở component nếu chưa hiểu file `editorSnapshot.ts`. + +## 4. Khi thêm mode/tool mới + +Checklist an toàn: + +1. Thêm mode vào `sessionTypes.ts`. +2. Thêm button vào `ToolsPanel.tsx`. +3. Nếu mode cần preview source/layer mới, thêm vào `setupMapLayers()`. +4. Nối mode với engine trong `useMapInteraction.ts`. +5. Nếu tool tạo geometry mới, chọn default: + - `type` + - `geometry_preset` + - `entity_ids` + - `binding` +6. Kiểm tra interaction cleanup khi chuyển mode. + +Nếu mode chưa được cleanup đúng, map rất dễ giữ preview cũ hoặc event listener cũ. + +## 5. Khi thêm geotype mới + +Checklist ngắn: + +1. Cập nhật `geoTypeMap` nếu cần mapping backend code <-> key. +2. Cập nhật `geometryTypeOptions.ts`. +3. Tạo style file trong `styles/geotypes/`. +4. Register ở `geotypeLayers.ts`. +5. Kiểm tra point icon hoặc label pipeline nếu type mới là point/route/polygon label. + +Nếu chỉ sửa `geometryTypeOptions.ts` mà quên style registry, UI sẽ cho chọn type nhưng map không render đúng. + +## 6. Khi sửa snapshot semantics + +File quan trọng nhất là `editorSnapshot.ts`. + +Ở đó đang có hai hướng xử lý khác nhau: + +- `normalizeEditorSnapshot(raw)` + - đọc payload từ backend + - rehydrate fields UI như `entity_ids`, `entity_name`, `binding`, `time_start`, `time_end` +- `buildEditorSnapshot(options)` + - strip các field generate-only khỏi `editor_feature_collection` + - build `geometry_entity[]` và `entity_wiki[]` + - tính operation phù hợp + +Nguyên tắc: + +- feature trong editor có thể mang field denormalized để UI dễ dùng +- payload gửi backend thì không nên mang những field denormalized đó + +## 7. Khi sửa wiki editor + +Wiki project editor hiện là Quill, không phải Tiptap. + +Các file nên đọc trước: + +- `WikiSidebarPanel.tsx` +- `PublicWikiSidebar.tsx` + +Các điểm dễ làm hỏng: + +- sanitize link của Quill +- compatibility với doc dạng HTML/Tiptap JSON/plain text +- slug links nội bộ +- sentinel `__missing__` + +Nếu thay storage format, phải sửa cả editor lẫn viewer compatibility path. + +## 8. Những key localStorage thật sự đang dùng + +- `uhm.backgroundLayerVisibility.v1` +- `uhm:mapProjection` + +Hiện không có local draft autosave toàn editor. +Đừng dựa vào doc cũ hoặc giả định rằng F5 sẽ hồi lại draft geometry/wiki/entity. + +## 9. Restore commit hiện là FE-only + +`CommitHistoryPanel -> Restore`: + +- load snapshot từ commit cũ +- reset editor state ở frontend +- không đổi head commit trên backend + +Nếu muốn restore server-side thật, cần dùng endpoint backend riêng và sửa cả UI wording. + +## 10. Pending submission lock là rule thật + +`openSectionEditor()` chủ động chặn project có `PENDING` submission. + +Nghĩa là: + +- không nên "lách" UI để cho sửa tiếp +- nếu đổi behavior này, phải thống nhất với backend contract + +## 11. Performance và state hygiene + +Một số nguyên tắc nên giữ: + +- dùng `draftRef`/refs trong map engines để tránh rebind handler vô ích +- giữ component panel càng dumb càng tốt, logic patch state đặt ở page/hooks +- khi cần undo cho entity/wiki/link, đi qua `editor.setSnapshot*()` để undo stack biết +- hạn chế thêm `JSON.stringify` compare ở chỗ nóng nếu chưa đo hiệu năng + +## 12. Chỗ dễ gây hiểu nhầm khi debug + +### Geometry biến mất + +Có thể do: + +- timeline filter +- geometry visibility theo type +- binding filter +- replay hide outside + +Không phải lúc nào cũng là bug render layer. + +### Commit count lạ + +`Commit (N)` là `pendingSaveCount`, không phải số mutation backend. + +### Selection mất + +Khi timeline filter làm geometry đang chọn không còn visible, page sẽ tự cắt `selectedFeatureIds`. + +## 13. Nên test gì sau khi sửa + +Ít nhất nên test thủ công: + +1. mở project có commit cũ +2. tạo geometry mới bằng mode liên quan +3. sửa metadata geometry +4. bind entity và geometry +5. tạo/sửa wiki +6. link entity-wiki +7. commit +8. restore từ commit cũ +9. mở project có pending submission nếu đang debug flow đó diff --git a/src/uhm/doc/editor_features.md b/src/uhm/doc/editor_features.md new file mode 100644 index 0000000..9c52ee3 --- /dev/null +++ b/src/uhm/doc/editor_features.md @@ -0,0 +1,297 @@ +# UHM Editor - tính năng hiện có + +Tài liệu này mô tả editor đang chạy tại `src/app/editor/[id]/page.tsx` và các panel liên quan trong `src/uhm/components/`. +Mục tiêu của tài liệu là phản ánh đúng implementation hiện tại, không mô tả các tính năng chưa được nối dây. + +## 1. Cách mở editor + +- `GET /editor/[id]`: mở editor đầy đủ với map, panel trái và panel phải. + +## 2. Bố cục giao diện + +- Cột trái (`Editor.tsx`) + - `ProjectPanel` + - `ToolsPanel` + - `CommitPanel` + - `CommitHistoryPanel` + - `UndoListPanel` +- Khu vực giữa + - `Map` + - `TimelineBar` khi không ở `replay` +- Cột phải (`BackgroundLayersPanel`) + - Search hợp nhất + - Geometry Binding + - Entities + - Wiki + - Entity ↔ Wiki + - Selected Geometry + +Hai cột hai bên đều resize được bằng drag handle. + +## 3. Editor modes + +`EditorMode` hiện có: + +- `idle` +- `select` +- `draw` +- `add-point` +- `add-line` +- `add-path` +- `add-circle` +- `replay` + +Ý nghĩa thực tế: + +- `select`: chọn geometry, xóa geometry, mở vertex editing cho polygon/circle, vào replay. +- `draw`: vẽ polygon. +- `add-point`: tạo point. +- `add-line`: vẽ `LineString`. +- `add-path`: vẽ `LineString` có render arrow layer cho route. +- `add-circle`: kéo chuột để tạo polygon hình tròn, có `circle_center` và `circle_radius`. +- `replay`: hiện là chế độ tập trung vào một geometry và các geometry trong `binding`; chưa có hệ thống script replay UI/map như file schema tham chiếu. + +## 4. Công cụ vẽ và phím điều khiển + +### Polygon (`draw`) + +- Click để thêm đỉnh. +- `Shift` hoặc `Alt` khi click/move để snap vào geometry gần nhất. +- `Enter` để hoàn tất polygon. +- `Escape` để hủy. +- `Backspace` để bỏ đỉnh cuối. + +Geometry mới mặc định có: + +- `type: "country"` +- `geometry_preset: "polygon"` +- `entity_ids: []` +- `binding: []` + +### Point (`add-point`) + +- Click một lần để tạo point. +- Geometry mới mặc định có `type: "city"` và `geometry_preset: "point"`. + +### Line (`add-line`) + +- Click để thêm đỉnh. +- `Enter` để hoàn tất. +- `Escape` để hủy. +- `Backspace` để bỏ đỉnh cuối. + +Geometry mới mặc định có `type: "defense_line"` và `geometry_preset: "line"`. + +### Path (`add-path`) + +- Tương tự `add-line`, nhưng render preview và layer theo route/path. +- Geometry mới mặc định có `type: "attack_route"` và `geometry_preset: "line"`. + +### Circle (`add-circle`) + +- `mousedown` để đặt tâm. +- Kéo chuột để thay đổi bán kính. +- `mouseup` để hoàn tất. +- `Escape` để hủy. + +Geometry trả về vẫn là `Polygon`, nhưng có thêm: + +- `circle_center` +- `circle_radius` + +Mặc định `type: "war"` và `geometry_preset: "circle-area"`. + +## 5. Chọn và sửa geometry + +### Selection + +- `Map` trả về danh sách `selectedFeatureIds`. +- `SelectedGeometryPanel`, `ProjectEntityRefsPanel` và `GeometryBindingPanel` đều đọc từ selection này. +- Multi-select có tồn tại ở level state, nhưng một số thao tác chỉ hợp lệ khi các geometry cùng shape. + +### Vertex editing + +Khi đang ở `select`, editor có thể sửa polygon/circle qua `editingEngine`. + +- Kéo handle để đổi vị trí đỉnh. +- Với circle: + - handle `0`: dời tâm + - handle `1`: đổi bán kính +- `Ctrl` hoặc `Cmd` + click lên đường edit để chèn thêm đỉnh mới cho polygon. +- `Enter` để áp dụng chỉnh sửa. +- `Escape` để hủy chỉnh sửa. + +### Xóa geometry + +- Hành động xóa được đi qua `onDeleteFeature`. +- Undo có thể khôi phục lại geometry vừa xóa. + +## 6. Metadata geometry + +`SelectedGeometryPanel` hiện cho phép sửa: + +- `type_key` +- `time_start` +- `time_end` + +`binding` đang được hiển thị trong state form nhưng không có input edit trực tiếp trong panel; việc bind/unbind geometry hiện đi qua `GeometryBindingPanel`. + +Các ràng buộc đang có: + +- `time_start` và `time_end` phải parse được thành số hoặc để trống. +- Nếu cả hai đều có giá trị thì `time_start <= time_end`. + +Khi apply, editor patch trực tiếp `feature.properties` của geometry đang chọn. + +## 7. Timeline + +`TimelineBar` hiện dùng dải năm cố định từ util timeline. + +- Slider + numeric input cùng điều khiển `timelineDraftYear`. +- Có toggle `filterEnabled`. +- Khi bật filter: + - geometry đã có trong baseline chỉ hiện nếu năm hiện tại nằm trong `[time_start, time_end]` + - geometry mới tạo trong session vẫn được giữ visible + +Timeline hiện là filter phía client, không fetch lại dữ liệu project theo năm. + +## 8. Search hợp nhất và import + +Panel phải có `UnifiedSearchBar` với 3 loại search: + +- `entity` + - tìm local + backend theo tên/mô tả + - nút `Add` sẽ thêm entity vào `snapshotEntities` dưới dạng `reference` +- `wiki` + - tìm backend theo title + - nút `Add` sẽ thêm wiki vào `snapshotWikis` dưới dạng `reference` +- `geo` + - tìm geometry theo tên entity + - nút `Import` sẽ import geometry vào draft hiện tại + - đồng thời thêm entity tương ứng vào `snapshotEntities` nếu chưa có + - import sẽ tự tắt timeline filter để geometry mới import không bị ẩn + +## 9. Entity và binding + +### Project entities + +`ProjectEntityRefsPanel` hỗ trợ: + +- tạo entity local (`source: "inline"`, `operation: "create"`) +- sửa entity đã có trong snapshot +- bind/unbind entity vào geometry đang chọn + +Editor không gọi API create entity riêng ở bước này. Entity mới chỉ sống trong snapshot cho tới khi commit project. + +### Geometry ↔ Entity + +Liên kết nhiều-nhiều được thể hiện bằng: + +- field UI trên feature: `entity_id`, `entity_ids`, `entity_name`, `entity_names` +- payload snapshot: `geometry_entity[]` + +Panel `ProjectEntityRefsPanel` là nơi bind/unbind entity theo geometry đang chọn. + +### Geometry ↔ Geometry + +`GeometryBindingPanel` thao tác trên `feature.properties.binding`. + +- Chọn một geometry làm gốc. +- Bind/unbind với geometry khác trong project. +- Có nút focus để zoom vào geometry trong list binding. +- Có toggle `Filter`: map chỉ hiển thị geometry liên quan tới selection nếu filter binding đang bật. + +## 10. Wiki và entity-wiki + +### Wiki panel + +`WikiSidebarPanel` dùng `react-quill-new`. + +Các khả năng đang có: + +- tạo wiki local +- sửa title/slug/doc +- import HTML file +- export nội dung hiện tại theo định dạng suy ra từ `doc` +- lưu wiki vào `snapshotWikis` + +Storage thực tế của `doc`: + +- format mới: HTML string +- format cũ tương thích: Tiptap JSON string +- plaintext fallback + +### Internal wiki link + +Toolbar `link` mở modal custom: + +- tìm wiki local theo title/slug +- tìm wiki global từ server +- chèn link bằng `slug`, không bắt buộc scheme URL +- có thể tạo `__missing__` link để đánh dấu liên kết chưa map được + +### Entity ↔ Wiki + +`EntityWikiBindingsPanel` quản lý `snapshotEntityWikiLinks`. + +- link mới dùng `operation: "binding"` +- unlink bằng cách remove row khỏi editor state +- khi build snapshot, editor tự sinh delta `binding` hoặc `delete` so với baseline + +## 11. Commit, submit và restore + +### Pending change count + +Số trong nút `Commit` không chỉ là geometry diff. Nó gồm: + +- `editor.changeCount` +- `+1` nếu danh sách wiki dirty +- `+1` nếu danh sách entity dirty +- `+1` nếu danh sách entity-wiki dirty + +### Commit + +`commitSection()`: + +- build snapshot từ `draft` + `snapshotEntities` + `snapshotWikis` + `snapshotEntityWikiLinks` +- gửi `snapshot_json` lên API tạo commit +- nếu thành công: + - reset baseline sang snapshot vừa commit + - clear undo stack + - clear geometry changes + +### Submit + +- chỉ submit được khi project có `head_commit_id` +- không submit nếu còn thay đổi chưa commit + +### Restore + +`CommitHistoryPanel` có nút `Restore`, nhưng restore hiện là: + +- load snapshot từ commit cũ vào FE +- không đổi head commit trên backend + +Đây là FE-only restore để tiếp tục chỉnh sửa từ snapshot cũ. + +## 12. Pending submission lock + +Khi `openSectionEditor()` thấy project có submission `PENDING`, editor bị chặn mở. + +UI hiện tại: + +- hiển thị màn hình lock +- cho phép xóa pending submission để unlock + +Luồng này bám sát rule backend mới, không phải readonly mode giả lập ở FE. + +## 13. Những thứ doc cũ từng nhắc nhưng code hiện chưa có + +Các mục sau không nên xem là tính năng hiện hành của editor: + +- autosave toàn bộ draft editor vào `localStorage` +- restore head commit trên backend từ UI editor +- import/export wiki JSON chuyên biệt như một workflow riêng +- bộ shortcut toàn cục kiểu `Ctrl+S`, `Ctrl+Z`, `Ctrl+Y` +- workflow duyệt `Approved/Rejected` được render đầy đủ trong editor page +- hệ thống replay script theo `replays[]` trong schema snapshot diff --git a/src/uhm/doc/editor_states.md b/src/uhm/doc/editor_states.md new file mode 100644 index 0000000..190e4e2 --- /dev/null +++ b/src/uhm/doc/editor_states.md @@ -0,0 +1,280 @@ +# UHM Editor - state và vòng đời dữ liệu + +Tài liệu này mô tả state thật đang được dùng bởi editor hiện tại. +Entry point chính là `useEditorSessionState()` và `useEditorState()`. + +## 1. Hai lớp state chính + +Editor đang tách làm hai khối: + +- `useEditorSessionState()` + - state UI, session, form, project, timeline, background, wiki +- `useEditorState(initialData, snapshotUndo)` + - state draft hình học, diff và undo + +Nói ngắn gọn: + +- `session state` quyết định editor đang nhìn cái gì và panel đang thao tác gì +- `editor state` quyết định geometry nào đang tồn tại trong draft và khác baseline ra sao + +## 2. State geometry trung tâm + +### `initialData` + +- Nằm ở `useEditorSessionState()` +- Là `FeatureCollection` đang được nạp vào editor khi mở project hoặc restore commit +- Khi thay đổi, `useEditorState()` sẽ reset toàn bộ draft và baseline tương ứng + +### `draft` + +- Nằm trong `useEditorState()` +- Là nguồn dữ liệu render trực tiếp cho `Map` +- Mọi thao tác create/update/delete geometry đều đi qua đây + +### `draftRef` + +- Bản ref của `draft` +- Được dùng trong event handlers của map engine để luôn đọc được state mới nhất mà không phải rebind callback liên tục + +### `initialMapRef` + +- `Map` tạo từ `initialData` +- Là baseline để tính diff giữa draft hiện tại và dữ liệu gốc của session + +### `changes` + +- Kết quả `diffDraftToInitial(draft, initialMapRef.current)` +- Map theo `feature.properties.id` +- Mỗi phần tử có thể là: + - `create` + - `update` + - `delete` + +Lưu ý: diff hiện chỉ là cơ chế nhận biết geometry nào đã thay đổi so với baseline. Snapshot commit thực tế vẫn được build từ toàn bộ `draft` cộng với các snapshot bảng phụ. + +### `changeCount` + +- Số lượng geometry thay đổi hiện tại +- Được cộng thêm dirty state của wiki/entity/entity-wiki để tạo `pendingSaveCount` + +## 3. Undo state + +Undo được quản lý bởi `useUndoStack()`. + +Kiểu action hiện có: + +- `create` +- `delete` +- `update` +- `properties` +- `snapshot_entities` +- `snapshot_wikis` +- `snapshot_entity_wiki` +- `group` + +Ý nghĩa: + +- geometry create/delete/update/properties undo được trực tiếp trên `draft` +- snapshot entity/wiki/link undo được apply qua `snapshotUndo` API truyền vào `useEditorState` +- `group` dùng để gom nhiều thay đổi thành một thao tác undo logic + +Editor hiện có `undo`, nhưng chưa có redo. + +## 4. Session state theo nhóm + +### 4.1. Mode và selection + +- `mode: EditorMode` +- `selectedFeatureIds` +- `selectedGeometryEntityIds` + +`selectedFeatureIds` là state gốc cho: + +- panel metadata geometry +- bind entity +- bind geometry +- focus geometry từ search/binding panel + +### 4.2. Form state + +- `entityForm` + - dùng cho form tạo entity local +- `geometryMetaForm` + - `type_key` + - `time_start` + - `time_end` + - `binding` + +`geometryMetaForm.binding` hiện chủ yếu là giá trị hiển thị/đồng bộ UI, còn chỉnh sửa binding thật đi qua `GeometryBindingPanel`. + +### 4.3. Project/session task state + +`useProjectSessionState()` gom các cờ async vào một state machine nhỏ: + +- `sectionTask: "idle" | "saving" | "submitting" | "opening-project"` + +Từ đó sinh ra: + +- `isSaving` +- `isSubmitting` +- `isOpeningSection` + +Ngoài ra còn có: + +- `activeSection` +- `projectState` +- `sectionCommits` +- `baselineSnapshot` +- `commitTitle` + +### 4.4. Timeline state + +`useTimelineState()` giữ: + +- `timelineYear` +- `timelineDraftYear` +- `isTimelineLoading` +- `timelineStatus` + +Trong page hiện tại, timeline filter đang dùng `timelineDraftYear`. +Không có fetch dữ liệu project theo `timelineYear`; timeline đang là client-side visibility filter. + +### 4.5. Background/session UI + +`useBackgroundSessionState()` giữ: + +- `backgroundVisibility` +- `isBackgroundVisibilityReady` + +Giá trị thật được load từ `localStorage` key `uhm.backgroundLayerVisibility.v1`. + +### 4.6. Wiki/session state + +`useWikiSessionState()` giữ: + +- `snapshotWikis` +- `snapshotEntityWikiLinks` + +Đây là single source of truth cho phần wiki trong snapshot commit. + +## 5. Snapshot state + +Editor đang làm việc với 3 snapshot collection chính ngoài geometry: + +- `snapshotEntities` +- `snapshotWikis` +- `snapshotEntityWikiLinks` + +Chúng đại diện cho "current session snapshot", không phải danh sách delta thô. + +Ví dụ: + +- entity ref được giữ bằng `operation: "reference"` +- entity/wiki local mới tạo có thể mang `operation: "create"` +- link entity-wiki mới tạo dùng `operation: "binding"` + +Khi commit, `buildEditorSnapshot()` sẽ so với `baselineSnapshot` để chuyển các collection này thành snapshot đúng semantic cho backend. + +## 6. Baseline snapshot là gì + +`baselineSnapshot` là snapshot đang được xem như gốc của session hiện tại. + +Nó được cập nhật khi: + +- mở project +- commit thành công +- restore từ một commit + +`baselineSnapshot` được dùng để: + +- biết link nào là `reference`, link nào là `binding`, link nào là `delete` +- biết wiki/entity nào là thay đổi thực sự so với snapshot trước +- giữ lại inline entity/wiki từ snapshot trước nếu user chưa xóa chúng + +## 7. Derived state quan trọng trong page + +### `timelineVisibleDraft` + +- là `draft` đã qua filter timeline nếu `timelineFilterEnabled = true` +- geometry mới tạo trong session không bị timeline filter ẩn + +### `snapshotEntitiesVisible` + +- loại bỏ các row `delete` +- dedupe theo `id` + +### `selectedFeatures` + +- map từ `selectedFeatureIds` sang feature thật trong `editor.draft.features` + +### `isMultiEditValid` + +- chỉ `true` khi tất cả geometry đang chọn cùng `geometry.type` +- một số thao tác bind sẽ chặn nếu giá trị này là `false` + +### `pendingSaveCount` + +Được tính như sau: + +- `editor.changeCount` +- `+1` nếu wiki dirty +- `+1` nếu entities dirty +- `+1` nếu entity-wiki dirty + +Đây là con số dùng trong UI commit, không phải số record backend chắc chắn sẽ thay đổi. + +## 8. Dirty detection + +Dirty check của: + +- `snapshotWikis` +- `snapshotEntities` +- `snapshotEntityWikiLinks` + +đều đang làm bằng cách normalize trước rồi so `JSON.stringify`. + +Điều này đủ thực dụng cho snapshot cỡ vừa, nhưng cần lưu ý: + +- không tối ưu cho dữ liệu rất lớn +- phụ thuộc vào tính ổn định của thứ tự mảng sau normalize + +## 9. State được persist vào localStorage + +Hiện editor chỉ persist hai nhóm nhỏ: + +- background layer visibility + - key: `uhm.backgroundLayerVisibility.v1` +- map projection + - key: `uhm:mapProjection` + +Editor hiện không persist toàn bộ draft/project snapshot vào localStorage. +Nếu cần autosave local draft, đó là tính năng phải làm thêm, không phải behavior hiện tại. + +## 10. Khi nào state bị reset + +### Reset toàn phần + +Xảy ra khi: + +- mở project khác +- mở lại project +- restore commit + +Hiệu ứng: + +- `initialData` đổi +- `useEditorState()` reset `draft` +- `undoStack` bị clear +- baseline map được build lại + +### Reset cục bộ + +- đổi selection có thể reset `geometryMetaForm` +- đóng/mở wiki modal không reset snapshot wiki, chỉ reset form local của modal + +## 11. Một số giới hạn hiện tại cần nhớ khi đọc code + +- có `undo`, chưa có `redo` +- timeline state có `timelineYear`, nhưng page hiện dùng `timelineDraftYear` cho filtering +- dirty count của commit không tương ứng một-một với số mutation backend +- map selection, binding filter và timeline filter đều là state client-side diff --git a/src/uhm/doc/map_engine.md b/src/uhm/doc/map_engine.md new file mode 100644 index 0000000..bf6464e --- /dev/null +++ b/src/uhm/doc/map_engine.md @@ -0,0 +1,200 @@ +# UHM Map engine - kiến trúc hiện tại + +Map editor hiện dùng `MapLibre GL` và được ghép từ 4 lớp chính: + +- `useMapInstance` +- `setupMapLayers` +- `useMapInteraction` +- `useMapSync` + +Container chính là `src/uhm/components/Map.tsx`. + +## 1. `useMapInstance` + +Phụ trách lifecycle của đối tượng `maplibregl.Map`. + +Các behavior đang có: + +- khởi tạo map với `getBaseMapStyle()` +- `center: [0, 20]`, `zoom: 2` +- áp `minZoom` và `maxZoom` +- lưu projection vào `localStorage` key `uhm:mapProjection` +- cho phép chuyển giữa: + - `mercator` + - `globe` +- theo dõi `zoomLevel` +- thử center theo geolocation một lần khi map load xong + +Nếu map init lỗi, `Map.tsx` render overlay lỗi thay vì crash im lặng. + +## 2. Base style và background layers + +`getBaseMapStyle()` dựng style MapLibre từ vector tile source `base`. + +Background layers hiện có: + +- `graticules-line` +- `land` +- `bg-countries-fill` +- `bg-country-borders-line` +- `country-labels` +- `regions-line` +- `lakes-fill` +- `rivers-line` +- `geolines-line` + +Visibility của các layer này đi qua `BackgroundLayerVisibility`. + +## 3. Sources mà editor đang dùng + +### Preview sources + +- `draw-preview` +- `draw-circle-preview` +- `draw-line-preview` +- `draw-path-preview` + +Chúng chỉ dùng cho hình preview trong lúc user đang vẽ. + +### Data sources + +- `countries` + - polygon + line-like features sau khi split/decorate +- `places` + - point features +- `PATH_ARROW_SOURCE_ID` + - shape phụ để render arrow cho path-like geometries +- `POLYGON_LABEL_SOURCE_ID` + - label source cho polygon names + +### Editing overlay + +- `edit-shape` +- `edit-handles` + +### Highlight/focus + +- `entity-focus` + +Source này dùng cho: + +- highlight geometry khi cần focus +- visual emphasis khi zoom từ search/binding panel + +## 4. Tách dữ liệu trước khi đẩy lên map + +`useMapSync()` chịu trách nhiệm: + +1. filter draft theo binding nếu `respectBindingFilter = true` +2. filter theo geometry visibility +3. split feature thành nhóm polygon/line/point +4. decorate line/polygon/point cho label rendering +5. build source riêng cho path arrows +6. set selected feature state + +Điểm quan trọng: + +- data mà map nhận không phải raw `draft` nguyên xi +- nó là `draft` sau khi đã qua visibility, binding filter và label decoration + +## 5. Map interaction layer + +`useMapInteraction()` nối editor mode với các engine. + +Binding hiện tại: + +- `draw` -> `initDrawing` +- `select` -> `initSelect` +- `replay` -> `initSelect` +- `add-line` -> `initLine` +- `add-path` -> `initPath` +- `add-circle` -> `initCircle` + +`add-point` được init riêng bằng `initPoint`, nhưng hiện chưa được đưa vào `engineBindingsRef` như các mode còn lại; logic create point vẫn được bind trong `setupMapInteractions`. + +## 6. Các engine cụ thể + +### `initDrawing` + +- vẽ polygon bằng chuỗi click +- preview fill + line +- hỗ trợ snap bằng `Shift` hoặc `Alt` + +### `initPoint` + +- tạo point bằng một click + +### `initLine` + +- tạo line nhiều đỉnh +- preview dashed line + +### `initPath` + +- giống line nhưng có path arrow layer khi preview/render + +### `initCircle` + +- tạo circle bằng kéo chuột +- kết quả cuối là `Polygon` có metadata circle + +### `createEditingEngine` + +- chỉ edit `Polygon` +- nếu polygon có `circle_center`, engine chuyển sang circle-edit mode +- hỗ trợ kéo handle và chèn thêm đỉnh bằng `Ctrl/Cmd` + +## 7. Chế độ `select` và `replay` + +`initSelect` hiện đóng nhiều vai trò: + +- chọn geometry +- xóa geometry +- bắt đầu edit geometry +- chuyển sang `replay` + +`replay` hiện không phải cinematic replay đầy đủ. +Nó là mode hiển thị tập trung vào một geometry: + +- có nút thoát replay +- có toggle `Hide Outside` +- có thể ẩn geometry ngoài danh sách `binding` + +## 8. Đồng bộ selection và feature state + +`useMapSync()` xóa feature state cũ trên các source liên quan, sau đó set lại `selected` cho `selectedFeatureIds`. + +Điều này giúp: + +- selected style trên map không bị stale +- selection vẫn đúng sau mỗi lần source data đổi + +## 9. Fit/focus behavior + +Map có hai kiểu focus khác nhau: + +- `fitToDraftBounds` + - dùng khi muốn fit toàn bộ draft +- `focusFeatureCollection` + `focusRequestKey` + - dùng khi zoom tới geometry cụ thể từ panel/search + +Focus này đi qua `fitMapToFeatureCollection(...)`. + +## 10. Geolocation + +Sau khi map load: + +- nếu chưa từng center theo geolocation trong session +- và không bật `fitToDraftBounds` +- và browser hỗ trợ geolocation + +thì map sẽ thử `navigator.geolocation.getCurrentPosition(...)` một lần để dời tâm người dùng. + +Nếu thất bại, map giữ nguyên center mặc định. + +## 11. Những điều cần nhớ khi sửa map engine + +- preview source/layer và persisted source/layer là hai tầng khác nhau +- `draftRef` được dùng để tránh closure stale trong event handlers +- `Map` chỉ là orchestration component; logic lớn nằm ở hooks +- geometry render pipeline phụ thuộc khá nhiều vào `mapUtils.ts`, không chỉ mỗi `useMapSync.ts` diff --git a/src/uhm/doc/map_styling.md b/src/uhm/doc/map_styling.md new file mode 100644 index 0000000..09e5ba2 --- /dev/null +++ b/src/uhm/doc/map_styling.md @@ -0,0 +1,182 @@ +# UHM map styling - hệ thống layer và style + +Tài liệu này mô tả styling thật đang được map editor dùng. + +## 1. Hai nhóm style chính + +Map hiện có hai nhóm style tách biệt: + +- background/base map style +- geotype style cho dữ liệu editor + +### Background/base map + +Định nghĩa trong `useMapLayers.ts` qua `getBaseMapStyle()`. + +### Geotype style + +Định nghĩa trong `src/uhm/lib/map/styles/`. + +## 2. Background layers đang có + +Danh sách layer toggle được expose ở `backgroundLayers.ts`: + +- `raster-base-layer` +- `graticules-line` +- `land` +- `bg-countries-fill` +- `bg-country-borders-line` +- `country-labels` +- `regions-line` +- `lakes-fill` +- `rivers-line` +- `geolines-line` + +Lưu ý: + +- không phải layer nào trong list cũng nhất thiết được add từ cùng một source path trong tương lai +- `BackgroundLayersPanel` chỉ biết toggle theo `id` + +Visibility mặc định: + +- tất cả `true` +- được persist bằng `uhm.backgroundLayerVisibility.v1` + +## 3. Geotype registry + +Geotype render hiện được tập trung ở `getAllGeotypeLayers(...)` trong `geotypeLayers.ts`. + +Các type đang được register: + +- `defense_line` +- `attack_route` +- `retreat_route` +- `invasion_route` +- `migration_route` +- `refugee_route` +- `trade_route` +- `shipping_route` +- `country` +- `state` +- `empire` +- `kingdom` +- `war` +- `battle` +- `civilization` +- `rebellion_zone` +- `person_deathplace` +- `person_birthplace` +- `person_activity` +- `temple` +- `capital` +- `city` +- `fortress` +- `castle` +- `ruin` +- `port` +- `bridge` + +`GEOMETRY_TYPE_OPTIONS` trong `geometryTypeOptions.ts` phải khớp với tập geotype này nếu muốn user chọn được từ UI. + +## 4. Type matching + +Style matcher trung tâm là: + +- `TYPE_MATCH_EXPR = ["coalesce", ["get", "type"], ["get", "entity_type_id"], ""]` + +Điều này cho phép layer match theo: + +- `feature.properties.type` +- fallback sang `entity_type_id` nếu cần + +Với editor hiện tại, `type` là field chính. + +## 5. Point, line, polygon và label sources + +Map không render mọi thứ từ một source duy nhất theo nghĩa trực tiếp. +Pipeline hiện tại tách ra: + +- `countries` + - polygon và line-like feature data +- `places` + - point data +- `PATH_ARROW_SOURCE_ID` + - arrow shapes cho route/path +- `POLYGON_LABEL_SOURCE_ID` + - polygon labels + +Label layer cho polygon/line đi qua: + +- `getAllGeotypeLabelLayers(...)` +- helper trong `shared/polygonLabels.ts` +- helper trong `shared/lineLabels.ts` + +## 6. Icon point + +Point geotype dùng icon pipeline trong: + +- `shared/pointStyle.ts` +- `ensurePointGeotypeIcons(map)` + +Điều này có nghĩa là khi thêm geotype point mới, chỉ thêm layer là chưa đủ; cần chắc icon/style builder cũng hiểu type mới đó. + +## 7. Preview và edit styling + +Ngoài style dữ liệu chính, map còn có style riêng cho: + +### Draw preview + +- `draw-preview-fill` +- `draw-preview-line` +- `draw-circle-preview-fill` +- `draw-circle-preview-line` +- `draw-line-preview-line` +- `draw-path-preview-line` +- `draw-path-preview-arrows` + +### Editing overlay + +- `edit-shape-line` +- `edit-handles-circle` + +### Focus/highlight + +- `entity-focus-fill` +- `entity-focus-line` +- `entity-focus-points` + +Các layer này không đi qua geotype registry. + +## 8. Visibility filtering + +Có ba lớp filter hiển thị trong runtime: + +1. background layer visibility +2. geometry visibility theo type key từ panel phải +3. binding filter / replay filter / timeline filter ở phía data trước khi set source + +Vì vậy khi một geometry "không hiện", có thể nguyên nhân nằm ở data filtering chứ không phải style layer. + +## 9. Thêm geotype mới - checklist đúng với code hiện tại + +Nếu thêm một geotype mới, nên đi theo checklist này: + +1. Thêm mapping vào `geoTypeMap` nếu backend dùng numeric/type code. +2. Thêm option vào `geometryTypeOptions.ts`. +3. Tạo file style mới trong `styles/geotypes/`. +4. Register nó trong `getAllGeotypeLayers(...)`. +5. Nếu cần label riêng, cập nhật layer builder tương ứng. +6. Nếu là point type, kiểm tra icon pipeline. +7. Nếu muốn user tạo geometry mới với type đó mặc định từ tool nào đó, cập nhật `useMapInteraction.ts`. + +## 10. Điều doc cũ mô tả chưa chính xác + +Doc cũ nói tới filter thời gian ở từng layer như một biểu thức layer-level chuẩn. +Implementation hiện tại không làm vậy. + +Thay vào đó: + +- timeline filter đang chạy phía data trong `page.tsx` +- binding filter và geometry visibility cũng chủ yếu chạy trước khi set source + +Tức là phần lớn filtering là `prepare data -> set source`, không phải `add layer filter expression per year`. diff --git a/src/uhm/doc/project_workflow.md b/src/uhm/doc/project_workflow.md new file mode 100644 index 0000000..e2b5d7e --- /dev/null +++ b/src/uhm/doc/project_workflow.md @@ -0,0 +1,175 @@ +# UHM Editor - project workflow hiện tại + +Tài liệu này mô tả đúng luồng project editor đang chạy ở frontend hiện tại. + +## 1. Mở project + +Editor vào từ route `/editor/[id]`. + +Luồng mở project: + +1. `fetchCurrentUser()` để chắc phiên đăng nhập còn hợp lệ. +2. `openSectionEditor(projectId)`: + - gọi API project detail + - gọi API commit list + - lấy `latest_commit_id` + - load `snapshot_json` của head commit nếu có +3. `normalizeEditorSnapshot()` để đưa snapshot về shape editor hiện tại. +4. `toEditorSessionSnapshot()` để chuyển snapshot thành session state: + - entities + - wikis + - entity-wiki + - feature collection đã rehydrate entity ids / names / metadata + +Nếu project chưa có commit, editor mở với `EMPTY_FEATURE_COLLECTION`. + +## 2. Rule khóa editor khi có pending submission + +Backend mới chặn chỉnh sửa nếu project có submission `PENDING`. + +Frontend xử lý như sau: + +- `openSectionEditor()` ném `ApiError(409)` kèm `pending_submission_id` +- page editor bắt lỗi đó +- hiển thị màn hình lock riêng +- cho phép xóa submission pending để mở khóa + +Trong trạng thái này: + +- không vào map editor +- không commit +- không submit mới + +## 3. Trạng thái project mà editor thực sự dùng + +`ProjectState` đang được FE dùng gồm: + +- `status` +- `head_commit_id` +- `locked_by` + +Editor page không tự dựng đầy đủ workflow `Approved/Rejected` ở UI. +Phần nó thật sự quan tâm là: + +- project có mở được không +- có `head_commit_id` để submit không +- có pending submission đang khóa project không + +## 4. Vòng đời một phiên chỉnh sửa + +### Bước 1: load baseline + +- `baselineSnapshot` lấy từ head commit hoặc commit được restore +- `initialData` lấy từ `baselineSnapshot.editor_feature_collection` +- `useEditorState()` reset draft và undo + +### Bước 2: chỉnh sửa cục bộ + +User có thể sửa: + +- geometry +- entity snapshot +- wiki snapshot +- entity-wiki snapshot + +Tất cả thay đổi lúc này mới chỉ ở memory của frontend. + +### Bước 3: commit + +`commitSection()` chỉ chạy khi: + +- đã mở được project +- `pendingSaveCount > 0` + +Luồng commit: + +1. build geometry diff từ `editor.buildPayload()` +2. build snapshot đầy đủ bằng `buildEditorSnapshot(...)` +3. kiểm tra kích thước payload trước khi gửi +4. gọi `createProjectCommit(projectId, { snapshot, edit_summary })` +5. nếu thành công: + - refresh `projectState` + - refresh `sectionCommits` + - cập nhật `baselineSnapshot` + - set `initialData = editor.draft` + - `editor.clearChanges()` + - clear `commitTitle` + +### Bước 4: submit + +`submitCurrentSection(content)` chỉ chạy khi: + +- project đang mở +- có `head_commit_id` +- `pendingSaveCount === 0` + +Frontend sẽ lấy latest commit từ project hiện tại rồi tạo submission mới. + +## 5. Restore commit + +Nút `Restore` trong `CommitHistoryPanel` hiện là restore phía frontend: + +- tải commit list mới nhất +- lấy snapshot của commit được chọn +- normalize snapshot +- nạp lại vào editor state + +Restore này: + +- không gọi endpoint đổi head commit +- không thay đổi head trên backend +- chủ yếu để user tiếp tục edit từ snapshot cũ + +Nói cách khác, đây là `load snapshot into editor`, không phải `server-side restore`. + +## 6. Snapshot commit được build như thế nào + +`buildEditorSnapshot()` nhận: + +- `draft` +- `changes` +- `snapshotEntities` +- `snapshotWikis` +- `snapshotEntityWikiLinks` +- `previousSnapshot` + +và sinh ra: + +- `editor_feature_collection` +- `entities` +- `geometries` +- `geometry_entity` +- `wikis` +- `entity_wiki` + +Các điểm quan trọng: + +- geometry many-to-many với entity được persist ở `geometry_entity[]` +- denormalized fields trên feature như `entity_ids`, `entity_name`, `binding`, `time_start` sẽ bị strip khỏi `editor_feature_collection` trước khi gửi API +- wiki/entity/link được chuẩn hóa lại thành `reference`, `binding`, `delete`, `create`, `update` tùy baseline + +## 7. Dirty state mà user nhìn thấy + +Số ở nút `Commit` là `pendingSaveCount`. + +Nó gồm: + +- số geometry change thật +- cộng thêm 1 nếu entity dirty +- cộng thêm 1 nếu wiki dirty +- cộng thêm 1 nếu entity-wiki dirty + +Vì vậy: + +- `Commit (3)` không có nghĩa là backend sẽ nhận đúng 3 record thay đổi +- nó là chỉ báo "có bao nhiêu nhóm thay đổi cần commit" + +## 8. Những gì workflow hiện chưa làm + +Editor hiện chưa có các behavior sau: + +- autosave local draft toàn project +- collaborative locking nhiều user ở FE +- review UI cho `Approved/Rejected` +- restore head commit trên backend từ trang editor +- branch/merge nhiều phiên edit song song diff --git a/src/uhm/doc/wiki_system.md b/src/uhm/doc/wiki_system.md new file mode 100644 index 0000000..5493e68 --- /dev/null +++ b/src/uhm/doc/wiki_system.md @@ -0,0 +1,185 @@ +# UHM Wiki system - trạng thái hiện tại + +Wiki trong UHM editor hiện chạy qua hai phần: + +- editor: `WikiSidebarPanel.tsx` +- viewer/sidebar public: `PublicWikiSidebar.tsx` + +## 1. Storage format của wiki doc + +Field `doc` trong `WikiSnapshot` hiện là `string | null`. +Frontend đang hỗ trợ ba dạng: + +- HTML string +- JSON string kiểu Tiptap cũ +- plain text fallback + +Quy ước hiện tại: + +- format ghi mới từ editor Quill là HTML +- Tiptap JSON chỉ còn để tương thích dữ liệu cũ + +`normalizeWikiDocForQuill()` và `normalizeWikiContentToHtml()` là hai hàm quan trọng cho compatibility này. + +## 2. Editor hiện dùng Quill, không dùng Tiptap + +Trong editor project, wiki đang dùng: + +- `react-quill-new` +- theme `snow` +- toolbar custom +- dynamic import để tránh SSR issues + +Toolbar hiện có: + +- heading `h1`, `h2`, `h3` +- align +- `bold`, `italic`, `underline`, `strike` +- ordered list, bullet list +- `blockquote` +- `code-block` +- `link` +- `image` +- `clean` + +Trang `app/user/wikieditor/page.tsx` vẫn dùng Tiptap, nhưng đó là trang riêng và không phải wiki editor chính của project UHM. + +## 3. Tạo, sửa và xóa wiki trong project editor + +`WikiSidebarPanel` hỗ trợ: + +- tạo wiki local từ panel +- sửa `title`, `slug`, `doc` +- xóa wiki khỏi `snapshotWikis` +- mở modal để sửa nội dung chi tiết + +Quy ước operation: + +- wiki mới local: `source: "inline"`, `operation: "create"` +- wiki ref thêm từ search: `source: "ref"`, `operation: "reference"` +- wiki đã tồn tại nhưng sửa nội dung: `operation: "update"` +- wiki bị remove khỏi current state: được chuyển thành `delete` khi build snapshot so với baseline + +## 4. Slug + +Slug trong editor hiện: + +- không tự generate bắt buộc ở lúc save +- có helper `slugifyWikiTitle()` để fill nhanh khi tạo mới +- được kiểm tra uniqueness bằng `checkWikiSlugExists(slug)` khi tạo wiki mới + +Với wiki mới: + +- nếu slug trống thì không cho create +- nếu slug đã tồn tại trên server thì chặn create/save + +## 5. Import và export + +### Import + +Editor chỉ hỗ trợ import file HTML: + +- chấp nhận `.html`, `.htm`, `text/html` +- nội dung phải parse được như HTML thô +- nếu file không phải HTML thì báo lỗi + +### Export + +Export hiện chỉ là download text từ `wikiDocHtml`. +Định dạng file được đoán từ nội dung hiện tại: + +- bắt đầu bằng `<` -> `html` +- bắt đầu bằng `{` hoặc `[` -> `json` +- còn lại -> `txt` + +Đây là export client-side, không có API export chuyên biệt. + +## 6. Link nội bộ giữa các wiki + +### Cách link đang được lưu + +Quill link hiện lưu trực tiếp `href = slug`. + +Ví dụ: + +- `dai-viet` +- `tran-dynasty` + +Quill sanitize mặc định đã được patch để chấp nhận slug/relative href, miễn không phải `javascript:`. + +### Modal chọn link + +Khi bấm nút link trên toolbar: + +- editor lấy selection hiện tại +- mở modal search +- tìm trong wiki local của project +- đồng thời có thể search wiki global từ server + +User có thể: + +- chọn một wiki local/global để chèn link +- chèn `__missing__` như một link placeholder +- remove link hiện tại + +### `__missing__` + +`__missing__` là sentinel để đánh dấu một link chưa map được tới wiki cụ thể. + +Trong editor: + +- link này được tô đỏ + +Trong viewer: + +- link này không click được +- vẫn được render như tín hiệu nội dung còn thiếu + +## 7. Render wiki phía public/sidebar + +`PublicWikiSidebar.tsx` xử lý wiki render như sau: + +1. normalize `content` về HTML +2. parse HTML bằng `DOMParser` +3. remove `