docs: add comprehensive project documentation and clean up snapshot-related files and components

This commit is contained in:
taDuc
2026-05-14 23:50:49 +07:00
parent b220798978
commit 57e3d6b3e5
13 changed files with 1527 additions and 288 deletions
-248
View File
@@ -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**`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"``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"``"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”).
+4 -13
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction, type PointerEvent as ReactPointerEvent } from "react"; 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 Map from "@/uhm/components/Map";
import Editor from "@/uhm/components/Editor"; import Editor from "@/uhm/components/Editor";
import BackgroundLayersPanel from "@/uhm/components/editor/BackgroundLayersPanel"; import BackgroundLayersPanel from "@/uhm/components/editor/BackgroundLayersPanel";
@@ -70,11 +70,8 @@ const DEFAULT_EDITOR_USER_ID = "local-editor";
export default function Page() { export default function Page() {
const params = useParams(); const params = useParams();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
const projectId = String(params.id || ""); const projectId = String(params.id || "");
const openedProjectIdRef = useRef<string | null>(null); const openedProjectIdRef = useRef<string | null>(null);
const autoOpenWiki = searchParams.get("only") === "wiki";
const wikiOnly = autoOpenWiki;
const [blockedPendingSubmissionId, setBlockedPendingSubmissionId] = useState<string | null>(null); const [blockedPendingSubmissionId, setBlockedPendingSubmissionId] = useState<string | null>(null);
const [searchKind, setSearchKind] = useState<UnifiedSearchKind>("entity"); const [searchKind, setSearchKind] = useState<UnifiedSearchKind>("entity");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@@ -423,8 +420,6 @@ export default function Page() {
internalSetMode(m); internalSetMode(m);
}, [internalSetMode]); }, [internalSetMode]);
const onSetMode = setMode;
const effectiveGeometryVisibility = useMemo(() => { const effectiveGeometryVisibility = useMemo(() => {
const visibility: Record<string, boolean> = { ...geometryVisibility }; const visibility: Record<string, boolean> = { ...geometryVisibility };
@@ -1360,7 +1355,7 @@ export default function Page() {
</div> </div>
) : null} ) : null}
{!wikiOnly && !blockedPendingSubmissionId ? ( {!blockedPendingSubmissionId ? (
<div style={{ flex: 1, position: "relative", minHeight: "100vh" }}> <div style={{ flex: 1, position: "relative", minHeight: "100vh" }}>
{isBackgroundVisibilityReady ? ( {isBackgroundVisibilityReady ? (
<Map <Map
@@ -1397,10 +1392,7 @@ export default function Page() {
/> />
)} )}
</div> </div>
) : blockedPendingSubmissionId ? null : ( ) : null}
// Wiki-only mode: avoid mounting Map/Timeline (WebGL + geometry fetching) to reduce lag.
<div style={{ flex: 1, minHeight: "100vh", background: "#0b1220" }} />
)}
{mode !== "replay" ? ( {mode !== "replay" ? (
<> <>
@@ -1676,7 +1668,6 @@ export default function Page() {
projectId={projectId} projectId={projectId}
wikis={snapshotWikis} wikis={snapshotWikis}
setWikis={setSnapshotWikisUndoable} setWikis={setSnapshotWikisUndoable}
autoOpen={autoOpenWiki}
requestedActiveId={requestedActiveWikiId} requestedActiveId={requestedActiveWikiId}
/> />
@@ -1686,7 +1677,7 @@ export default function Page() {
links={snapshotEntityWikiLinks} links={snapshotEntityWikiLinks}
setLinks={setSnapshotEntityWikiLinksUndoable} setLinks={setSnapshotEntityWikiLinksUndoable}
/> />
{!wikiOnly && selectedFeature ? ( {selectedFeature ? (
<SelectedGeometryPanel <SelectedGeometryPanel
selectedFeatures={selectedFeatures} selectedFeatures={selectedFeatures}
entityTypeOptions={GEOMETRY_TYPE_OPTIONS} entityTypeOptions={GEOMETRY_TYPE_OPTIONS}
-3
View File
@@ -311,9 +311,6 @@ export default function ProjectDetailsPage() {
<Button size="sm" variant="outline" onClick={() => router.push(`/editor/${id}`)}> <Button size="sm" variant="outline" onClick={() => router.push(`/editor/${id}`)}>
Mo editor Mo editor
</Button> </Button>
<Button size="sm" variant="outline" onClick={() => router.push(`/editor/${id}?only=wiki`)}>
Editor only wiki
</Button>
</div> </div>
</div> </div>
</div> </div>
-15
View File
@@ -380,21 +380,6 @@ export default function ProjectsPage() {
</span> </span>
</div> </div>
<div className="relative group/btn3 inline-flex">
<Button
size="sm"
variant="outline"
className="!p-0 w-9 h-9 flex items-center justify-center"
onClick={() => router.push(`/editor/${project.id}?only=wiki`)}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
</Button>
<span className="absolute -top-8 left-1/2 -translate-x-1/2 scale-0 rounded bg-gray-900 px-2 py-1 text-[11px] font-medium text-white opacity-0 transition-all group-hover/btn3:scale-100 group-hover/btn3:opacity-100 z-50 pointer-events-none whitespace-nowrap shadow-sm dark:bg-gray-700">
Wiki Editor
</span>
</div>
</div> </div>
</div> </div>
))} ))}
+1 -8
View File
@@ -48,7 +48,6 @@ type Props = {
projectId: string; projectId: string;
wikis: WikiSnapshot[]; wikis: WikiSnapshot[];
setWikis: React.Dispatch<React.SetStateAction<WikiSnapshot[]>>; setWikis: React.Dispatch<React.SetStateAction<WikiSnapshot[]>>;
autoOpen?: boolean;
requestedActiveId?: string | null; requestedActiveId?: string | null;
}; };
@@ -57,7 +56,7 @@ function clampTitle(title: string) {
return t.length ? t.slice(0, 120) : "Untitled wiki"; 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 [open, setOpen] = useState(false);
const [activeId, setActiveId] = useState<string | null>(null); const [activeId, setActiveId] = useState<string | null>(null);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
@@ -93,12 +92,6 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, autoOpen,
const globalWikiSearchRequestRef = useRef(0); const globalWikiSearchRequestRef = useRef(0);
const importFileInputRef = useRef<HTMLInputElement | null>(null); const importFileInputRef = useRef<HTMLInputElement | null>(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). // Allow Quill to keep wiki links where href is a slug (no scheme).
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@@ -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` nhóm `UIFunctionName` / `MapFunctionName` tả schema dự phòng hoặc tương lai.
* Editor route `/editor/[id]` hiện 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`
* 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 ---- // ---- Root request ----
+191
View File
@@ -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[]``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)``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 đó
+297
View File
@@ -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``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"``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"``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"``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"``geometry_preset: "circle-area"`.
## 5. Chọn và sửa geometry
### Selection
- `Map` trả về danh sách `selectedFeatureIds`.
- `SelectedGeometryPanel`, `ProjectEntityRefsPanel``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``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
+280
View File
@@ -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()``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()`
-`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<featureId, Feature>` 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`
-`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
-`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
+200
View File
@@ -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``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`
+182
View File
@@ -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`.
+175
View File
@@ -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
-`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ở
-`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``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
+185
View File
@@ -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()``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 `<script>`
4. rewrite link nội bộ từ slug thành `#wiki:{slug}`
5. giữ external links mở tab mới
6. sinh TOC từ `h1..h6`
Viewer này còn hỗ trợ:
- auto tạo heading id
- TOC dạng chip ngang
- intercept click vào `a[data-wiki-slug]` để điều hướng wiki nội bộ bằng logic app
## 8. Wiki và entity-wiki binding
Link giữa entity và wiki không nằm trong field của chính wiki.
Nó sống ở collection riêng:
- `snapshotEntityWikiLinks`
- payload commit: `entity_wiki[]`
`EntityWikiBindingsPanel` cho phép:
- chọn entity
- chọn wiki
- link/unlink cặp đó
Khi build snapshot:
- cặp mới -> `binding`
- cặp đã có từ baseline -> `reference`
- cặp bị gỡ -> `delete`
## 9. Những gì wiki system hiện chưa có
Hiện tại chưa có:
- media upload workflow riêng lên server cho Quill image
- version history riêng cho từng wiki ngoài commit history của project
- markdown storage/render
- schema block editor mới cho project wiki
- cross-project link graph UI
File `doc/commit_snapshot.ts` có chứa schema `replays[]`, nhưng phần replay narrative đó chưa được nối với wiki editor hiện tại.