refactor: migrate project data models and transition editor state management to the new project-based API architecture.
This commit is contained in:
@@ -14,7 +14,7 @@ import GeometryBindingPanel from "@/uhm/components/editor/GeometryBindingPanel";
|
|||||||
import { Entity, fetchEntities, searchEntitiesByName } from "@/uhm/api/entities";
|
import { Entity, fetchEntities, searchEntitiesByName } from "@/uhm/api/entities";
|
||||||
import { ApiError } from "@/uhm/api/http";
|
import { ApiError } from "@/uhm/api/http";
|
||||||
import { fetchCurrentUser } from "@/uhm/api/auth";
|
import { fetchCurrentUser } from "@/uhm/api/auth";
|
||||||
import { SectionCommit } from "@/uhm/api/sections";
|
import { ProjectCommit } from "@/uhm/api/projects";
|
||||||
import { searchWikisByTitle, type Wiki } from "@/uhm/api/wikis";
|
import { searchWikisByTitle, type Wiki } from "@/uhm/api/wikis";
|
||||||
import { searchGeometriesByEntityName, type EntityGeometriesSearchItem, type EntityGeometrySearchGeo } from "@/uhm/api/geometries";
|
import { searchGeometriesByEntityName, type EntityGeometriesSearchItem, type EntityGeometrySearchGeo } from "@/uhm/api/geometries";
|
||||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||||
@@ -57,13 +57,13 @@ import {
|
|||||||
loadBackgroundLayerVisibilityFromStorage,
|
loadBackgroundLayerVisibilityFromStorage,
|
||||||
persistBackgroundLayerVisibility,
|
persistBackgroundLayerVisibility,
|
||||||
} from "@/uhm/lib/editor/background/backgroundVisibilityStorage";
|
} from "@/uhm/lib/editor/background/backgroundVisibilityStorage";
|
||||||
import { useSectionCommands } from "@/uhm/lib/editor/section/useSectionCommands";
|
import { useProjectCommands } from "@/uhm/lib/editor/project/useProjectCommands";
|
||||||
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
|
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
|
||||||
import { FIXED_TIMELINE_RANGE, clampYearToFixedRange } from "@/uhm/lib/utils/timeline";
|
import { FIXED_TIMELINE_RANGE, clampYearToFixedRange } from "@/uhm/lib/utils/timeline";
|
||||||
import { useFeatureCommands } from "./featureCommands";
|
import { useFeatureCommands } from "./featureCommands";
|
||||||
import { deleteSubmission } from "@/uhm/api/sections";
|
import { deleteSubmission } from "@/uhm/api/projects";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/sections";
|
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
import UnifiedSearchBar, { type UnifiedSearchKind } from "@/uhm/components/ui/UnifiedSearchBar";
|
import UnifiedSearchBar, { type UnifiedSearchKind } from "@/uhm/components/ui/UnifiedSearchBar";
|
||||||
|
|
||||||
const CURRENT_YEAR = new Date().getUTCFullYear();
|
const CURRENT_YEAR = new Date().getUTCFullYear();
|
||||||
@@ -107,8 +107,8 @@ export default function Page() {
|
|||||||
isOpeningSection,
|
isOpeningSection,
|
||||||
setIsOpeningSection,
|
setIsOpeningSection,
|
||||||
setAvailableSections,
|
setAvailableSections,
|
||||||
selectedSectionId,
|
selectedProjectId,
|
||||||
setSelectedSectionId,
|
setSelectedProjectId,
|
||||||
newSectionTitle,
|
newSectionTitle,
|
||||||
setNewSectionTitle,
|
setNewSectionTitle,
|
||||||
commitTitle,
|
commitTitle,
|
||||||
@@ -116,10 +116,10 @@ export default function Page() {
|
|||||||
editorUserIdInput,
|
editorUserIdInput,
|
||||||
activeSection,
|
activeSection,
|
||||||
setActiveSection,
|
setActiveSection,
|
||||||
sectionState,
|
projectState,
|
||||||
setSectionState,
|
setProjectState,
|
||||||
sectionCommits,
|
sectionCommits,
|
||||||
setSectionCommits,
|
setProjectCommits,
|
||||||
baselineSnapshot,
|
baselineSnapshot,
|
||||||
setBaselineSnapshot,
|
setBaselineSnapshot,
|
||||||
entityCatalog,
|
entityCatalog,
|
||||||
@@ -367,13 +367,13 @@ export default function Page() {
|
|||||||
+ (entitiesDirty ? 1 : 0)
|
+ (entitiesDirty ? 1 : 0)
|
||||||
+ (entityWikiDirty ? 1 : 0);
|
+ (entityWikiDirty ? 1 : 0);
|
||||||
|
|
||||||
const sectionCommands = useSectionCommands({
|
const sectionCommands = useProjectCommands({
|
||||||
editor,
|
editor,
|
||||||
editorUserId,
|
editorUserId,
|
||||||
emptyFeatureCollection: EMPTY_FEATURE_COLLECTION,
|
emptyFeatureCollection: EMPTY_FEATURE_COLLECTION,
|
||||||
activeSection,
|
activeSection,
|
||||||
sectionState,
|
projectState,
|
||||||
selectedSectionId,
|
selectedProjectId,
|
||||||
newSectionTitle,
|
newSectionTitle,
|
||||||
pendingSaveCount,
|
pendingSaveCount,
|
||||||
snapshotEntities,
|
snapshotEntities,
|
||||||
@@ -382,11 +382,11 @@ export default function Page() {
|
|||||||
baselineSnapshot,
|
baselineSnapshot,
|
||||||
commitTitle,
|
commitTitle,
|
||||||
setActiveSection,
|
setActiveSection,
|
||||||
setSelectedSectionId,
|
setSelectedProjectId,
|
||||||
setSectionState,
|
setProjectState,
|
||||||
setBaselineSnapshot,
|
setBaselineSnapshot,
|
||||||
setInitialData,
|
setInitialData,
|
||||||
setSectionCommits,
|
setProjectCommits,
|
||||||
setSnapshotEntities,
|
setSnapshotEntities,
|
||||||
setSnapshotWikis,
|
setSnapshotWikis,
|
||||||
setSnapshotEntityWikiLinks,
|
setSnapshotEntityWikiLinks,
|
||||||
@@ -1144,8 +1144,8 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const headCommit = sectionState?.head_commit_id
|
const headCommit = projectState?.head_commit_id
|
||||||
? sectionCommits.find((commit) => commit.id === sectionState.head_commit_id) || null
|
? sectionCommits.find((commit) => commit.id === projectState.head_commit_id) || null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const handleCreateFeature = (feature: Feature) => {
|
const handleCreateFeature = (feature: Feature) => {
|
||||||
@@ -1166,12 +1166,12 @@ export default function Page() {
|
|||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
sectionTitle={activeSection?.title || "Đang tải project"}
|
sectionTitle={activeSection?.title || "Đang tải project"}
|
||||||
sectionStatus={sectionState?.status || "editing"}
|
projectStatus={projectState?.status || "editing"}
|
||||||
commitTitle={commitTitle}
|
commitTitle={commitTitle}
|
||||||
onCommitTitleChange={setCommitTitle}
|
onCommitTitleChange={setCommitTitle}
|
||||||
commitCount={sectionCommits.length}
|
commitCount={sectionCommits.length}
|
||||||
hasHeadCommit={Boolean(sectionState?.head_commit_id)}
|
hasHeadCommit={Boolean(projectState?.head_commit_id)}
|
||||||
headCommitId={sectionState?.head_commit_id || null}
|
headCommitId={projectState?.head_commit_id || null}
|
||||||
latestCommitLabel={headCommit ? `Head: ${formatCommitTitle(headCommit)}` : null}
|
latestCommitLabel={headCommit ? `Head: ${formatCommitTitle(headCommit)}` : null}
|
||||||
commits={sectionCommits}
|
commits={sectionCommits}
|
||||||
changesCount={pendingSaveCount}
|
changesCount={pendingSaveCount}
|
||||||
@@ -1633,7 +1633,7 @@ function normalizeEditorUserId(value: string): string {
|
|||||||
return normalized || DEFAULT_EDITOR_USER_ID;
|
return normalized || DEFAULT_EDITOR_USER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCommitTitle(commit: SectionCommit): string {
|
function formatCommitTitle(commit: ProjectCommit): string {
|
||||||
return commit.edit_summary?.trim() || `Commit ${commit.id.slice(0, 8)}`;
|
return commit.edit_summary?.trim() || `Commit ${commit.id.slice(0, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Badge from "@/components/ui/badge/Badge";
|
|||||||
import { CreateProjectPayload, Project } from "@/interface/project";
|
import { CreateProjectPayload, Project } from "@/interface/project";
|
||||||
import { apiCreateProject, apiCreateProjectCommit, apiGetProjectCommits, getCurrentProject } from "@/service/projectService";
|
import { apiCreateProject, apiCreateProjectCommit, apiGetProjectCommits, getCurrentProject } from "@/service/projectService";
|
||||||
import { normalizeEditorSnapshot } from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
import { normalizeEditorSnapshot } from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
||||||
import type { EditorSnapshot } from "@/uhm/types/sections";
|
import type { EditorSnapshot } from "@/uhm/types/projects";
|
||||||
|
|
||||||
export type ProjectSortColumn = "created_at" | "updated_at" | "title";
|
export type ProjectSortColumn = "created_at" | "updated_at" | "title";
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export default function WikiEditorPage() {
|
|||||||
type: "doc",
|
type: "doc",
|
||||||
content: [
|
content: [
|
||||||
{ type: "paragraph", content: [{ type: "text", text: "Write your wiki content here." }] },
|
{ type: "paragraph", content: [{ type: "text", text: "Write your wiki content here." }] },
|
||||||
{ type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Section" }] },
|
{ type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Project" }] },
|
||||||
{ type: "paragraph", content: [{ type: "text", text: "Use H1/H2/H3 and the TOC will follow." }] },
|
{ type: "paragraph", content: [{ type: "text", text: "Use H1/H2/H3 and the TOC will follow." }] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,43 +2,43 @@ import { API_BASE_URL, API_ENDPOINTS } from "@/uhm/api/config";
|
|||||||
import { ApiError, jsonRequestInit, requestJson } from "@/uhm/api/http";
|
import { ApiError, jsonRequestInit, requestJson } from "@/uhm/api/http";
|
||||||
import type {
|
import type {
|
||||||
CreateCommitInput,
|
CreateCommitInput,
|
||||||
CreateSectionInput,
|
CreateProjectInput,
|
||||||
EditorLoadResponse,
|
EditorLoadResponse,
|
||||||
RestoreCommitInput,
|
RestoreCommitInput,
|
||||||
Section,
|
Project,
|
||||||
SectionCommit,
|
ProjectCommit,
|
||||||
SectionState,
|
ProjectState,
|
||||||
SectionSubmission,
|
ProjectSubmission,
|
||||||
} from "@/uhm/types/sections";
|
} from "@/uhm/types/projects";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CreateCommitInput,
|
CreateCommitInput,
|
||||||
CreateSectionInput,
|
CreateProjectInput,
|
||||||
EditorLoadResponse,
|
EditorLoadResponse,
|
||||||
RestoreCommitInput,
|
RestoreCommitInput,
|
||||||
Section,
|
Project,
|
||||||
SectionCommit,
|
ProjectCommit,
|
||||||
SectionState,
|
ProjectState,
|
||||||
SectionSubmission,
|
ProjectSubmission,
|
||||||
} from "@/uhm/types/sections";
|
} from "@/uhm/types/projects";
|
||||||
|
|
||||||
// Sections (API cũ) => Projects (API mới)
|
// Projects (API cũ) => Projects (API mới)
|
||||||
|
|
||||||
export async function fetchSections(): Promise<Section[]> {
|
export async function fetchProjects(): Promise<Project[]> {
|
||||||
// /users/current/project requires JWT.
|
// /users/current/project requires JWT.
|
||||||
return requestJson<Section[]>(API_ENDPOINTS.currentUserProjects);
|
return requestJson<Project[]>(API_ENDPOINTS.currentUserProjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSection(input: CreateSectionInput): Promise<Section> {
|
export async function createProject(input: CreateProjectInput): Promise<Project> {
|
||||||
// POST /projects
|
// POST /projects
|
||||||
return requestJson<Section>(API_ENDPOINTS.projects, jsonRequestInit("POST", input));
|
return requestJson<Project>(API_ENDPOINTS.projects, jsonRequestInit("POST", input));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openSectionEditor(sectionId: string): Promise<EditorLoadResponse> {
|
export async function openSectionEditor(projectId: string): Promise<EditorLoadResponse> {
|
||||||
// API mới không có endpoint "editor". FE tự load:
|
// API mới không có endpoint "editor". FE tự load:
|
||||||
// 1) Project details
|
// 1) Project details
|
||||||
// 2) Project commits (to get snapshot_json of latest commit)
|
// 2) Project commits (to get snapshot_json of latest commit)
|
||||||
const project = await requestJson<Section>(`${API_ENDPOINTS.projects}/${encodeURIComponent(sectionId)}`);
|
const project = await requestJson<Project>(`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}`);
|
||||||
|
|
||||||
const pending = (project.submissions || []).find((s) => s?.status === "PENDING") || null;
|
const pending = (project.submissions || []).find((s) => s?.status === "PENDING") || null;
|
||||||
if (pending) {
|
if (pending) {
|
||||||
@@ -51,33 +51,33 @@ export async function openSectionEditor(sectionId: string): Promise<EditorLoadRe
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const commits = await fetchSectionCommits(sectionId);
|
const commits = await fetchProjectCommits(projectId);
|
||||||
|
|
||||||
const headCommitId = project.latest_commit_id ?? null;
|
const headCommitId = project.latest_commit_id ?? null;
|
||||||
const headCommit = headCommitId ? commits.find((c) => c.id === headCommitId) || null : null;
|
const headCommit = headCommitId ? commits.find((c) => c.id === headCommitId) || null : null;
|
||||||
const snapshot = headCommit?.snapshot_json ?? null;
|
const snapshot = headCommit?.snapshot_json ?? null;
|
||||||
|
|
||||||
const state: SectionState = {
|
const state: ProjectState = {
|
||||||
status: project.project_status || "ACTIVE",
|
status: project.project_status || "ACTIVE",
|
||||||
head_commit_id: headCommitId,
|
head_commit_id: headCommitId,
|
||||||
locked_by: project.locked_by ?? null,
|
locked_by: project.locked_by ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
section: project,
|
project: project,
|
||||||
state,
|
state,
|
||||||
commit: headCommit,
|
commit: headCommit,
|
||||||
snapshot,
|
snapshot,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSectionCommit(
|
export async function createProjectCommit(
|
||||||
sectionId: string,
|
projectId: string,
|
||||||
input: CreateCommitInput
|
input: CreateCommitInput
|
||||||
): Promise<{ commit: SectionCommit; state: SectionState }> {
|
): Promise<{ commit: ProjectCommit; state: ProjectState }> {
|
||||||
// POST /projects/{id}/commits
|
// POST /projects/{id}/commits
|
||||||
const commit = await requestJson<SectionCommit>(
|
const commit = await requestJson<ProjectCommit>(
|
||||||
`${API_ENDPOINTS.projects}/${encodeURIComponent(sectionId)}/commits`,
|
`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}/commits`,
|
||||||
jsonRequestInit("POST", {
|
jsonRequestInit("POST", {
|
||||||
snapshot_json: input.snapshot,
|
snapshot_json: input.snapshot,
|
||||||
edit_summary: input.edit_summary,
|
edit_summary: input.edit_summary,
|
||||||
@@ -85,8 +85,8 @@ export async function createSectionCommit(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Refresh project state (latest_commit_id may have moved).
|
// Refresh project state (latest_commit_id may have moved).
|
||||||
const project = await requestJson<Section>(`${API_ENDPOINTS.projects}/${encodeURIComponent(sectionId)}`);
|
const project = await requestJson<Project>(`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}`);
|
||||||
const state: SectionState = {
|
const state: ProjectState = {
|
||||||
status: project.project_status || "ACTIVE",
|
status: project.project_status || "ACTIVE",
|
||||||
head_commit_id: project.latest_commit_id ?? null,
|
head_commit_id: project.latest_commit_id ?? null,
|
||||||
locked_by: project.locked_by ?? null,
|
locked_by: project.locked_by ?? null,
|
||||||
@@ -95,27 +95,27 @@ export async function createSectionCommit(
|
|||||||
return { commit, state };
|
return { commit, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSectionCommits(sectionId: string): Promise<SectionCommit[]> {
|
export async function fetchProjectCommits(projectId: string): Promise<ProjectCommit[]> {
|
||||||
return requestJson<SectionCommit[]>(`${API_ENDPOINTS.projects}/${encodeURIComponent(sectionId)}/commits`);
|
return requestJson<ProjectCommit[]>(`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}/commits`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restoreSectionCommit(
|
export async function restoreProjectCommit(
|
||||||
sectionId: string,
|
projectId: string,
|
||||||
input: RestoreCommitInput
|
input: RestoreCommitInput
|
||||||
): Promise<{ commit: SectionCommit | null; state: SectionState }> {
|
): Promise<{ commit: ProjectCommit | null; state: ProjectState }> {
|
||||||
// POST /projects/{id}/commits/restore
|
// POST /projects/{id}/commits/restore
|
||||||
await requestJson(
|
await requestJson(
|
||||||
`${API_ENDPOINTS.projects}/${encodeURIComponent(sectionId)}/commits/restore`,
|
`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}/commits/restore`,
|
||||||
jsonRequestInit("POST", { commit_id: input.commit_id })
|
jsonRequestInit("POST", { commit_id: input.commit_id })
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reload commits + project to determine new head commit.
|
// Reload commits + project to determine new head commit.
|
||||||
const project = await requestJson<Section>(`${API_ENDPOINTS.projects}/${encodeURIComponent(sectionId)}`);
|
const project = await requestJson<Project>(`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}`);
|
||||||
const commits = await fetchSectionCommits(sectionId);
|
const commits = await fetchProjectCommits(projectId);
|
||||||
const headCommitId = project.latest_commit_id ?? null;
|
const headCommitId = project.latest_commit_id ?? null;
|
||||||
const headCommit = headCommitId ? commits.find((c) => c.id === headCommitId) || null : null;
|
const headCommit = headCommitId ? commits.find((c) => c.id === headCommitId) || null : null;
|
||||||
|
|
||||||
const state: SectionState = {
|
const state: ProjectState = {
|
||||||
status: project.project_status || "ACTIVE",
|
status: project.project_status || "ACTIVE",
|
||||||
head_commit_id: headCommitId,
|
head_commit_id: headCommitId,
|
||||||
locked_by: project.locked_by ?? null,
|
locked_by: project.locked_by ?? null,
|
||||||
@@ -124,18 +124,18 @@ export async function restoreSectionCommit(
|
|||||||
return { commit: headCommit, state };
|
return { commit: headCommit, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitSection(sectionId: string, content: string): Promise<SectionSubmission> {
|
export async function submitSection(projectId: string, content: string): Promise<ProjectSubmission> {
|
||||||
// Submit latest commit of project
|
// Submit latest commit of project
|
||||||
const project = await requestJson<Section>(`${API_ENDPOINTS.projects}/${encodeURIComponent(sectionId)}`);
|
const project = await requestJson<Project>(`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}`);
|
||||||
const commitId = project.latest_commit_id;
|
const commitId = project.latest_commit_id;
|
||||||
if (!commitId) {
|
if (!commitId) {
|
||||||
throw new Error("Project has no latest commit to submit");
|
throw new Error("Project has no latest commit to submit");
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestJson<SectionSubmission>(
|
return requestJson<ProjectSubmission>(
|
||||||
API_ENDPOINTS.submissions,
|
API_ENDPOINTS.submissions,
|
||||||
jsonRequestInit("POST", {
|
jsonRequestInit("POST", {
|
||||||
project_id: sectionId,
|
project_id: projectId,
|
||||||
commit_id: commitId,
|
commit_id: commitId,
|
||||||
content: content,
|
content: content,
|
||||||
})
|
})
|
||||||
@@ -23,7 +23,7 @@ type Props = {
|
|||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
sectionStatus: string;
|
projectStatus: string;
|
||||||
commitTitle: string;
|
commitTitle: string;
|
||||||
onCommitTitleChange: (title: string) => void;
|
onCommitTitleChange: (title: string) => void;
|
||||||
commitCount: number;
|
commitCount: number;
|
||||||
@@ -62,7 +62,7 @@ export default function Editor({
|
|||||||
isSaving,
|
isSaving,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
sectionTitle,
|
sectionTitle,
|
||||||
sectionStatus,
|
projectStatus,
|
||||||
commitTitle,
|
commitTitle,
|
||||||
onCommitTitleChange,
|
onCommitTitleChange,
|
||||||
commitCount,
|
commitCount,
|
||||||
@@ -109,7 +109,7 @@ export default function Editor({
|
|||||||
|
|
||||||
<ProjectPanel
|
<ProjectPanel
|
||||||
sectionTitle={sectionTitle}
|
sectionTitle={sectionTitle}
|
||||||
sectionStatus={sectionStatus}
|
projectStatus={projectStatus}
|
||||||
commitCount={commitCount}
|
commitCount={commitCount}
|
||||||
latestCommitLabel={latestCommitLabel}
|
latestCommitLabel={latestCommitLabel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import type { Entity } from "@/uhm/types/entities";
|
import type { Entity } from "@/uhm/types/entities";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/sections";
|
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
|
|
||||||
type EntityChoice = { id: string; name: string };
|
type EntityChoice = { id: string; name: string };
|
||||||
type WikiChoice = { id: string; title: string; operation?: string };
|
type WikiChoice = { id: string; title: string; operation?: string };
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Panel } from "./Panel";
|
|||||||
|
|
||||||
type ProjectPanelProps = {
|
type ProjectPanelProps = {
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
sectionStatus: string;
|
projectStatus: string;
|
||||||
commitCount: number;
|
commitCount: number;
|
||||||
latestCommitLabel: string | null;
|
latestCommitLabel: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProjectPanel({
|
export function ProjectPanel({
|
||||||
sectionTitle,
|
sectionTitle,
|
||||||
sectionStatus,
|
projectStatus,
|
||||||
commitCount,
|
commitCount,
|
||||||
latestCommitLabel,
|
latestCommitLabel,
|
||||||
}: ProjectPanelProps) {
|
}: ProjectPanelProps) {
|
||||||
@@ -18,7 +18,7 @@ export function ProjectPanel({
|
|||||||
<div style={{ fontSize: 12, color: "#cbd5e1", lineHeight: 1.4 }}>
|
<div style={{ fontSize: 12, color: "#cbd5e1", lineHeight: 1.4 }}>
|
||||||
<div style={{ color: "white", fontWeight: 850, overflowWrap: "anywhere" }}>{sectionTitle}</div>
|
<div style={{ color: "white", fontWeight: 850, overflowWrap: "anywhere" }}>{sectionTitle}</div>
|
||||||
<div style={{ marginTop: 6 }}>
|
<div style={{ marginTop: 6 }}>
|
||||||
Status: <span style={{ color: "#e2e8f0" }}>{sectionStatus}</span>
|
Status: <span style={{ color: "#e2e8f0" }}>{projectStatus}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 6 }}>
|
<div style={{ marginTop: 6 }}>
|
||||||
Commits: <span style={{ color: "#e2e8f0" }}>{commitCount}</span>
|
Commits: <span style={{ color: "#e2e8f0" }}>{commitCount}</span>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
} from "@/uhm/types/geo";
|
} from "@/uhm/types/geo";
|
||||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/sections";
|
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
|
|
||||||
export type Change = GeometryChange;
|
export type Change = GeometryChange;
|
||||||
|
|
||||||
|
|||||||
+53
-53
@@ -2,17 +2,17 @@ import { useCallback } from "react";
|
|||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { ApiError } from "@/uhm/api/http";
|
import { ApiError } from "@/uhm/api/http";
|
||||||
import {
|
import {
|
||||||
createSection,
|
createProject,
|
||||||
createSectionCommit,
|
createProjectCommit,
|
||||||
fetchSectionCommits,
|
fetchProjectCommits,
|
||||||
fetchSections,
|
fetchProjects,
|
||||||
openSectionEditor,
|
openSectionEditor,
|
||||||
submitSection,
|
submitSection,
|
||||||
} from "@/uhm/api/sections";
|
} from "@/uhm/api/projects";
|
||||||
import { buildEditorSnapshot, normalizeEditorSnapshot } from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
import { buildEditorSnapshot, normalizeEditorSnapshot } from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
||||||
import type { Change } from "@/uhm/lib/editor/draft/editorTypes";
|
import type { Change } from "@/uhm/lib/editor/draft/editorTypes";
|
||||||
import type { Feature, FeatureCollection, FeatureId, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
import type { Feature, FeatureCollection, FeatureId, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
||||||
import type { EditorSnapshot, Section, SectionCommit, SectionState, EntityWikiLinkSnapshot } from "@/uhm/types/sections";
|
import type { EditorSnapshot, Project, ProjectCommit, ProjectState, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ type Options = {
|
|||||||
editor: EditorDraftApi;
|
editor: EditorDraftApi;
|
||||||
editorUserId: string;
|
editorUserId: string;
|
||||||
emptyFeatureCollection: FeatureCollection;
|
emptyFeatureCollection: FeatureCollection;
|
||||||
activeSection: Section | null;
|
activeSection: Project | null;
|
||||||
sectionState: SectionState | null;
|
projectState: ProjectState | null;
|
||||||
selectedSectionId: string;
|
selectedProjectId: string;
|
||||||
newSectionTitle: string;
|
newSectionTitle: string;
|
||||||
pendingSaveCount: number;
|
pendingSaveCount: number;
|
||||||
snapshotEntities: EntitySnapshot[];
|
snapshotEntities: EntitySnapshot[];
|
||||||
@@ -37,12 +37,12 @@ type Options = {
|
|||||||
snapshotEntityWikiLinks: EntityWikiLinkSnapshot[];
|
snapshotEntityWikiLinks: EntityWikiLinkSnapshot[];
|
||||||
baselineSnapshot: EditorSnapshot | null;
|
baselineSnapshot: EditorSnapshot | null;
|
||||||
commitTitle: string;
|
commitTitle: string;
|
||||||
setActiveSection: Dispatch<SetStateAction<Section | null>>;
|
setActiveSection: Dispatch<SetStateAction<Project | null>>;
|
||||||
setSelectedSectionId: Dispatch<SetStateAction<string>>;
|
setSelectedProjectId: Dispatch<SetStateAction<string>>;
|
||||||
setSectionState: Dispatch<SetStateAction<SectionState | null>>;
|
setProjectState: Dispatch<SetStateAction<ProjectState | null>>;
|
||||||
setBaselineSnapshot: Dispatch<SetStateAction<EditorSnapshot | null>>;
|
setBaselineSnapshot: Dispatch<SetStateAction<EditorSnapshot | null>>;
|
||||||
setInitialData: Dispatch<SetStateAction<FeatureCollection>>;
|
setInitialData: Dispatch<SetStateAction<FeatureCollection>>;
|
||||||
setSectionCommits: Dispatch<SetStateAction<SectionCommit[]>>;
|
setProjectCommits: Dispatch<SetStateAction<ProjectCommit[]>>;
|
||||||
setSnapshotEntities: Dispatch<SetStateAction<EntitySnapshot[]>>;
|
setSnapshotEntities: Dispatch<SetStateAction<EntitySnapshot[]>>;
|
||||||
setSnapshotWikis: Dispatch<SetStateAction<WikiSnapshot[]>>;
|
setSnapshotWikis: Dispatch<SetStateAction<WikiSnapshot[]>>;
|
||||||
setSnapshotEntityWikiLinks: Dispatch<SetStateAction<EntityWikiLinkSnapshot[]>>;
|
setSnapshotEntityWikiLinks: Dispatch<SetStateAction<EntityWikiLinkSnapshot[]>>;
|
||||||
@@ -52,27 +52,27 @@ type Options = {
|
|||||||
setIsSaving: Dispatch<SetStateAction<boolean>>;
|
setIsSaving: Dispatch<SetStateAction<boolean>>;
|
||||||
setIsSubmitting: Dispatch<SetStateAction<boolean>>;
|
setIsSubmitting: Dispatch<SetStateAction<boolean>>;
|
||||||
setIsOpeningSection: Dispatch<SetStateAction<boolean>>;
|
setIsOpeningSection: Dispatch<SetStateAction<boolean>>;
|
||||||
setAvailableSections: Dispatch<SetStateAction<Section[]>>;
|
setAvailableSections: Dispatch<SetStateAction<Project[]>>;
|
||||||
setNewSectionTitle: Dispatch<SetStateAction<string>>;
|
setNewSectionTitle: Dispatch<SetStateAction<string>>;
|
||||||
setCommitTitle: Dispatch<SetStateAction<string>>;
|
setCommitTitle: Dispatch<SetStateAction<string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useSectionCommands(options: Options) {
|
export function useProjectCommands(options: Options) {
|
||||||
const openSectionForEditing = useCallback(async (sectionId: string) => {
|
const openSectionForEditing = useCallback(async (projectId: string) => {
|
||||||
const editorPayload = await openSectionEditor(sectionId);
|
const editorPayload = await openSectionEditor(projectId);
|
||||||
const snapshot = normalizeEditorSnapshot(editorPayload.snapshot);
|
const snapshot = normalizeEditorSnapshot(editorPayload.snapshot);
|
||||||
// When starting a fresh editor session from a commit snapshot, treat all rows as baseline state:
|
// When starting a fresh editor session from a commit snapshot, treat all rows as baseline state:
|
||||||
// operations should not carry over as deltas into the next commit.
|
// operations should not carry over as deltas into the next commit.
|
||||||
const sessionSnapshot = snapshot ? toEditorSessionSnapshot(snapshot) : null;
|
const sessionSnapshot = snapshot ? toEditorSessionSnapshot(snapshot) : null;
|
||||||
const commits = await fetchSectionCommits(sectionId);
|
const commits = await fetchProjectCommits(projectId);
|
||||||
const nextInitialData = sessionSnapshot?.editor_feature_collection || options.emptyFeatureCollection;
|
const nextInitialData = sessionSnapshot?.editor_feature_collection || options.emptyFeatureCollection;
|
||||||
|
|
||||||
options.setActiveSection(editorPayload.section);
|
options.setActiveSection(editorPayload.project);
|
||||||
options.setSelectedSectionId(editorPayload.section.id);
|
options.setSelectedProjectId(editorPayload.project.id);
|
||||||
options.setSectionState(editorPayload.state);
|
options.setProjectState(editorPayload.state);
|
||||||
options.setBaselineSnapshot(sessionSnapshot);
|
options.setBaselineSnapshot(sessionSnapshot);
|
||||||
options.setInitialData(nextInitialData);
|
options.setInitialData(nextInitialData);
|
||||||
options.setSectionCommits(commits);
|
options.setProjectCommits(commits);
|
||||||
options.setSnapshotEntities(sessionSnapshot?.entities || []);
|
options.setSnapshotEntities(sessionSnapshot?.entities || []);
|
||||||
options.setSnapshotWikis(sessionSnapshot?.wikis || []);
|
options.setSnapshotWikis(sessionSnapshot?.wikis || []);
|
||||||
options.setSnapshotEntityWikiLinks(sessionSnapshot?.entity_wiki || []);
|
options.setSnapshotEntityWikiLinks(sessionSnapshot?.entity_wiki || []);
|
||||||
@@ -81,8 +81,8 @@ export function useSectionCommands(options: Options) {
|
|||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
const commitSection = useCallback(async () => {
|
const commitSection = useCallback(async () => {
|
||||||
if (!options.activeSection || !options.sectionState) {
|
if (!options.activeSection || !options.projectState) {
|
||||||
options.setEntityStatus("Chưa mở được section editor.");
|
options.setEntityStatus("Chưa mở được project editor.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.pendingSaveCount <= 0) {
|
if (options.pendingSaveCount <= 0) {
|
||||||
@@ -95,7 +95,7 @@ export function useSectionCommands(options: Options) {
|
|||||||
options.setEntityStatus(null);
|
options.setEntityStatus(null);
|
||||||
try {
|
try {
|
||||||
const snapshot = buildEditorSnapshot({
|
const snapshot = buildEditorSnapshot({
|
||||||
section: options.activeSection,
|
project: options.activeSection,
|
||||||
draft: options.editor.draft,
|
draft: options.editor.draft,
|
||||||
changes: geometryChanges,
|
changes: geometryChanges,
|
||||||
snapshotEntities: options.snapshotEntities,
|
snapshotEntities: options.snapshotEntities,
|
||||||
@@ -124,13 +124,13 @@ export function useSectionCommands(options: Options) {
|
|||||||
// If stringify fails, let API call throw a more actionable error downstream.
|
// If stringify fails, let API call throw a more actionable error downstream.
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await createSectionCommit(options.activeSection.id, {
|
const result = await createProjectCommit(options.activeSection.id, {
|
||||||
snapshot,
|
snapshot,
|
||||||
edit_summary: editSummary,
|
edit_summary: editSummary,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionSnapshot = toEditorSessionSnapshot(snapshot);
|
const sessionSnapshot = toEditorSessionSnapshot(snapshot);
|
||||||
options.setSectionState(result.state);
|
options.setProjectState(result.state);
|
||||||
options.setBaselineSnapshot(sessionSnapshot);
|
options.setBaselineSnapshot(sessionSnapshot);
|
||||||
options.setSnapshotEntities(sessionSnapshot.entities || []);
|
options.setSnapshotEntities(sessionSnapshot.entities || []);
|
||||||
options.setSnapshotWikis(sessionSnapshot.wikis || []);
|
options.setSnapshotWikis(sessionSnapshot.wikis || []);
|
||||||
@@ -138,7 +138,7 @@ export function useSectionCommands(options: Options) {
|
|||||||
options.setInitialData(options.editor.draft);
|
options.setInitialData(options.editor.draft);
|
||||||
options.editor.clearChanges();
|
options.editor.clearChanges();
|
||||||
options.setCommitTitle("");
|
options.setCommitTitle("");
|
||||||
options.setSectionCommits(await fetchSectionCommits(options.activeSection.id));
|
options.setProjectCommits(await fetchProjectCommits(options.activeSection.id));
|
||||||
options.setEntityFormStatus("Đã tạo commit.");
|
options.setEntityFormStatus("Đã tạo commit.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
@@ -154,26 +154,26 @@ export function useSectionCommands(options: Options) {
|
|||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
const openSelectedSection = useCallback(async () => {
|
const openSelectedSection = useCallback(async () => {
|
||||||
const sectionId = options.selectedSectionId.trim();
|
const projectId = options.selectedProjectId.trim();
|
||||||
if (!sectionId) {
|
if (!projectId) {
|
||||||
options.setEntityStatus("Hãy chọn section để mở.");
|
options.setEntityStatus("Hãy chọn project để mở.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.pendingSaveCount > 0) {
|
if (options.pendingSaveCount > 0) {
|
||||||
const confirmed = window.confirm("Section hiện tại có thay đổi chưa Commit. Mở section khác sẽ bỏ các thay đổi này. Tiếp tục?");
|
const confirmed = window.confirm("Project hiện tại có thay đổi chưa Commit. Mở project khác sẽ bỏ các thay đổi này. Tiếp tục?");
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.setIsOpeningSection(true);
|
options.setIsOpeningSection(true);
|
||||||
options.setEntityStatus(null);
|
options.setEntityStatus(null);
|
||||||
try {
|
try {
|
||||||
await openSectionForEditing(sectionId);
|
await openSectionForEditing(projectId);
|
||||||
options.setEntityStatus("Đã mở section để chỉnh sửa.");
|
options.setEntityStatus("Đã mở project để chỉnh sửa.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
options.setEntityStatus(`Mở section thất bại: ${err.body}`);
|
options.setEntityStatus(`Mở project thất bại: ${err.body}`);
|
||||||
} else {
|
} else {
|
||||||
options.setEntityStatus("Mở section thất bại.");
|
options.setEntityStatus("Mở project thất bại.");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
options.setIsOpeningSection(false);
|
options.setIsOpeningSection(false);
|
||||||
@@ -183,31 +183,31 @@ export function useSectionCommands(options: Options) {
|
|||||||
const createAndOpenSection = useCallback(async () => {
|
const createAndOpenSection = useCallback(async () => {
|
||||||
const title = options.newSectionTitle.trim();
|
const title = options.newSectionTitle.trim();
|
||||||
if (!title) {
|
if (!title) {
|
||||||
options.setEntityStatus("Tên section là bắt buộc.");
|
options.setEntityStatus("Tên project là bắt buộc.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.pendingSaveCount > 0) {
|
if (options.pendingSaveCount > 0) {
|
||||||
const confirmed = window.confirm("Section hiện tại có thay đổi chưa Commit. Tạo section mới sẽ bỏ các thay đổi này. Tiếp tục?");
|
const confirmed = window.confirm("Project hiện tại có thay đổi chưa Commit. Tạo project mới sẽ bỏ các thay đổi này. Tiếp tục?");
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.setIsOpeningSection(true);
|
options.setIsOpeningSection(true);
|
||||||
options.setEntityStatus(null);
|
options.setEntityStatus(null);
|
||||||
try {
|
try {
|
||||||
const section = await createSection({
|
const project = await createProject({
|
||||||
title,
|
title,
|
||||||
description: null,
|
description: null,
|
||||||
});
|
});
|
||||||
const sections = await fetchSections();
|
const projects = await fetchProjects();
|
||||||
options.setAvailableSections(sections);
|
options.setAvailableSections(projects);
|
||||||
options.setNewSectionTitle("");
|
options.setNewSectionTitle("");
|
||||||
await openSectionForEditing(section.id);
|
await openSectionForEditing(project.id);
|
||||||
options.setEntityStatus("Đã tạo và mở section mới.");
|
options.setEntityStatus("Đã tạo và mở project mới.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
options.setEntityStatus(`Tạo section thất bại: ${err.body}`);
|
options.setEntityStatus(`Tạo project thất bại: ${err.body}`);
|
||||||
} else {
|
} else {
|
||||||
options.setEntityStatus("Tạo section thất bại.");
|
options.setEntityStatus("Tạo project thất bại.");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
options.setIsOpeningSection(false);
|
options.setIsOpeningSection(false);
|
||||||
@@ -215,8 +215,8 @@ export function useSectionCommands(options: Options) {
|
|||||||
}, [openSectionForEditing, options]);
|
}, [openSectionForEditing, options]);
|
||||||
|
|
||||||
const submitCurrentSection = useCallback(async (content: string) => {
|
const submitCurrentSection = useCallback(async (content: string) => {
|
||||||
if (!options.activeSection || !options.sectionState?.head_commit_id) {
|
if (!options.activeSection || !options.projectState?.head_commit_id) {
|
||||||
options.setEntityStatus("Section hiện tại chưa có head để submit.");
|
options.setEntityStatus("Project hiện tại chưa có head để submit.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.pendingSaveCount > 0) {
|
if (options.pendingSaveCount > 0) {
|
||||||
@@ -241,8 +241,8 @@ export function useSectionCommands(options: Options) {
|
|||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
const restoreCommit = useCallback(async (commitId: string) => {
|
const restoreCommit = useCallback(async (commitId: string) => {
|
||||||
if (!options.activeSection || !options.sectionState) {
|
if (!options.activeSection || !options.projectState) {
|
||||||
options.setEntityStatus("Chưa mở được section editor.");
|
options.setEntityStatus("Chưa mở được project editor.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.pendingSaveCount > 0) {
|
if (options.pendingSaveCount > 0) {
|
||||||
@@ -255,8 +255,8 @@ export function useSectionCommands(options: Options) {
|
|||||||
try {
|
try {
|
||||||
// FE-only restore: load snapshot from selected commit and apply to editor state.
|
// FE-only restore: load snapshot from selected commit and apply to editor state.
|
||||||
// Do NOT move project's head commit on backend.
|
// Do NOT move project's head commit on backend.
|
||||||
const commits = await fetchSectionCommits(options.activeSection.id);
|
const commits = await fetchProjectCommits(options.activeSection.id);
|
||||||
const target = commits.find((c: SectionCommit) => c.id === commitId) || null;
|
const target = commits.find((c: ProjectCommit) => c.id === commitId) || null;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
options.setEntityStatus("Không tìm thấy commit để restore.");
|
options.setEntityStatus("Không tìm thấy commit để restore.");
|
||||||
return;
|
return;
|
||||||
@@ -274,8 +274,8 @@ export function useSectionCommands(options: Options) {
|
|||||||
options.setSelectedFeatureIds([]);
|
options.setSelectedFeatureIds([]);
|
||||||
options.setEntityFormStatus(null);
|
options.setEntityFormStatus(null);
|
||||||
|
|
||||||
// Refresh commits list for UI, but keep sectionState/head as-is.
|
// Refresh commits list for UI, but keep projectState/head as-is.
|
||||||
options.setSectionCommits(commits);
|
options.setProjectCommits(commits);
|
||||||
options.setEntityFormStatus("Đã load snapshot từ commit (không đổi head trên BE).");
|
options.setEntityFormStatus("Đã load snapshot từ commit (không đổi head trên BE).");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
+22
-22
@@ -1,15 +1,15 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import type { EditorSnapshot, Section, SectionCommit, SectionState } from "@/uhm/types/sections";
|
import type { EditorSnapshot, Project, ProjectCommit, ProjectState } from "@/uhm/types/projects";
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
defaultEditorUserId: string;
|
defaultEditorUserId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SectionTask = "idle" | "saving" | "submitting" | "opening-section";
|
type SectionTask = "idle" | "saving" | "submitting" | "opening-project";
|
||||||
|
|
||||||
export function useSectionSessionState(options: Options) {
|
export function useProjectSessionState(options: Options) {
|
||||||
// Single state machine cho các tác vụ async của section (saving/submitting/opening).
|
// Single state machine cho các tác vụ async của project (saving/submitting/opening).
|
||||||
const [sectionTask, setSectionTask] = useState<SectionTask>("idle");
|
const [sectionTask, setSectionTask] = useState<SectionTask>("idle");
|
||||||
const setTaskFlag = useCallback((task: Exclude<SectionTask, "idle">, next: SetStateAction<boolean>) => {
|
const setTaskFlag = useCallback((task: Exclude<SectionTask, "idle">, next: SetStateAction<boolean>) => {
|
||||||
setSectionTask((prev) => {
|
setSectionTask((prev) => {
|
||||||
@@ -22,7 +22,7 @@ export function useSectionSessionState(options: Options) {
|
|||||||
|
|
||||||
const isSaving = sectionTask === "saving";
|
const isSaving = sectionTask === "saving";
|
||||||
const isSubmitting = sectionTask === "submitting";
|
const isSubmitting = sectionTask === "submitting";
|
||||||
const isOpeningSection = sectionTask === "opening-section";
|
const isOpeningSection = sectionTask === "opening-project";
|
||||||
const setIsSaving: Dispatch<SetStateAction<boolean>> = useCallback((next) => {
|
const setIsSaving: Dispatch<SetStateAction<boolean>> = useCallback((next) => {
|
||||||
setTaskFlag("saving", next);
|
setTaskFlag("saving", next);
|
||||||
}, [setTaskFlag]);
|
}, [setTaskFlag]);
|
||||||
@@ -30,25 +30,25 @@ export function useSectionSessionState(options: Options) {
|
|||||||
setTaskFlag("submitting", next);
|
setTaskFlag("submitting", next);
|
||||||
}, [setTaskFlag]);
|
}, [setTaskFlag]);
|
||||||
const setIsOpeningSection: Dispatch<SetStateAction<boolean>> = useCallback((next) => {
|
const setIsOpeningSection: Dispatch<SetStateAction<boolean>> = useCallback((next) => {
|
||||||
setTaskFlag("opening-section", next);
|
setTaskFlag("opening-project", next);
|
||||||
}, [setTaskFlag]);
|
}, [setTaskFlag]);
|
||||||
|
|
||||||
// Danh sách sections để user chọn mở.
|
// Danh sách projects để user chọn mở.
|
||||||
const [availableSections, setAvailableSections] = useState<Section[]>([]);
|
const [availableSections, setAvailableSections] = useState<Project[]>([]);
|
||||||
// Section ID đang được chọn trong dropdown.
|
// Project ID đang được chọn trong dropdown.
|
||||||
const [selectedSectionId, setSelectedSectionId] = useState("");
|
const [selectedProjectId, setSelectedProjectId] = useState("");
|
||||||
// Title section mới (để create).
|
// Title project mới (để create).
|
||||||
const [newSectionTitle, setNewSectionTitle] = useState("");
|
const [newSectionTitle, setNewSectionTitle] = useState("");
|
||||||
// Input title cho commit.
|
// Input title cho commit.
|
||||||
const [commitTitle, setCommitTitle] = useState("");
|
const [commitTitle, setCommitTitle] = useState("");
|
||||||
// User ID dùng để gắn vào commit/submit/lock.
|
// User ID dùng để gắn vào commit/submit/lock.
|
||||||
const [editorUserIdInput, setEditorUserIdInput] = useState(options.defaultEditorUserId);
|
const [editorUserIdInput, setEditorUserIdInput] = useState(options.defaultEditorUserId);
|
||||||
// Section đang mở để edit (null nếu chưa mở).
|
// Project đang mở để edit (null nếu chưa mở).
|
||||||
const [activeSection, setActiveSection] = useState<Section | null>(null);
|
const [activeSection, setActiveSection] = useState<Project | null>(null);
|
||||||
// Trạng thái section (version/head/status/lock).
|
// Trạng thái project (version/head/status/lock).
|
||||||
const [sectionState, setSectionState] = useState<SectionState | null>(null);
|
const [projectState, setProjectState] = useState<ProjectState | null>(null);
|
||||||
// Danh sách commits của section đang mở.
|
// Danh sách commits của project đang mở.
|
||||||
const [sectionCommits, setSectionCommits] = useState<SectionCommit[]>([]);
|
const [sectionCommits, setProjectCommits] = useState<ProjectCommit[]>([]);
|
||||||
// Baseline snapshot currently loaded for this editor session.
|
// Baseline snapshot currently loaded for this editor session.
|
||||||
const [baselineSnapshot, setBaselineSnapshot] = useState<EditorSnapshot | null>(null);
|
const [baselineSnapshot, setBaselineSnapshot] = useState<EditorSnapshot | null>(null);
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ export function useSectionSessionState(options: Options) {
|
|||||||
setIsOpeningSection,
|
setIsOpeningSection,
|
||||||
availableSections,
|
availableSections,
|
||||||
setAvailableSections,
|
setAvailableSections,
|
||||||
selectedSectionId,
|
selectedProjectId,
|
||||||
setSelectedSectionId,
|
setSelectedProjectId,
|
||||||
newSectionTitle,
|
newSectionTitle,
|
||||||
setNewSectionTitle,
|
setNewSectionTitle,
|
||||||
commitTitle,
|
commitTitle,
|
||||||
@@ -71,10 +71,10 @@ export function useSectionSessionState(options: Options) {
|
|||||||
setEditorUserIdInput,
|
setEditorUserIdInput,
|
||||||
activeSection,
|
activeSection,
|
||||||
setActiveSection,
|
setActiveSection,
|
||||||
sectionState,
|
projectState,
|
||||||
setSectionState,
|
setProjectState,
|
||||||
sectionCommits,
|
sectionCommits,
|
||||||
setSectionCommits,
|
setProjectCommits,
|
||||||
baselineSnapshot,
|
baselineSnapshot,
|
||||||
setBaselineSnapshot,
|
setBaselineSnapshot,
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/sections";
|
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
|
|
||||||
export function useWikiSessionState() {
|
export function useWikiSessionState() {
|
||||||
const [snapshotWikis, setSnapshotWikis] = useState<WikiSnapshot[]>([]);
|
const [snapshotWikis, setSnapshotWikis] = useState<WikiSnapshot[]>([]);
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import type { Change } from "@/uhm/lib/editor/draft/editorTypes";
|
|||||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||||
import type { EntitySnapshotOperation } from "@/uhm/types/entities";
|
import type { EntitySnapshotOperation } from "@/uhm/types/entities";
|
||||||
import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
||||||
import type { EditorSnapshot, Section } from "@/uhm/types/sections";
|
import type { EditorSnapshot, Project } from "@/uhm/types/projects";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/sections";
|
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
|
|
||||||
type UnknownRecord = Record<string, unknown>;
|
type UnknownRecord = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildEditorSnapshot(options: {
|
export function buildEditorSnapshot(options: {
|
||||||
section: Section;
|
project: Project;
|
||||||
draft: FeatureCollection;
|
draft: FeatureCollection;
|
||||||
changes: Change[];
|
changes: Change[];
|
||||||
snapshotEntities: EntitySnapshot[];
|
snapshotEntities: EntitySnapshot[];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|||||||
import type { FeatureCollection } from "@/uhm/types/geo";
|
import type { FeatureCollection } from "@/uhm/types/geo";
|
||||||
import { useBackgroundSessionState } from "@/uhm/lib/editor/session/useBackgroundSessionState";
|
import { useBackgroundSessionState } from "@/uhm/lib/editor/session/useBackgroundSessionState";
|
||||||
import { useEntitySessionState } from "@/uhm/lib/editor/session/useEntitySessionState";
|
import { useEntitySessionState } from "@/uhm/lib/editor/session/useEntitySessionState";
|
||||||
import { useSectionSessionState } from "@/uhm/lib/editor/session/useSectionSessionState";
|
import { useProjectSessionState } from "@/uhm/lib/editor/session/useProjectSessionState";
|
||||||
import { useTimelineState } from "@/uhm/lib/editor/session/useTimelineState";
|
import { useTimelineState } from "@/uhm/lib/editor/session/useTimelineState";
|
||||||
import { useWikiSessionState } from "@/uhm/lib/editor/session/useWikiSessionState";
|
import { useWikiSessionState } from "@/uhm/lib/editor/session/useWikiSessionState";
|
||||||
import type { EditorMode, TimelineRange } from "@/uhm/lib/editor/session/sessionTypes";
|
import type { EditorMode, TimelineRange } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
@@ -24,10 +24,10 @@ type Options = {
|
|||||||
export function useEditorSessionState(options: Options) {
|
export function useEditorSessionState(options: Options) {
|
||||||
// Mode thao tác map/editor hiện tại.
|
// Mode thao tác map/editor hiện tại.
|
||||||
const [mode, setMode] = useState<EditorMode>("idle");
|
const [mode, setMode] = useState<EditorMode>("idle");
|
||||||
// FeatureCollection "gốc" của session hiện tại (global timeline hoặc section snapshot).
|
// FeatureCollection "gốc" của session hiện tại (global timeline hoặc project snapshot).
|
||||||
const [initialData, setInitialData] = useState<FeatureCollection>(options.emptyFeatureCollection);
|
const [initialData, setInitialData] = useState<FeatureCollection>(options.emptyFeatureCollection);
|
||||||
|
|
||||||
const section = useSectionSessionState({
|
const project = useProjectSessionState({
|
||||||
defaultEditorUserId: options.defaultEditorUserId,
|
defaultEditorUserId: options.defaultEditorUserId,
|
||||||
});
|
});
|
||||||
const entity = useEntitySessionState();
|
const entity = useEntitySessionState();
|
||||||
@@ -43,7 +43,7 @@ export function useEditorSessionState(options: Options) {
|
|||||||
setMode,
|
setMode,
|
||||||
initialData,
|
initialData,
|
||||||
setInitialData,
|
setInitialData,
|
||||||
...section,
|
...project,
|
||||||
...entity,
|
...entity,
|
||||||
...timeline,
|
...timeline,
|
||||||
...background,
|
...background,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useUndoStack } from "@/uhm/lib/editor/draft/useUndoStack";
|
|||||||
import type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
|
import type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
|
||||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/sections";
|
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||||
|
|
||||||
export type { Feature, FeatureCollection, FeatureProperties, Geometry } from "@/uhm/types/geo";
|
export type { Feature, FeatureCollection, FeatureProperties, Geometry } from "@/uhm/types/geo";
|
||||||
export type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
|
export type { Change, UndoAction } from "@/uhm/lib/editor/draft/editorTypes";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type EntityWikiLinkSnapshot = {
|
|||||||
operation?: "reference" | "binding" | "delete";
|
operation?: "reference" | "binding" | "delete";
|
||||||
};
|
};
|
||||||
|
|
||||||
// BackEndGo uses Projects/Commits/Submissions. "Section" is legacy naming in FE.
|
// BackEndGo uses Projects/Commits/Submissions. "Project" is legacy naming in FE.
|
||||||
export type ProjectStatus = string;
|
export type ProjectStatus = string;
|
||||||
export type ProjectSubmissionStatus = "PENDING" | "APPROVED" | "REJECTED" | string;
|
export type ProjectSubmissionStatus = "PENDING" | "APPROVED" | "REJECTED" | string;
|
||||||
|
|
||||||
@@ -71,9 +71,9 @@ export type ProjectSubmission = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type EditorSnapshot = {
|
export type EditorSnapshot = {
|
||||||
// Legacy: before BEGo flow moved fully to project/commit records, FE stored a minimal "section" ref
|
// Legacy: before BEGo flow moved fully to project/commit records, FE stored a minimal "project" ref
|
||||||
// inside snapshot_json. New snapshots omit this entirely.
|
// inside snapshot_json. New snapshots omit this entirely.
|
||||||
section?: {
|
project?: {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
@@ -90,13 +90,13 @@ export type EditorSnapshot = {
|
|||||||
export type CommitSnapshot = EditorSnapshot;
|
export type CommitSnapshot = EditorSnapshot;
|
||||||
|
|
||||||
export type EditorLoadResponse = {
|
export type EditorLoadResponse = {
|
||||||
section: Project;
|
project: Project;
|
||||||
state: ProjectState;
|
state: ProjectState;
|
||||||
commit: ProjectCommit | null;
|
commit: ProjectCommit | null;
|
||||||
snapshot: EditorSnapshot | null;
|
snapshot: EditorSnapshot | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateSectionInput = {
|
export type CreateProjectInput = {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
status?: "PRIVATE" | "PUBLIC" | "ARCHIVE";
|
status?: "PRIVATE" | "PUBLIC" | "ARCHIVE";
|
||||||
@@ -111,10 +111,3 @@ export type RestoreCommitInput = {
|
|||||||
commit_id: string;
|
commit_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Legacy aliases (to reduce churn in existing FE code). Prefer Project* names above.
|
|
||||||
export type SectionStatus = ProjectStatus;
|
|
||||||
export type SectionSubmissionStatus = ProjectSubmissionStatus;
|
|
||||||
export type SectionState = ProjectState;
|
|
||||||
export type Section = Project;
|
|
||||||
export type SectionCommit = ProjectCommit;
|
|
||||||
export type SectionSubmission = ProjectSubmission;
|
|
||||||
Reference in New Issue
Block a user