add editor

This commit is contained in:
taDuc
2026-05-02 02:48:17 +07:00
parent 41af501b51
commit a74047fd09
62 changed files with 9049 additions and 9 deletions
@@ -0,0 +1,44 @@
import type { EntityGeometryPreset } from "@/uhm/lib/entityTypeOptions";
export type EditorMode =
| "idle"
| "draw"
| "select"
| "add-point"
| "add-line"
| "add-path"
| "add-circle";
export type TimelineRange = {
min: number;
max: number;
};
export type EntityFormState = {
name: string;
slug: string;
type_id: string;
};
export type GeometryMetaFormState = {
time_start: string;
time_end: string;
binding: string;
};
export type PendingEntityCreate = {
id: string;
name: string;
slug: string | null;
type_id: string;
status: number;
};
export type CreatedEntitySummary = {
id: string;
name: string;
type_id?: string | null;
};
export type GeometryPreset = EntityGeometryPreset;
@@ -0,0 +1,21 @@
import { useState } from "react";
import {
BackgroundLayerVisibility,
HIDDEN_BACKGROUND_LAYER_VISIBILITY,
} from "@/uhm/lib/backgroundLayers";
export function useBackgroundSessionState() {
// Trạng thái bật/tắt layer nền (khởi tạo default hidden; sẽ load từ storage ở page).
const [backgroundVisibility, setBackgroundVisibility] = useState<BackgroundLayerVisibility>(
() => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY })
);
// Đảm bảo đã load visibility trước khi render map thật.
const [isBackgroundVisibilityReady, setIsBackgroundVisibilityReady] = useState(false);
return {
backgroundVisibility,
setBackgroundVisibility,
isBackgroundVisibilityReady,
setIsBackgroundVisibilityReady,
};
}
@@ -0,0 +1,80 @@
import { useState } from "react";
import type { Entity } from "@/uhm/types/entities";
import type { FeatureId } from "@/uhm/types/geo";
import { DEFAULT_ENTITY_TYPE_ID } from "@/uhm/lib/entityTypeOptions";
import type {
CreatedEntitySummary,
EntityFormState,
GeometryMetaFormState,
PendingEntityCreate,
} from "@/uhm/lib/editor/session/sessionTypes";
export function useEntitySessionState() {
// Entities đã persisted từ backend (dùng cho search/binding).
const [persistedEntities, setPersistedEntities] = useState<Entity[]>([]);
// Entities tạo mới trong phiên nhưng chưa commit lên backend.
const [pendingEntityCreates, setPendingEntityCreates] = useState<PendingEntityCreate[]>([]);
// Tóm tắt entities đã tạo (để hiển thị nhanh ở sidebar).
const [createdEntities, setCreatedEntities] = useState<CreatedEntitySummary[]>([]);
// Thông báo trạng thái/lỗi liên quan entity/session.
const [entityStatus, setEntityStatus] = useState<string | null>(null);
// Feature đang được chọn để thao tác bind entities/metadata.
const [selectedFeatureId, setSelectedFeatureId] = useState<FeatureId | null>(null);
// Form tạo entity mới (độc lập).
const [entityForm, setEntityForm] = useState<EntityFormState>({
name: "",
slug: "",
type_id: DEFAULT_ENTITY_TYPE_ID,
});
// Danh sách entity IDs đang chọn để bind vào geometry hiện tại.
const [selectedGeometryEntityIds, setSelectedGeometryEntityIds] = useState<string[]>([]);
// Form metadata geometry (time range + binding ids).
const [geometryMetaForm, setGeometryMetaForm] = useState<GeometryMetaFormState>({
time_start: "",
time_end: "",
binding: "",
});
// Cờ loading khi apply entity/metadata (local submit).
const [isEntitySubmitting, setIsEntitySubmitting] = useState(false);
// Thông báo trạng thái/lỗi cho form entity/metadata.
const [entityFormStatus, setEntityFormStatus] = useState<string | null>(null);
// Keyword search entity theo name.
const [entitySearchQuery, setEntitySearchQuery] = useState("");
// Kết quả search entity để user chọn.
const [entitySearchResults, setEntitySearchResults] = useState<Entity[]>([]);
// Entity ID đang được chọn trong dropdown kết quả search.
const [selectedSearchEntityId, setSelectedSearchEntityId] = useState<string | null>(null);
// Cờ loading khi search entity.
const [isEntitySearchLoading, setIsEntitySearchLoading] = useState(false);
return {
persistedEntities,
setPersistedEntities,
pendingEntityCreates,
setPendingEntityCreates,
createdEntities,
setCreatedEntities,
entityStatus,
setEntityStatus,
selectedFeatureId,
setSelectedFeatureId,
entityForm,
setEntityForm,
selectedGeometryEntityIds,
setSelectedGeometryEntityIds,
geometryMetaForm,
setGeometryMetaForm,
isEntitySubmitting,
setIsEntitySubmitting,
entityFormStatus,
setEntityFormStatus,
entitySearchQuery,
setEntitySearchQuery,
entitySearchResults,
setEntitySearchResults,
selectedSearchEntityId,
setSelectedSearchEntityId,
isEntitySearchLoading,
setIsEntitySearchLoading,
};
}
@@ -0,0 +1,85 @@
import { useCallback, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import type { EditorSnapshot, Section, SectionCommit, SectionState } from "@/uhm/types/sections";
type Options = {
defaultEditorUserId: string;
};
type SectionTask = "idle" | "saving" | "submitting" | "opening-section";
export function useSectionSessionState(options: Options) {
// Single state machine cho các tác vụ async của section (saving/submitting/opening).
const [sectionTask, setSectionTask] = useState<SectionTask>("idle");
const setTaskFlag = useCallback((task: Exclude<SectionTask, "idle">, next: SetStateAction<boolean>) => {
setSectionTask((prev) => {
const currentValue = prev === task;
const nextValue = typeof next === "function" ? next(currentValue) : next;
if (nextValue) return task;
return prev === task ? "idle" : prev;
});
}, []);
const isSaving = sectionTask === "saving";
const isSubmitting = sectionTask === "submitting";
const isOpeningSection = sectionTask === "opening-section";
const setIsSaving: Dispatch<SetStateAction<boolean>> = useCallback((next) => {
setTaskFlag("saving", next);
}, [setTaskFlag]);
const setIsSubmitting: Dispatch<SetStateAction<boolean>> = useCallback((next) => {
setTaskFlag("submitting", next);
}, [setTaskFlag]);
const setIsOpeningSection: Dispatch<SetStateAction<boolean>> = useCallback((next) => {
setTaskFlag("opening-section", next);
}, [setTaskFlag]);
// Danh sách sections để user chọn mở.
const [availableSections, setAvailableSections] = useState<Section[]>([]);
// Section ID đang được chọn trong dropdown.
const [selectedSectionId, setSelectedSectionId] = useState("");
// Title section mới (để create).
const [newSectionTitle, setNewSectionTitle] = useState("");
// Input title cho commit.
const [commitTitle, setCommitTitle] = useState("");
// Input note cho commit.
const [commitNote, setCommitNote] = useState("");
// User ID dùng để gắn vào commit/submit/lock.
const [editorUserIdInput, setEditorUserIdInput] = useState(options.defaultEditorUserId);
// Section đang mở để edit (null nếu chưa mở).
const [activeSection, setActiveSection] = useState<Section | null>(null);
// Trạng thái section (version/head/status/lock).
const [sectionState, setSectionState] = useState<SectionState | null>(null);
// Danh sách commits của section đang mở.
const [sectionCommits, setSectionCommits] = useState<SectionCommit[]>([]);
// Snapshot gần nhất đã load (để build snapshot diff/metadata).
const [lastSectionSnapshot, setLastSectionSnapshot] = useState<EditorSnapshot | null>(null);
return {
isSaving,
setIsSaving,
isSubmitting,
setIsSubmitting,
isOpeningSection,
setIsOpeningSection,
availableSections,
setAvailableSections,
selectedSectionId,
setSelectedSectionId,
newSectionTitle,
setNewSectionTitle,
commitTitle,
setCommitTitle,
commitNote,
setCommitNote,
editorUserIdInput,
setEditorUserIdInput,
activeSection,
setActiveSection,
sectionState,
setSectionState,
sectionCommits,
setSectionCommits,
lastSectionSnapshot,
setLastSectionSnapshot,
};
}
@@ -0,0 +1,42 @@
import { useState } from "react";
import type { TimelineRange } from "@/uhm/lib/editor/session/sessionTypes";
import { clampYearValue } from "@/uhm/lib/timeline";
type Options = {
currentYear: number;
fallbackTimelineRange: TimelineRange;
};
export function useTimelineState(options: Options) {
// Năm timeline "đã chốt" để fetch dữ liệu.
const [timelineYear, setTimelineYear] = useState<number>(() =>
clampYearValue(
options.currentYear,
options.fallbackTimelineRange.min,
options.fallbackTimelineRange.max
)
);
// Năm timeline đang chỉnh (debounce rồi đẩy sang timelineYear).
const [timelineDraftYear, setTimelineDraftYear] = useState<number>(() =>
clampYearValue(
options.currentYear,
options.fallbackTimelineRange.min,
options.fallbackTimelineRange.max
)
);
// Cờ loading khi fetch theo timeline.
const [isTimelineLoading, setIsTimelineLoading] = useState(false);
// Thông báo trạng thái/lỗi khi fetch theo timeline.
const [timelineStatus, setTimelineStatus] = useState<string | null>(null);
return {
timelineYear,
setTimelineYear,
timelineDraftYear,
setTimelineDraftYear,
isTimelineLoading,
setIsTimelineLoading,
timelineStatus,
setTimelineStatus,
};
}