docs: add comprehensive project documentation and clean up snapshot-related files and components
This commit is contained in:
@@ -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”).
|
||||
@@ -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<string | null>(null);
|
||||
const autoOpenWiki = searchParams.get("only") === "wiki";
|
||||
const wikiOnly = autoOpenWiki;
|
||||
const [blockedPendingSubmissionId, setBlockedPendingSubmissionId] = useState<string | null>(null);
|
||||
const [searchKind, setSearchKind] = useState<UnifiedSearchKind>("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<string, boolean> = { ...geometryVisibility };
|
||||
|
||||
@@ -1360,7 +1355,7 @@ export default function Page() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!wikiOnly && !blockedPendingSubmissionId ? (
|
||||
{!blockedPendingSubmissionId ? (
|
||||
<div style={{ flex: 1, position: "relative", minHeight: "100vh" }}>
|
||||
{isBackgroundVisibilityReady ? (
|
||||
<Map
|
||||
@@ -1397,10 +1392,7 @@ export default function Page() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : blockedPendingSubmissionId ? null : (
|
||||
// Wiki-only mode: avoid mounting Map/Timeline (WebGL + geometry fetching) to reduce lag.
|
||||
<div style={{ flex: 1, minHeight: "100vh", background: "#0b1220" }} />
|
||||
)}
|
||||
) : 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 ? (
|
||||
<SelectedGeometryPanel
|
||||
selectedFeatures={selectedFeatures}
|
||||
entityTypeOptions={GEOMETRY_TYPE_OPTIONS}
|
||||
|
||||
@@ -311,9 +311,6 @@ export default function ProjectDetailsPage() {
|
||||
<Button size="sm" variant="outline" onClick={() => router.push(`/editor/${id}`)}>
|
||||
Mo editor
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => router.push(`/editor/${id}?only=wiki`)}>
|
||||
Editor only wiki
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -380,21 +380,6 @@ export default function ProjectsPage() {
|
||||
</span>
|
||||
</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>
|
||||
))}
|
||||
|
||||
@@ -48,7 +48,6 @@ type Props = {
|
||||
projectId: string;
|
||||
wikis: WikiSnapshot[];
|
||||
setWikis: React.Dispatch<React.SetStateAction<WikiSnapshot[]>>;
|
||||
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<string | null>(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<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).
|
||||
useEffect(() => {
|
||||
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` 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 ----
|
||||
|
||||
@@ -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 đó
|
||||
@@ -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
|
||||
@@ -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<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`
|
||||
|
||||
- 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
|
||||
@@ -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`
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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 `<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.
|
||||
Reference in New Issue
Block a user