refactor(important): reduce editor drill state
This commit is contained in:
@@ -1,34 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { persistBackgroundLayerVisibility } from "@/uhm/lib/editor/background/backgroundVisibilityStorage";
|
||||
import {
|
||||
BACKGROUND_LAYER_OPTIONS,
|
||||
BackgroundLayerId,
|
||||
BackgroundLayerVisibility,
|
||||
DEFAULT_BACKGROUND_LAYER_VISIBILITY,
|
||||
HIDDEN_BACKGROUND_LAYER_VISIBILITY,
|
||||
} from "@/uhm/lib/map/styles/backgroundLayers";
|
||||
import { GEO_TYPE_KEYS } from "@/uhm/lib/map/geo/geoTypeMap";
|
||||
import { useEditorStore } from "@/uhm/store/editorStore";
|
||||
|
||||
type Props = {
|
||||
visibility: BackgroundLayerVisibility;
|
||||
onToggleLayer: (id: BackgroundLayerId) => void;
|
||||
onShowAll: () => void;
|
||||
onHideAll: () => void;
|
||||
geometryVisibility?: Record<string, boolean>;
|
||||
onToggleGeometryType?: (typeKey: string) => void;
|
||||
topContent?: ReactNode;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export default function BackgroundLayersPanel({
|
||||
visibility,
|
||||
onToggleLayer,
|
||||
onShowAll,
|
||||
onHideAll,
|
||||
geometryVisibility,
|
||||
onToggleGeometryType,
|
||||
topContent,
|
||||
width = 240,
|
||||
}: Props) {
|
||||
const {
|
||||
visibility,
|
||||
setBackgroundVisibility,
|
||||
geometryVisibility,
|
||||
setGeometryVisibility,
|
||||
} = useEditorStore(
|
||||
useShallow((state) => ({
|
||||
visibility: state.backgroundVisibility,
|
||||
setBackgroundVisibility: state.setBackgroundVisibility,
|
||||
geometryVisibility: state.geometryVisibility,
|
||||
setGeometryVisibility: state.setGeometryVisibility,
|
||||
}))
|
||||
);
|
||||
|
||||
const updateBackgroundVisibility = (
|
||||
updater: (prev: typeof visibility) => typeof visibility
|
||||
) => {
|
||||
setBackgroundVisibility((prev) => {
|
||||
const next = updater(prev);
|
||||
persistBackgroundLayerVisibility(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleLayer = (id: BackgroundLayerId) => {
|
||||
updateBackgroundVisibility((prev) => ({
|
||||
...prev,
|
||||
[id]: !prev[id],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleShowAll = () => {
|
||||
updateBackgroundVisibility(() => ({ ...DEFAULT_BACKGROUND_LAYER_VISIBILITY }));
|
||||
};
|
||||
|
||||
const handleHideAll = () => {
|
||||
updateBackgroundVisibility(() => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY }));
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
style={{
|
||||
@@ -52,7 +83,7 @@ export default function BackgroundLayersPanel({
|
||||
<button
|
||||
key={layer.id}
|
||||
type="button"
|
||||
onClick={() => onToggleLayer(layer.id)}
|
||||
onClick={() => handleToggleLayer(layer.id)}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
@@ -75,44 +106,75 @@ export default function BackgroundLayersPanel({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{geometryVisibility && onToggleGeometryType ? (
|
||||
<>
|
||||
<div style={{ height: 1, background: "#1f2937", margin: "12px 0" }} />
|
||||
<div style={{ margin: 0, marginBottom: 10, fontWeight: 800, fontSize: 13, color: "#e5e7eb" }}>
|
||||
Geometries
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px 12px" }}>
|
||||
{GEO_TYPE_KEYS.map((typeKey) => {
|
||||
const on = geometryVisibility[typeKey] !== false;
|
||||
return (
|
||||
<button
|
||||
key={typeKey}
|
||||
type="button"
|
||||
onClick={() => onToggleGeometryType(typeKey)}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
cursor: "pointer",
|
||||
color: on ? "#22c55e" : "#e5e7eb",
|
||||
textDecorationLine: on ? "none" : "line-through",
|
||||
textDecorationThickness: on ? undefined : "2px",
|
||||
textDecorationColor: on ? undefined : "rgba(148, 163, 184, 0.7)",
|
||||
fontSize: 13,
|
||||
fontWeight: 750,
|
||||
whiteSpace: "nowrap",
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
title={on ? "On" : "Off"}
|
||||
>
|
||||
{typeKey.replaceAll("_", " ")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div style={{ marginTop: 10, display: "flex", gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShowAll}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
Show all
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleHideAll}
|
||||
style={secondaryButtonStyle}
|
||||
>
|
||||
Hide all
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<div style={{ height: 1, background: "#1f2937", margin: "12px 0" }} />
|
||||
<div style={{ margin: 0, marginBottom: 10, fontWeight: 800, fontSize: 13, color: "#e5e7eb" }}>
|
||||
Geometries
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px 12px" }}>
|
||||
{GEO_TYPE_KEYS.map((typeKey) => {
|
||||
const on = geometryVisibility[typeKey] !== false;
|
||||
return (
|
||||
<button
|
||||
key={typeKey}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setGeometryVisibility((prev) => ({
|
||||
...prev,
|
||||
[typeKey]: prev[typeKey] === false,
|
||||
}));
|
||||
}}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
cursor: "pointer",
|
||||
color: on ? "#22c55e" : "#e5e7eb",
|
||||
textDecorationLine: on ? "none" : "line-through",
|
||||
textDecorationThickness: on ? undefined : "2px",
|
||||
textDecorationColor: on ? undefined : "rgba(148, 163, 184, 0.7)",
|
||||
fontSize: 13,
|
||||
fontWeight: 750,
|
||||
whiteSpace: "nowrap",
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
title={on ? "On" : "Off"}
|
||||
>
|
||||
{typeKey.replaceAll("_", " ")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
const secondaryButtonStyle = {
|
||||
border: "1px solid #334155",
|
||||
borderRadius: 6,
|
||||
background: "#0b1220",
|
||||
color: "#cbd5e1",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
padding: "6px 8px",
|
||||
} as const;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import NewBadge from "@/uhm/components/editor/NewBadge";
|
||||
import { useEditorStore } from "@/uhm/store/editorStore";
|
||||
|
||||
type EntityChoice = { id: string; name: string; isNew?: boolean };
|
||||
type WikiChoice = { id: string; title: string; isNew?: boolean };
|
||||
@@ -18,9 +20,6 @@ type BindingRow = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
entities: EntityChoice[];
|
||||
wikis: WikiSnapshot[];
|
||||
links: EntityWikiLinkSnapshot[];
|
||||
setLinks: React.Dispatch<React.SetStateAction<EntityWikiLinkSnapshot[]>>;
|
||||
};
|
||||
|
||||
@@ -29,7 +28,20 @@ function wikiTitle(w: WikiSnapshot): string {
|
||||
return t.length ? t : "Untitled wiki";
|
||||
}
|
||||
|
||||
export default function EntityWikiBindingsPanel({ entities, wikis, links, setLinks }: Props) {
|
||||
export default function EntityWikiBindingsPanel({ setLinks }: Props) {
|
||||
const {
|
||||
entityCatalog,
|
||||
snapshotEntities,
|
||||
wikis,
|
||||
links,
|
||||
} = useEditorStore(
|
||||
useShallow((state) => ({
|
||||
entityCatalog: state.entityCatalog,
|
||||
snapshotEntities: state.snapshotEntities,
|
||||
wikis: state.snapshotWikis,
|
||||
links: state.snapshotEntityWikiLinks,
|
||||
}))
|
||||
);
|
||||
const [activeEntityId, setActiveEntityId] = useState<string>("");
|
||||
const [activeWikiId, setActiveWikiId] = useState<string>("");
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
@@ -46,11 +58,29 @@ export default function EntityWikiBindingsPanel({ entities, wikis, links, setLin
|
||||
[wikis]
|
||||
);
|
||||
|
||||
const entityChoices = useMemo(() => {
|
||||
const cleaned = (entities || []).filter((e) => e && typeof e.id === "string" && e.id.trim().length > 0);
|
||||
cleaned.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return cleaned;
|
||||
}, [entities]);
|
||||
const entityChoices = useMemo<EntityChoice[]>(() => {
|
||||
const visibleSnapshotEntities = new globalThis.Map<string, { id: string; name: string; isNew: boolean }>();
|
||||
for (const ref of snapshotEntities || []) {
|
||||
const id = String(ref?.id || "").trim();
|
||||
if (!id || ref?.operation === "delete" || visibleSnapshotEntities.has(id)) continue;
|
||||
visibleSnapshotEntities.set(id, {
|
||||
id,
|
||||
name: String(ref?.name || id),
|
||||
isNew: ref?.source === "inline" && ref?.operation === "create",
|
||||
});
|
||||
}
|
||||
|
||||
const rows = Array.from(visibleSnapshotEntities.values()).map((entity) => {
|
||||
const found = entityCatalog.find((item) => String(item.id) === entity.id) || null;
|
||||
return {
|
||||
id: entity.id,
|
||||
name: String(found?.name || entity.name || entity.id),
|
||||
isNew: entity.isNew,
|
||||
};
|
||||
});
|
||||
rows.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return rows;
|
||||
}, [entityCatalog, snapshotEntities]);
|
||||
|
||||
const activeLinks = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, type CSSProperties, type KeyboardEvent } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import NewBadge from "@/uhm/components/editor/NewBadge";
|
||||
import { useEditorStore } from "@/uhm/store/editorStore";
|
||||
|
||||
type GeometryChoice = {
|
||||
id: string;
|
||||
@@ -11,13 +13,10 @@ type GeometryChoice = {
|
||||
|
||||
type Props = {
|
||||
geometries: GeometryChoice[];
|
||||
selectedGeometryId: string | null;
|
||||
selectedGeometryId?: string | null;
|
||||
selectedGeometryBindingIds: string[];
|
||||
onToggleBindGeometryForSelectedGeometry?: (geometryId: string, nextChecked: boolean) => void;
|
||||
onFocusGeometry?: (geometryId: string) => void;
|
||||
statusText?: string | null;
|
||||
bindingFilterEnabled: boolean;
|
||||
onBindingFilterEnabledChange: (next: boolean) => void;
|
||||
};
|
||||
|
||||
export default function GeometryBindingPanel({
|
||||
@@ -26,12 +25,29 @@ export default function GeometryBindingPanel({
|
||||
selectedGeometryBindingIds,
|
||||
onToggleBindGeometryForSelectedGeometry,
|
||||
onFocusGeometry,
|
||||
statusText,
|
||||
bindingFilterEnabled,
|
||||
onBindingFilterEnabledChange,
|
||||
}: Props) {
|
||||
const {
|
||||
selectedFeatureIds,
|
||||
statusText,
|
||||
bindingFilterEnabled,
|
||||
setGeometryBindingFilterEnabled,
|
||||
hoveredGeometryId,
|
||||
setHoveredGeometryId,
|
||||
} = useEditorStore(
|
||||
useShallow((state) => ({
|
||||
selectedFeatureIds: state.selectedFeatureIds,
|
||||
statusText: state.geoBindingStatus,
|
||||
bindingFilterEnabled: state.geometryBindingFilterEnabled,
|
||||
setGeometryBindingFilterEnabled: state.setGeometryBindingFilterEnabled,
|
||||
hoveredGeometryId: state.hoveredGeometryId,
|
||||
setHoveredGeometryId: state.setHoveredGeometryId,
|
||||
}))
|
||||
);
|
||||
const effectiveSelectedGeometryId =
|
||||
selectedGeometryId ??
|
||||
(selectedFeatureIds.length > 0 ? String(selectedFeatureIds[0]) : null);
|
||||
const canBindToggle =
|
||||
Boolean(selectedGeometryId) && typeof onToggleBindGeometryForSelectedGeometry === "function";
|
||||
Boolean(effectiveSelectedGeometryId) && typeof onToggleBindGeometryForSelectedGeometry === "function";
|
||||
const canFocusGeometry = typeof onFocusGeometry === "function";
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
@@ -46,24 +62,30 @@ export default function GeometryBindingPanel({
|
||||
|
||||
const bindingSet = useMemo(() => new Set(selectedGeometryBindingIds || []), [selectedGeometryBindingIds]);
|
||||
const selectedGeometry = useMemo(() => {
|
||||
if (!selectedGeometryId) return null;
|
||||
return rows.find((g) => g.id === selectedGeometryId) || null;
|
||||
}, [rows, selectedGeometryId]);
|
||||
if (!effectiveSelectedGeometryId) return null;
|
||||
return rows.find((g) => g.id === effectiveSelectedGeometryId) || null;
|
||||
}, [effectiveSelectedGeometryId, rows]);
|
||||
const visibleRows = useMemo(() => {
|
||||
return rows
|
||||
.filter((g) => g.id !== selectedGeometryId)
|
||||
.filter((g) => g.id !== effectiveSelectedGeometryId)
|
||||
.sort((a, b) => {
|
||||
const aBound = bindingSet.has(a.id);
|
||||
const bBound = bindingSet.has(b.id);
|
||||
if (aBound !== bBound) return aBound ? -1 : 1;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}, [bindingSet, rows, selectedGeometryId]);
|
||||
}, [bindingSet, effectiveSelectedGeometryId, rows]);
|
||||
|
||||
const handleFocusKeyDown = (event: KeyboardEvent<HTMLDivElement>, geometryId: string) => {
|
||||
if (!canFocusGeometry) return;
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
setHoveredGeometryId((current) => (current === geometryId ? null : current));
|
||||
onFocusGeometry?.(geometryId);
|
||||
};
|
||||
|
||||
const handleFocusGeometry = (geometryId: string) => {
|
||||
setHoveredGeometryId((current) => (current === geometryId ? null : current));
|
||||
onFocusGeometry?.(geometryId);
|
||||
};
|
||||
|
||||
@@ -75,6 +97,7 @@ export default function GeometryBindingPanel({
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #1f2937",
|
||||
}}
|
||||
onMouseLeave={() => setHoveredGeometryId(null)}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
|
||||
@@ -92,7 +115,7 @@ export default function GeometryBindingPanel({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={bindingFilterEnabled}
|
||||
onChange={(e) => onBindingFilterEnabledChange(e.target.checked)}
|
||||
onChange={(e) => setGeometryBindingFilterEnabled(e.target.checked)}
|
||||
style={{ width: 14, height: 14 }}
|
||||
/>
|
||||
<span style={{ fontSize: 12, color: "#94a3b8", whiteSpace: "nowrap" }}>Filter</span>
|
||||
@@ -130,15 +153,26 @@ export default function GeometryBindingPanel({
|
||||
marginTop: 10,
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid rgba(59, 130, 246, 0.45)",
|
||||
background: "rgba(37, 99, 235, 0.12)",
|
||||
border:
|
||||
hoveredGeometryId === selectedGeometry.id
|
||||
? "1px solid rgba(245, 158, 11, 0.95)"
|
||||
: "1px solid rgba(59, 130, 246, 0.45)",
|
||||
background:
|
||||
hoveredGeometryId === selectedGeometry.id
|
||||
? "rgba(245, 158, 11, 0.18)"
|
||||
: "rgba(37, 99, 235, 0.12)",
|
||||
cursor: canFocusGeometry ? "pointer" : "default",
|
||||
boxShadow:
|
||||
hoveredGeometryId === selectedGeometry.id
|
||||
? "0 0 0 2px rgba(251, 191, 36, 0.18)"
|
||||
: "none",
|
||||
}}
|
||||
title={selectedGeometry.id}
|
||||
role={canFocusGeometry ? "button" : undefined}
|
||||
tabIndex={canFocusGeometry ? 0 : undefined}
|
||||
onClick={() => onFocusGeometry?.(selectedGeometry.id)}
|
||||
onClick={() => handleFocusGeometry(selectedGeometry.id)}
|
||||
onKeyDown={(event) => handleFocusKeyDown(event, selectedGeometry.id)}
|
||||
onMouseEnter={() => setHoveredGeometryId(selectedGeometry.id)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -187,25 +221,36 @@ export default function GeometryBindingPanel({
|
||||
{visibleRows
|
||||
.map((g) => {
|
||||
const isBound = bindingSet.has(g.id);
|
||||
const isHovered = hoveredGeometryId === g.id;
|
||||
return (
|
||||
<div
|
||||
key={g.id}
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: isBound ? "1px solid rgba(20, 184, 166, 0.65)" : "1px solid #1f2937",
|
||||
background: isBound ? "rgba(20, 184, 166, 0.12)" : "transparent",
|
||||
border: isHovered
|
||||
? "1px solid rgba(245, 158, 11, 0.95)"
|
||||
: isBound
|
||||
? "1px solid rgba(20, 184, 166, 0.65)"
|
||||
: "1px solid #1f2937",
|
||||
background: isHovered
|
||||
? "rgba(245, 158, 11, 0.18)"
|
||||
: isBound
|
||||
? "rgba(20, 184, 166, 0.12)"
|
||||
: "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
cursor: canFocusGeometry ? "pointer" : "default",
|
||||
opacity: canBindToggle ? 1 : 0.75,
|
||||
boxShadow: isHovered ? "0 0 0 2px rgba(251, 191, 36, 0.18)" : "none",
|
||||
}}
|
||||
title={g.id}
|
||||
role={canFocusGeometry ? "button" : undefined}
|
||||
tabIndex={canFocusGeometry ? 0 : undefined}
|
||||
onClick={() => onFocusGeometry?.(g.id)}
|
||||
onClick={() => handleFocusGeometry(g.id)}
|
||||
onKeyDown={(event) => handleFocusKeyDown(event, g.id)}
|
||||
onMouseEnter={() => setHoveredGeometryId(g.id)}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
|
||||
@@ -2,34 +2,40 @@
|
||||
|
||||
import { useMemo, useState, type CSSProperties } from "react";
|
||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||
import type { EntityFormState } from "@/uhm/lib/editor/session/sessionTypes";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import NewBadge from "@/uhm/components/editor/NewBadge";
|
||||
import { useEditorStore } from "@/uhm/store/editorStore";
|
||||
|
||||
type Props = {
|
||||
entityRefs: EntitySnapshot[];
|
||||
entityForm: EntityFormState;
|
||||
onEntityFormChange: (key: keyof EntityFormState, value: string) => void;
|
||||
isEntitySubmitting: boolean;
|
||||
onCreateEntityOnly: () => void;
|
||||
onUpdateEntity?: (entityId: string, payload: { name: string; description: string | null }) => void;
|
||||
entityFormStatus: string | null;
|
||||
selectedGeometryEntityIds?: string[];
|
||||
hasSelectedGeometry?: boolean;
|
||||
onToggleBindEntityForSelectedGeometry?: (entityId: string, nextChecked: boolean) => void;
|
||||
};
|
||||
|
||||
export default function ProjectEntityRefsPanel({
|
||||
entityRefs,
|
||||
entityForm,
|
||||
onEntityFormChange,
|
||||
isEntitySubmitting,
|
||||
onCreateEntityOnly,
|
||||
onUpdateEntity,
|
||||
entityFormStatus,
|
||||
selectedGeometryEntityIds,
|
||||
hasSelectedGeometry,
|
||||
onToggleBindEntityForSelectedGeometry,
|
||||
}: Props) {
|
||||
const {
|
||||
snapshotEntities,
|
||||
entityForm,
|
||||
setEntityForm,
|
||||
isEntitySubmitting,
|
||||
entityFormStatus,
|
||||
selectedGeometryEntityIds,
|
||||
} = useEditorStore(
|
||||
useShallow((state) => ({
|
||||
snapshotEntities: state.snapshotEntities,
|
||||
entityForm: state.entityForm,
|
||||
setEntityForm: state.setEntityForm,
|
||||
isEntitySubmitting: state.isEntitySubmitting,
|
||||
entityFormStatus: state.entityFormStatus,
|
||||
selectedGeometryEntityIds: state.selectedGeometryEntityIds,
|
||||
}))
|
||||
);
|
||||
const canBindToggle =
|
||||
Boolean(hasSelectedGeometry) &&
|
||||
Array.isArray(selectedGeometryEntityIds) &&
|
||||
@@ -43,6 +49,16 @@ export default function ProjectEntityRefsPanel({
|
||||
() => new Set((selectedGeometryEntityIds || []).map(String)),
|
||||
[selectedGeometryEntityIds]
|
||||
);
|
||||
const entityRefs = useMemo(() => {
|
||||
const byId = new globalThis.Map<string, EntitySnapshot>();
|
||||
for (const ref of snapshotEntities || []) {
|
||||
const id = String(ref?.id || "").trim();
|
||||
if (!id || byId.has(id)) continue;
|
||||
if (ref.operation === "delete") continue;
|
||||
byId.set(id, ref);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}, [snapshotEntities]);
|
||||
const sortedEntityRefs = useMemo(() => {
|
||||
const rows = [...(entityRefs || [])];
|
||||
rows.sort((a, b) => {
|
||||
@@ -68,6 +84,9 @@ export default function ProjectEntityRefsPanel({
|
||||
setEditName(typeof entity.name === "string" ? entity.name : "");
|
||||
setEditDescription(entity.description == null ? "" : String(entity.description));
|
||||
};
|
||||
const handleEntityFormChange = (key: "name" | "description", value: string) => {
|
||||
setEntityForm((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -325,14 +344,14 @@ export default function ProjectEntityRefsPanel({
|
||||
<>
|
||||
<input
|
||||
value={entityForm.name}
|
||||
onChange={(event) => onEntityFormChange("name", event.target.value)}
|
||||
onChange={(event) => handleEntityFormChange("name", event.target.value)}
|
||||
placeholder="Tên entity mới"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={entityForm.description}
|
||||
onChange={(event) => onEntityFormChange("description", event.target.value)}
|
||||
onChange={(event) => handleEntityFormChange("description", event.target.value)}
|
||||
placeholder="Description"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { type CSSProperties, useMemo, useState } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { Feature } from "@/uhm/lib/editor/state/useEditorState";
|
||||
import {
|
||||
GEOMETRY_TYPE_OPTIONS,
|
||||
GeometryPreset,
|
||||
GeometryTypeGroupId,
|
||||
GeometryTypeOption,
|
||||
findGeometryTypeOption,
|
||||
groupGeometryTypeOptions,
|
||||
} from "@/uhm/lib/map/geo/geometryTypeOptions";
|
||||
import { normalizeGeoTypeKey } from "@/uhm/lib/map/geo/geoTypeMap";
|
||||
import type { GeometryMetaFormState } from "@/uhm/lib/editor/session/sessionTypes";
|
||||
import { useEditorStore } from "@/uhm/store/editorStore";
|
||||
|
||||
type Props = {
|
||||
selectedFeatures: Feature[];
|
||||
entityTypeOptions: GeometryTypeOption[];
|
||||
geometryMetaForm: GeometryMetaFormState;
|
||||
onGeometryMetaFormChange: (key: keyof GeometryMetaFormState, value: string) => void;
|
||||
isEntitySubmitting: boolean;
|
||||
onApplyGeometryMetadata: () => Promise<{ ok: boolean; error?: string }>;
|
||||
changeCount: number;
|
||||
onReplayEdit?: (id: string | number) => void;
|
||||
@@ -25,14 +22,21 @@ type Props = {
|
||||
|
||||
export default function SelectedGeometryPanel({
|
||||
selectedFeatures,
|
||||
entityTypeOptions,
|
||||
geometryMetaForm,
|
||||
onGeometryMetaFormChange,
|
||||
isEntitySubmitting,
|
||||
onApplyGeometryMetadata,
|
||||
changeCount,
|
||||
onReplayEdit,
|
||||
}: Props) {
|
||||
const {
|
||||
geometryMetaForm,
|
||||
setGeometryMetaForm,
|
||||
isEntitySubmitting,
|
||||
} = useEditorStore(
|
||||
useShallow((state) => ({
|
||||
geometryMetaForm: state.geometryMetaForm,
|
||||
setGeometryMetaForm: state.setGeometryMetaForm,
|
||||
isEntitySubmitting: state.isEntitySubmitting,
|
||||
}))
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [geoApplyFeedback, setGeoApplyFeedback] = useState<
|
||||
| {
|
||||
@@ -73,7 +77,7 @@ export default function SelectedGeometryPanel({
|
||||
if (!selectedFeatures || selectedFeatures.length === 0) return null;
|
||||
const representativeFeature = selectedFeatures[0];
|
||||
|
||||
const groupedGeometryTypeOptions = groupGeometryTypeOptions(entityTypeOptions);
|
||||
const groupedGeometryTypeOptions = groupGeometryTypeOptions(GEOMETRY_TYPE_OPTIONS);
|
||||
const featureGeometryPreset = resolveFeatureGeometryPreset(representativeFeature);
|
||||
const allowedGroupIds = getAllowedGroupIdsForPreset(featureGeometryPreset);
|
||||
const groupedGeoTypeOptions = groupedGeometryTypeOptions.filter((group) =>
|
||||
@@ -143,7 +147,12 @@ export default function SelectedGeometryPanel({
|
||||
</div>
|
||||
<select
|
||||
value={geometryMetaForm.type_key}
|
||||
onChange={(event) => onGeometryMetaFormChange("type_key", event.target.value)}
|
||||
onChange={(event) =>
|
||||
setGeometryMetaForm((prev) => ({
|
||||
...prev,
|
||||
type_key: event.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
>
|
||||
@@ -176,14 +185,24 @@ export default function SelectedGeometryPanel({
|
||||
) : null}
|
||||
<input
|
||||
value={geometryMetaForm.time_start}
|
||||
onChange={(event) => onGeometryMetaFormChange("time_start", event.target.value)}
|
||||
onChange={(event) =>
|
||||
setGeometryMetaForm((prev) => ({
|
||||
...prev,
|
||||
time_start: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="time_start"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={geometryMetaForm.time_end}
|
||||
onChange={(event) => onGeometryMetaFormChange("time_end", event.target.value)}
|
||||
onChange={(event) =>
|
||||
setGeometryMetaForm((prev) => ({
|
||||
...prev,
|
||||
time_end: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="time_end"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
|
||||
Reference in New Issue
Block a user