editor panel improve experience
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, type KeyboardEvent } from "react";
|
||||
import { useMemo, useState, type CSSProperties, type KeyboardEvent } from "react";
|
||||
import NewBadge from "@/uhm/components/editor/NewBadge";
|
||||
|
||||
type GeometryChoice = {
|
||||
@@ -49,6 +49,16 @@ export default function GeometryBindingPanel({
|
||||
if (!selectedGeometryId) return null;
|
||||
return rows.find((g) => g.id === selectedGeometryId) || null;
|
||||
}, [rows, selectedGeometryId]);
|
||||
const visibleRows = useMemo(() => {
|
||||
return rows
|
||||
.filter((g) => g.id !== selectedGeometryId)
|
||||
.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]);
|
||||
|
||||
const handleFocusKeyDown = (event: KeyboardEvent<HTMLDivElement>, geometryId: string) => {
|
||||
if (!canFocusGeometry) return;
|
||||
@@ -174,8 +184,7 @@ export default function GeometryBindingPanel({
|
||||
|
||||
{collapsed ? null : rows.length ? (
|
||||
<div style={{ marginTop: "10px", display: "grid", gap: "6px", maxHeight: 250, overflowY: "auto", paddingRight: 4 }}>
|
||||
{rows
|
||||
.filter((g) => g.id !== selectedGeometryId)
|
||||
{visibleRows
|
||||
.map((g) => {
|
||||
const isBound = bindingSet.has(g.id);
|
||||
return (
|
||||
@@ -184,8 +193,8 @@ export default function GeometryBindingPanel({
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid #1f2937",
|
||||
background: "transparent",
|
||||
border: isBound ? "1px solid rgba(20, 184, 166, 0.65)" : "1px solid #1f2937",
|
||||
background: isBound ? "rgba(20, 184, 166, 0.12)" : "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
@@ -219,6 +228,7 @@ export default function GeometryBindingPanel({
|
||||
>
|
||||
{g.label || g.id}
|
||||
</span>
|
||||
{isBound ? <span style={boundBadgeStyle}>bound</span> : null}
|
||||
{g.isNew ? <NewBadge /> : null}
|
||||
</div>
|
||||
<div
|
||||
@@ -283,6 +293,24 @@ export default function GeometryBindingPanel({
|
||||
);
|
||||
}
|
||||
|
||||
const boundBadgeStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "0 0 auto",
|
||||
height: 17,
|
||||
padding: "0 6px",
|
||||
borderRadius: 999,
|
||||
border: "1px solid rgba(45, 212, 191, 0.5)",
|
||||
background: "rgba(20, 184, 166, 0.18)",
|
||||
color: "#99f6e4",
|
||||
fontSize: 10,
|
||||
fontWeight: 900,
|
||||
lineHeight: 1,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0,
|
||||
};
|
||||
|
||||
function LockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
|
||||
@@ -39,6 +39,22 @@ export default function ProjectEntityRefsPanel({
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [activeEntityId, setActiveEntityId] = useState<string | null>(null);
|
||||
const selectedEntityIdSet = useMemo(
|
||||
() => new Set((selectedGeometryEntityIds || []).map(String)),
|
||||
[selectedGeometryEntityIds]
|
||||
);
|
||||
const sortedEntityRefs = useMemo(() => {
|
||||
const rows = [...(entityRefs || [])];
|
||||
rows.sort((a, b) => {
|
||||
const aBound = selectedEntityIdSet.has(String(a.id));
|
||||
const bBound = selectedEntityIdSet.has(String(b.id));
|
||||
if (aBound !== bBound) return aBound ? -1 : 1;
|
||||
const aLabel = String(a.name || a.id || "");
|
||||
const bLabel = String(b.name || b.id || "");
|
||||
return aLabel.localeCompare(bLabel);
|
||||
});
|
||||
return rows;
|
||||
}, [entityRefs, selectedEntityIdSet]);
|
||||
|
||||
const activeEntity = useMemo(
|
||||
() => (activeEntityId ? entityRefs.find((e) => String(e.id) === String(activeEntityId)) || null : null),
|
||||
@@ -90,83 +106,93 @@ export default function ProjectEntityRefsPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{collapsed ? null : entityRefs.length ? (
|
||||
{collapsed ? null : sortedEntityRefs.length ? (
|
||||
<div style={{ marginTop: "10px", display: "grid", gap: "6px", maxHeight: 250, overflowY: "auto", paddingRight: 4 }}>
|
||||
{entityRefs.map((e) => (
|
||||
<div
|
||||
key={e.id}
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: activeEntityId === String(e.id) ? "1px solid #2563eb" : "1px solid #1f2937",
|
||||
background: "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEntityEditor(e)}
|
||||
title="Chon de sua"
|
||||
{sortedEntityRefs.map((e) => {
|
||||
const entityId = String(e.id);
|
||||
const isBoundToSelectedGeometry = selectedEntityIdSet.has(entityId);
|
||||
const isActive = activeEntityId === entityId;
|
||||
return (
|
||||
<div
|
||||
key={e.id}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "left",
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
cursor: canEditEntity ? "pointer" : "default",
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: isActive
|
||||
? "1px solid #2563eb"
|
||||
: isBoundToSelectedGeometry
|
||||
? "1px solid rgba(20, 184, 166, 0.65)"
|
||||
: "1px solid #1f2937",
|
||||
background: isBoundToSelectedGeometry ? "rgba(20, 184, 166, 0.12)" : "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
disabled={!canEditEntity}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6, minWidth: 0 }}>
|
||||
<span style={{ fontSize: "12px", color: "#e5e7eb", fontWeight: 700, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{e.name || e.id}
|
||||
</span>
|
||||
{isNewEntityRef(e) ? <NewBadge /> : null}
|
||||
</div>
|
||||
<div style={{ fontSize: "11px", color: "#94a3b8", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{e.id}
|
||||
</div>
|
||||
</button>
|
||||
{canBindToggle ? (
|
||||
<button
|
||||
type="button"
|
||||
title={selectedGeometryEntityIds!.includes(String(e.id)) ? "Unbind from selected geometry" : "Bind to selected geometry"}
|
||||
onClick={() =>
|
||||
onToggleBindEntityForSelectedGeometry!(
|
||||
String(e.id),
|
||||
!selectedGeometryEntityIds!.includes(String(e.id))
|
||||
)
|
||||
}
|
||||
onClick={() => openEntityEditor(e)}
|
||||
title="Chon de sua"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 6,
|
||||
border: "1px solid #334155",
|
||||
background: "#0b1220",
|
||||
cursor: "pointer",
|
||||
flex: "0 0 auto",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "left",
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
cursor: canEditEntity ? "pointer" : "default",
|
||||
}}
|
||||
aria-label={
|
||||
selectedGeometryEntityIds!.includes(String(e.id))
|
||||
? `Unbind entity ${String(e.id)} from selected geometry`
|
||||
: `Bind entity ${String(e.id)} to selected geometry`
|
||||
}
|
||||
disabled={!canEditEntity}
|
||||
>
|
||||
{selectedGeometryEntityIds!.includes(String(e.id)) ? (
|
||||
<UnlockIcon />
|
||||
) : (
|
||||
<LockIcon />
|
||||
)}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6, minWidth: 0 }}>
|
||||
<span style={{ fontSize: "12px", color: "#e5e7eb", fontWeight: 700, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{e.name || e.id}
|
||||
</span>
|
||||
{isBoundToSelectedGeometry ? <span style={boundBadgeStyle}>bound</span> : null}
|
||||
{isNewEntityRef(e) ? <NewBadge /> : null}
|
||||
</div>
|
||||
<div style={{ fontSize: "11px", color: "#94a3b8", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{e.id}
|
||||
</div>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{canBindToggle ? (
|
||||
<button
|
||||
type="button"
|
||||
title={isBoundToSelectedGeometry ? "Unbind from selected geometry" : "Bind to selected geometry"}
|
||||
onClick={() =>
|
||||
onToggleBindEntityForSelectedGeometry!(
|
||||
entityId,
|
||||
!isBoundToSelectedGeometry
|
||||
)
|
||||
}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 6,
|
||||
border: "1px solid #334155",
|
||||
background: "#0b1220",
|
||||
cursor: "pointer",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
aria-label={
|
||||
isBoundToSelectedGeometry
|
||||
? `Unbind entity ${entityId} from selected geometry`
|
||||
: `Bind entity ${entityId} to selected geometry`
|
||||
}
|
||||
>
|
||||
{isBoundToSelectedGeometry ? (
|
||||
<UnlockIcon />
|
||||
) : (
|
||||
<LockIcon />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
</div>
|
||||
) : (
|
||||
@@ -358,6 +384,24 @@ const entityInputStyle: CSSProperties = {
|
||||
fontSize: "13px",
|
||||
};
|
||||
|
||||
const boundBadgeStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "0 0 auto",
|
||||
height: 17,
|
||||
padding: "0 6px",
|
||||
borderRadius: 999,
|
||||
border: "1px solid rgba(45, 212, 191, 0.5)",
|
||||
background: "rgba(20, 184, 166, 0.18)",
|
||||
color: "#99f6e4",
|
||||
fontSize: 10,
|
||||
fontWeight: 900,
|
||||
lineHeight: 1,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0,
|
||||
};
|
||||
|
||||
function LockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type CSSProperties, useMemo, useState } from "react";
|
||||
import { Entity } from "@/uhm/api/entities";
|
||||
import { Feature } from "@/uhm/lib/editor/state/useEditorState";
|
||||
import {
|
||||
GeometryPreset,
|
||||
@@ -14,11 +13,6 @@ import type { GeometryMetaFormState } from "@/uhm/lib/editor/session/sessionType
|
||||
|
||||
type Props = {
|
||||
selectedFeatures: Feature[];
|
||||
selectedFeatureEntitySummary: string;
|
||||
selectedFeatureBindingSummary: string;
|
||||
entities: Entity[];
|
||||
selectedGeometryEntityIds: string[];
|
||||
onEntityIdsChange: (values: string[]) => void;
|
||||
entityTypeOptions: GeometryTypeOption[];
|
||||
geometryMetaForm: GeometryMetaFormState;
|
||||
onGeometryMetaFormChange: (key: keyof GeometryMetaFormState, value: string) => void;
|
||||
@@ -29,11 +23,6 @@ type Props = {
|
||||
|
||||
export default function SelectedGeometryPanel({
|
||||
selectedFeatures,
|
||||
selectedFeatureEntitySummary,
|
||||
selectedFeatureBindingSummary,
|
||||
entities,
|
||||
selectedGeometryEntityIds,
|
||||
onEntityIdsChange,
|
||||
entityTypeOptions,
|
||||
geometryMetaForm,
|
||||
onGeometryMetaFormChange,
|
||||
@@ -103,7 +92,7 @@ export default function SelectedGeometryPanel({
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: 700, fontSize: "14px" }}>
|
||||
Entity & Geometry
|
||||
Geometry property
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -130,67 +119,6 @@ export default function SelectedGeometryPanel({
|
||||
|
||||
{collapsed ? null : (
|
||||
<div style={{ display: "grid", gap: "8px", fontSize: "13px" }}>
|
||||
<div style={{ color: "#e2e8f0" }}>
|
||||
ID: {selectedFeatures.map(f => String(f.properties.id)).join(", ")}
|
||||
</div>
|
||||
<div style={{ color: "#cbd5e1" }}>
|
||||
Entities hiện tại: {selectedFeatureEntitySummary}
|
||||
</div>
|
||||
<div style={{ color: "#cbd5e1" }}>
|
||||
Binding hiện tại: {selectedFeatureBindingSummary}
|
||||
</div>
|
||||
<div style={{ color: "#cbd5e1" }}>
|
||||
Geometry preset: {formatGeometryPresetLabel(featureGeometryPreset)}
|
||||
</div>
|
||||
|
||||
<div style={{ color: "#94a3b8", fontSize: "12px" }}>
|
||||
Entities đã chọn:
|
||||
</div>
|
||||
{selectedGeometryEntityIds.length ? (
|
||||
<div style={{ display: "grid", gap: "6px" }}>
|
||||
{selectedGeometryEntityIds.map((entityId) => {
|
||||
const entity = entities.find((item) => item.id === entityId) || null;
|
||||
const label = entity?.name
|
||||
? `${entity.name} (${entityId})`
|
||||
: entityId;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={entityId}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "8px",
|
||||
background: "#111827",
|
||||
border: "1px solid #334155",
|
||||
borderRadius: "6px",
|
||||
padding: "6px 8px",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#e2e8f0" }}>{label}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onEntityIdsChange(
|
||||
selectedGeometryEntityIds.filter((id) => id !== entityId)
|
||||
)
|
||||
}
|
||||
disabled={isEntitySubmitting}
|
||||
style={removeButtonStyle}
|
||||
>
|
||||
Bỏ
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: "#fca5a5", fontSize: "12px" }}>
|
||||
Chưa có entity nào được gắn.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
@@ -306,16 +234,6 @@ const entityInputStyle: CSSProperties = {
|
||||
fontSize: "13px",
|
||||
};
|
||||
|
||||
const removeButtonStyle: CSSProperties = {
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "4px 8px",
|
||||
cursor: "pointer",
|
||||
background: "#7f1d1d",
|
||||
color: "#ffffff",
|
||||
fontSize: "12px",
|
||||
};
|
||||
|
||||
const primaryGeometryButtonStyle: CSSProperties = {
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
@@ -404,11 +322,3 @@ function getAllowedGroupIdsForPreset(
|
||||
|
||||
return ["polygon"];
|
||||
}
|
||||
|
||||
function formatGeometryPresetLabel(preset: GeometryPreset | null): string {
|
||||
if (preset === "point") return "point - Điểm";
|
||||
if (preset === "line") return "line - Tuyến";
|
||||
if (preset === "circle-area") return "circle - Tròn";
|
||||
if (preset === "polygon") return "polygon - Đa giác";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user