From cb3e720644317972f6ecd9bfb118b22bc4816246 Mon Sep 17 00:00:00 2001 From: taDuc Date: Tue, 12 May 2026 21:35:27 +0700 Subject: [PATCH] editor UI for better experience :)) --- src/app/editor/[id]/page.tsx | 56 ++++++++- src/app/user/about-us/page.tsx | 2 +- .../editor/EntityWikiBindingsPanel.tsx | 73 ++++++++---- .../editor/GeometryBindingPanel.tsx | 111 ++++++++++++++++-- .../editor/ProjectEntityRefsPanel.tsx | 40 ++++--- src/uhm/components/map/mapUtils.ts | 18 ++- src/uhm/components/map/useMapSync.ts | 36 ++++-- src/uhm/components/wiki/WikiSidebarPanel.tsx | 75 +++++++----- 8 files changed, 309 insertions(+), 102 deletions(-) diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx index 3ce8602..42b7e2b 100644 --- a/src/app/editor/[id]/page.tsx +++ b/src/app/editor/[id]/page.tsx @@ -20,6 +20,7 @@ import { searchGeometriesByEntityName, type EntityGeometriesSearchItem, type Ent import type { EntitySnapshot } from "@/uhm/types/entities"; import { Feature, + FeatureCollection, Geometry, useEditorState, } from "@/uhm/lib/editor/state/useEditorState"; @@ -93,6 +94,10 @@ export default function Page() { const entityFormStatusTimeoutRef = useRef(null); const geoBindingStatusTimeoutRef = useRef(null); const [geoBindingStatus, setGeoBindingStatus] = useState(null); + const [geometryFocusRequest, setGeometryFocusRequest] = useState<{ + key: number; + collection: FeatureCollection; + } | null>(null); const lastSelectedFeatureIdRef = useRef(null); const { @@ -279,17 +284,26 @@ export default function Page() { const selectedFeature = selectedFeatures.length > 0 && isMultiEditValid ? selectedFeatures[0] : null; const geometryChoices = useMemo(() => { + const createdGeometryIds = new Set(); + for (const [id, change] of editor.changes.entries()) { + if (change.action === "create") createdGeometryIds.add(String(id)); + } + const rows = (editor.draft.features || []) .filter((f) => f && f.properties && (typeof f.properties.id === "string" || typeof f.properties.id === "number")) .map((f) => { const id = String(f.properties.id); const semantic = String(f.properties.type || getDefaultTypeIdForFeature(f) || "").trim(); const label = semantic.length ? `${semantic} (${f.geometry.type})` : f.geometry.type; - return { id, label }; + return { + id, + label, + isNew: createdGeometryIds.has(id) || !editor.hasPersistedFeature(f.properties.id), + }; }); rows.sort((a, b) => a.id.localeCompare(b.id)); return rows; - }, [editor.draft.features]); + }, [editor]); const selectedGeometryBindingIds = useMemo(() => { if (!selectedFeature) return []; @@ -985,6 +999,40 @@ export default function Page() { setIsEntitySubmitting, ]); + const handleFocusGeometryFromBindingPanel = useCallback((geoId: string) => { + const id = String(geoId || "").trim(); + if (!id) return; + + const feature = editor.draft.features.find((item) => String(item.properties.id) === id) || null; + if (!feature) { + flashGeoBindingStatus("Không tìm thấy geometry để zoom."); + return; + } + + const visibleInCurrentTimeline = timelineVisibleDraft.features.some( + (item) => String(item.properties.id) === id + ); + if (timelineFilterEnabled && !visibleInCurrentTimeline) { + setTimelineFilterEnabled(false); + } + + setSelectedFeatureIds([feature.properties.id]); + setGeometryFocusRequest((prev) => ({ + key: (prev?.key ?? 0) + 1, + collection: { + type: "FeatureCollection", + features: [feature], + }, + })); + }, [ + editor.draft.features, + flashGeoBindingStatus, + setSelectedFeatureIds, + setTimelineFilterEnabled, + timelineFilterEnabled, + timelineVisibleDraft.features, + ]); + const handleAddWikiRefToProject = useCallback((wiki: Wiki) => { const id = String(wiki.id || "").trim(); if (!id) return; @@ -1245,6 +1293,9 @@ export default function Page() { backgroundVisibility={backgroundVisibility} geometryVisibility={geometryVisibility} respectBindingFilter={geometryBindingFilterEnabled} + focusFeatureCollection={geometryFocusRequest?.collection || null} + focusRequestKey={geometryFocusRequest?.key ?? null} + focusPadding={96} /> ) : (
@@ -1513,6 +1564,7 @@ export default function Page() { selectedGeometryId={selectedFeature ? String(selectedFeature.properties.id) : null} selectedGeometryBindingIds={selectedGeometryBindingIds} onToggleBindGeometryForSelectedGeometry={handleToggleBindGeometryForSelectedGeometry} + onFocusGeometry={handleFocusGeometryFromBindingPanel} statusText={geoBindingStatus} bindingFilterEnabled={geometryBindingFilterEnabled} onBindingFilterEnabledChange={setGeometryBindingFilterEnabled} diff --git a/src/app/user/about-us/page.tsx b/src/app/user/about-us/page.tsx index 287e74e..6318459 100644 --- a/src/app/user/about-us/page.tsx +++ b/src/app/user/about-us/page.tsx @@ -27,7 +27,7 @@ export default function LandingPage() { { name: "Trần Anh Đức", role: "Project Manager", - desc: "Đẹp trai cao m8", + desc: "Fan cứng anh Lại Ngứa Chân", avatar: "/images/teamdev/tad.jpeg", }, { diff --git a/src/uhm/components/editor/EntityWikiBindingsPanel.tsx b/src/uhm/components/editor/EntityWikiBindingsPanel.tsx index c4b4b80..ea5acc4 100644 --- a/src/uhm/components/editor/EntityWikiBindingsPanel.tsx +++ b/src/uhm/components/editor/EntityWikiBindingsPanel.tsx @@ -1,12 +1,11 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import type { Entity } from "@/uhm/types/entities"; +import { useMemo, useState } from "react"; import type { WikiSnapshot } from "@/uhm/types/wiki"; import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects"; type EntityChoice = { id: string; name: string }; -type WikiChoice = { id: string; title: string; operation?: string }; +type WikiChoice = { id: string; title: string }; type Props = { entities: EntityChoice[]; @@ -29,7 +28,7 @@ export default function EntityWikiBindingsPanel({ entities, wikis, links, setLin () => (wikis || []) .filter((w) => w && typeof w.id === "string" && w.id.trim().length > 0) - .map((w) => ({ id: w.id, title: wikiTitle(w), operation: w.operation })), + .map((w) => ({ id: w.id, title: wikiTitle(w) })), [wikis] ); @@ -39,17 +38,6 @@ export default function EntityWikiBindingsPanel({ entities, wikis, links, setLin return cleaned; }, [entities]); - // Don't auto-select entity. The user must explicitly pick one. - // Only clear the selection if the currently selected entity is no longer available. - useEffect(() => { - if (!activeEntityId) return; - const stillExists = entityChoices.some((e) => e.id === activeEntityId); - if (!stillExists) { - setActiveEntityId(""); - setActiveWikiId(""); - } - }, [activeEntityId, entityChoices]); - const activeLinks = useMemo(() => { const set = new Set(); for (const l of links || []) { @@ -144,6 +132,12 @@ export default function EntityWikiBindingsPanel({ entities, wikis, links, setLin ))} + {activeEntityId ? ( + e.id === activeEntityId)?.name || activeEntityId} + id={activeEntityId} + /> + ) : null}
@@ -175,6 +169,12 @@ export default function EntityWikiBindingsPanel({ entities, wikis, links, setLin ))} + {activeWikiChoice ? ( + + ) : null} {wikiChoices.length === 0 ? (
No wiki in project yet.
@@ -228,17 +228,19 @@ export default function EntityWikiBindingsPanel({ entities, wikis, links, setLin title={id} >
-
- {w?.title || "Untitled wiki"} +
+ + {w?.title || "Untitled wiki"} +
{id} @@ -279,6 +281,25 @@ export default function EntityWikiBindingsPanel({ entities, wikis, links, setLin ); } +function ActiveSelectionLabel({ + label, + id, +}: { + label: string; + id: string; +}) { + return ( +
+ + {label} + + + {id} + +
+ ); +} + function PlusIcon() { return (
+ {collapsed ? null : selectedGeometry ? ( +
onFocusGeometry?.(selectedGeometry.id)} + onKeyDown={(event) => handleFocusKeyDown(event, selectedGeometry.id)} + > +
+ Selected +
+
+ + {selectedGeometry.label || selectedGeometry.id} + + {selectedGeometry.isNew ? : null} +
+
+ {selectedGeometry.id} +
+
+ ) : null} + {collapsed ? null : rows.length ? (
{rows @@ -116,22 +189,37 @@ export default function GeometryBindingPanel({ display: "flex", alignItems: "center", gap: 10, + cursor: canFocusGeometry ? "pointer" : "default", opacity: canBindToggle ? 1 : 0.75, }} title={g.id} + role={canFocusGeometry ? "button" : undefined} + tabIndex={canFocusGeometry ? 0 : undefined} + onClick={() => onFocusGeometry?.(g.id)} + onKeyDown={(event) => handleFocusKeyDown(event, g.id)} >
- {g.label || g.id} + + {g.label || g.id} + + {g.isNew ? : null}
onToggleBindGeometryForSelectedGeometry!(g.id, !isBound)} + onClick={(event) => { + event.stopPropagation(); + onToggleBindGeometryForSelectedGeometry!(g.id, !isBound); + }} style={{ display: "inline-flex", alignItems: "center", diff --git a/src/uhm/components/editor/ProjectEntityRefsPanel.tsx b/src/uhm/components/editor/ProjectEntityRefsPanel.tsx index 19c264c..22d5be3 100644 --- a/src/uhm/components/editor/ProjectEntityRefsPanel.tsx +++ b/src/uhm/components/editor/ProjectEntityRefsPanel.tsx @@ -1,8 +1,9 @@ "use client"; -import { useEffect, useMemo, useState, type CSSProperties } from "react"; +import { useMemo, useState, type CSSProperties } from "react"; import type { EntitySnapshot } from "@/uhm/types/entities"; import type { EntityFormState } from "@/uhm/lib/editor/session/sessionTypes"; +import NewBadge from "@/uhm/components/editor/NewBadge"; type Props = { entityRefs: EntitySnapshot[]; @@ -46,18 +47,11 @@ export default function ProjectEntityRefsPanel({ const [editName, setEditName] = useState(""); const [editDescription, setEditDescription] = useState(""); - useEffect(() => { - if (!activeEntityId) return; - if (!entityRefs.some((e) => String(e.id) === String(activeEntityId))) { - setActiveEntityId(null); - } - }, [activeEntityId, entityRefs]); - - useEffect(() => { - if (!activeEntity) return; - setEditName(typeof activeEntity.name === "string" ? activeEntity.name : ""); - setEditDescription(activeEntity.description == null ? "" : String(activeEntity.description)); - }, [activeEntity?.description, activeEntity?.id, activeEntity?.name]); + const openEntityEditor = (entity: EntitySnapshot) => { + setActiveEntityId(String(entity.id)); + setEditName(typeof entity.name === "string" ? entity.name : ""); + setEditDescription(entity.description == null ? "" : String(entity.description)); + }; return (