refactor(important): reduce editor drill state

This commit is contained in:
taDuc
2026-05-16 01:45:19 +07:00
parent 4c81862bb4
commit a8097c95d4
12 changed files with 989 additions and 467 deletions
@@ -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}
+5 -3
View File
@@ -7,7 +7,7 @@ import { initLine } from "@/uhm/lib/map/engines/lineEngine";
import { initPath } from "@/uhm/lib/map/engines/pathEngine";
import { initCircle } from "@/uhm/lib/map/engines/circleEngine";
import { createEditingEngine } from "@/uhm/lib/map/engines/editingEngine";
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
import { FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
import { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
import { buildClientFeatureId, getSelectableLayers } from "./mapUtils";
import { MapHoverPayload } from "../Map";
@@ -15,7 +15,7 @@ import { MapHoverPayload } from "../Map";
type EngineBinding = {
cleanup: () => void;
cancel?: () => void;
clearSelection?: () => void;
clearSelection?: (skipNotify?: boolean) => void;
};
type UseMapInteractionProps = {
@@ -143,7 +143,9 @@ export function useMapInteraction({
const originalFeature = draftRef.current.features.find(
(item) => String(item.properties.id) === String(rawId)
);
editingEngineRef.current?.beginEditing((originalFeature || feature) as any);
editingEngineRef.current?.beginEditing(
(originalFeature || feature) as unknown as maplibregl.MapGeoJSONFeature
);
}
: undefined,
(ids) => onSelectFeatureIdsRef.current?.(ids),
+25 -17
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ComponentProps } from "react";
import dynamic from "next/dynamic";
import "react-quill-new/dist/quill.snow.css";
import { useShallow } from "zustand/react/shallow";
import { Modal } from "@/components/ui/modal";
import Button from "@/components/ui/button/Button";
@@ -13,6 +14,7 @@ import { newId } from "@/uhm/lib/utils/id";
import type ReactQuill from "react-quill-new";
import { checkWikiSlugExists, fetchWikiBySlug, searchWikisByTitle, type Wiki } from "@/uhm/api/wikis";
import NewBadge from "@/uhm/components/editor/NewBadge";
import { useEditorStore } from "@/uhm/store/editorStore";
type ReactQuillProps = ComponentProps<typeof ReactQuill>;
type QuillRange = { index: number; length: number };
@@ -39,7 +41,7 @@ type QuillLinkFormat = {
type QuillImageFormatCtor = {
new (): {
domNode: Element;
format: (name: string, value: string) => void;
format(name: string, value: string): void;
};
formats: (domNode: Element) => Record<string, string>;
};
@@ -53,9 +55,7 @@ let quillLinkSanitizePatched = false;
type Props = {
projectId: string;
wikis: WikiSnapshot[];
setWikis: React.Dispatch<React.SetStateAction<WikiSnapshot[]>>;
requestedActiveId?: string | null;
};
function clampTitle(title: string) {
@@ -63,7 +63,13 @@ function clampTitle(title: string) {
return t.length ? t.slice(0, 120) : "Untitled wiki";
}
export default function WikiSidebarPanel({ projectId, wikis, setWikis, requestedActiveId }: Props) {
export default function WikiSidebarPanel({ projectId, setWikis }: Props) {
const { wikis, requestedActiveId } = useEditorStore(
useShallow((state) => ({
wikis: state.snapshotWikis,
requestedActiveId: state.requestedActiveWikiId,
}))
);
const [open, setOpen] = useState(false);
const [activeId, setActiveId] = useState<string | null>(null);
const [collapsed, setCollapsed] = useState(false);
@@ -122,13 +128,18 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, requested
const ImageFormat = Quill.import?.("formats/image") as QuillImageFormatCtor | undefined;
if (ImageFormat) {
class CustomImage extends ImageFormat {
const BaseImageFormat = ImageFormat;
class CustomImage extends BaseImageFormat {
static formats(domNode: Element) {
const formats = ImageFormat.formats(domNode) || {};
if (domNode.hasAttribute("style")) formats.style = domNode.getAttribute("style");
if (domNode.hasAttribute("width")) formats.width = domNode.getAttribute("width");
if (domNode.hasAttribute("height")) formats.height = domNode.getAttribute("height");
if (domNode.hasAttribute("class")) formats.class = domNode.getAttribute("class");
const formats = BaseImageFormat.formats(domNode) || {};
const style = domNode.getAttribute("style");
const width = domNode.getAttribute("width");
const height = domNode.getAttribute("height");
const className = domNode.getAttribute("class");
if (style) formats.style = style;
if (width) formats.width = width;
if (height) formats.height = height;
if (className) formats.class = className;
return formats;
}
@@ -291,13 +302,10 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, requested
if (!activeId) return;
const fmt = detectWikiDocStorageFormat(wikiDocHtml);
const label = fmt === "json" ? "json" : fmt === "text" ? "txt" : "html";
const mime =
fmt === "json"
? "application/json;charset=utf-8"
: fmt === "text"
? "text/plain;charset=utf-8"
: "text/html;charset=utf-8";
const label = fmt === "text" ? "txt" : "html";
const mime = fmt === "text"
? "text/plain;charset=utf-8"
: "text/html;charset=utf-8";
const base =
normalizeWikiSlugInput(wikiSlug) ||