refactor: enhance selectingEngine with multi-delete support and structured context menu items

This commit is contained in:
taDuc
2026-05-22 16:42:51 +07:00
parent ee468fe4fe
commit 3b4ff71b9a
8 changed files with 2989 additions and 180 deletions
@@ -18,6 +18,8 @@ type Props = {
onApplyGeometryMetadata: () => Promise<{ ok: boolean; error?: string }>;
changeCount: number;
onReplayEdit?: (id: string | number) => void;
onDeleteFeatures?: (ids: (string | number)[]) => void;
onDeselectAll?: () => void;
};
export default function SelectedGeometryPanel({
@@ -25,6 +27,8 @@ export default function SelectedGeometryPanel({
onApplyGeometryMetadata,
changeCount,
onReplayEdit,
onDeleteFeatures,
onDeselectAll,
}: Props) {
const {
geometryMetaForm,
@@ -74,6 +78,13 @@ export default function SelectedGeometryPanel({
const visibleGeoApplyFeedback =
geoApplyFeedback && geoApplyFeedback.signature === geoMetaSignature ? geoApplyFeedback : null;
const isBulkMode = selectedFeatures.length >= 2;
const isMultiEditValid = useMemo(() => {
if (selectedFeatures.length <= 1) return true;
const firstShape = selectedFeatures[0].geometry.type;
return selectedFeatures.every((f) => f.geometry.type === firstShape);
}, [selectedFeatures]);
if (!selectedFeatures || selectedFeatures.length === 0) return null;
const representativeFeature = selectedFeatures[0];
@@ -99,7 +110,7 @@ export default function SelectedGeometryPanel({
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, marginBottom: "8px" }}>
<div style={{ fontWeight: 700, fontSize: "14px" }}>
Geometry property
{isBulkMode ? `Đang chọn ${selectedFeatures.length} Geometries` : "Geometry property"}
</div>
<button
type="button"
@@ -125,7 +136,78 @@ export default function SelectedGeometryPanel({
</div>
{collapsed ? null : (
<div style={{ display: "grid", gap: "8px", fontSize: "13px" }}>
<div style={{ display: "grid", gap: "8px", fontSize: "13px" }}>
{isBulkMode && (
<div
style={{
display: "grid",
gap: "8px",
border: "1px solid #334155",
borderRadius: "8px",
padding: "8px",
background: "#1e293b",
}}
>
<div style={{ color: "#93c5fd", fontWeight: 700, fontSize: "12px" }}>
HÀNH ĐNG NHANH
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "6px" }}>
<button
type="button"
onClick={() => onReplayEdit?.(representativeFeature.properties.id)}
style={{
border: "none",
borderRadius: "6px",
padding: "8px 10px",
cursor: "pointer",
background: "#2563eb",
color: "#ffffff",
fontWeight: 700,
fontSize: "13px",
textAlign: "center",
gridColumn: "span 2",
}}
>
Vào Replay ({selectedFeatures.length} geo)
</button>
<button
type="button"
onClick={() => onDeleteFeatures?.(selectedFeatures.map(f => f.properties.id))}
style={{
border: "none",
borderRadius: "6px",
padding: "7px 10px",
cursor: "pointer",
background: "#dc2626",
color: "#ffffff",
fontWeight: 600,
fontSize: "12px",
textAlign: "center",
}}
>
Xóa ({selectedFeatures.length} geo)
</button>
<button
type="button"
onClick={() => onDeselectAll?.()}
style={{
border: "1px solid #475569",
borderRadius: "6px",
padding: "7px 10px",
cursor: "pointer",
background: "transparent",
color: "#cbd5e1",
fontWeight: 600,
fontSize: "12px",
textAlign: "center",
}}
>
Bỏ chọn tất cả
</button>
</div>
</div>
)}
<div
style={{
display: "grid",
@@ -142,111 +224,113 @@ export default function SelectedGeometryPanel({
<div style={{ color: "#94a3b8", fontSize: "11px" }}>
Các giá trị này thuộc về GEO đang chọn, không phụ thuộc entity.
</div>
<div style={{ color: "#e2e8f0", fontWeight: 600, fontSize: "12px" }}>
Loại GEO
</div>
<select
value={geometryMetaForm.type_key}
onChange={(event) =>
setGeometryMetaForm((prev) => ({
...prev,
type_key: event.target.value,
}))
}
disabled={isEntitySubmitting}
style={entityInputStyle}
>
{!hasCurrentVisibleTypeOption && geometryMetaForm.type_key ? (
<option value={geometryMetaForm.type_key}>
Custom Type ({geometryMetaForm.type_key})
</option>
) : null}
{groupedGeoTypeOptions.map((group) => (
<optgroup
key={group.id}
label={`${group.label} (${group.geometryLabel})`}
{!isMultiEditValid ? (
<div style={{ color: "#fca5a5", fontSize: "12px", padding: "8px", border: "1px solid #7f1d1d", borderRadius: "6px", background: "#450a0a", marginTop: "4px" }}>
Không thể chỉnh sửa thuộc tính cho các geometry không cùng loại hình dạng (Point, Line, Polygon).
</div>
) : (
<>
<div style={{ color: "#e2e8f0", fontWeight: 600, fontSize: "12px" }}>
Loại GEO
</div>
<select
value={geometryMetaForm.type_key}
onChange={(event) =>
setGeometryMetaForm((prev) => ({
...prev,
type_key: event.target.value,
}))
}
disabled={isEntitySubmitting}
style={entityInputStyle}
>
{group.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
{!hasCurrentVisibleTypeOption && geometryMetaForm.type_key ? (
<option value={geometryMetaForm.type_key}>
Custom Type ({geometryMetaForm.type_key})
</option>
) : null}
{groupedGeoTypeOptions.map((group) => (
<optgroup
key={group.id}
label={`${group.label} (${group.geometryLabel})`}
>
{group.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
))}
</optgroup>
))}
</select>
{selectedTypeOption ? (
<div style={{ color: "#cbd5e1", fontSize: "12px" }}>
Đang chọn: <b>{selectedTypeOption.label}</b> ({selectedTypeOption.groupLabel})
</div>
) : geometryMetaForm.type_key ? (
<div style={{ color: "#cbd5e1", fontSize: "12px" }}>
Đang chọn: <b>{geometryMetaForm.type_key}</b>
</div>
) : null}
<input
value={geometryMetaForm.time_start}
onChange={(event) =>
setGeometryMetaForm((prev) => ({
...prev,
time_start: event.target.value,
}))
}
placeholder="time_start"
disabled={isEntitySubmitting}
style={entityInputStyle}
/>
<input
value={geometryMetaForm.time_end}
onChange={(event) =>
setGeometryMetaForm((prev) => ({
...prev,
time_end: event.target.value,
}))
}
placeholder="time_end"
disabled={isEntitySubmitting}
style={entityInputStyle}
/>
{/*<input*/}
{/* value={geometryMetaForm.binding}*/}
{/* onChange={(event) => onGeometryMetaFormChange("binding", event.target.value)}*/}
{/* placeholder="binding (geometry ids, comma separated)"*/}
{/* disabled={isEntitySubmitting}*/}
{/* style={entityInputStyle}*/}
{/*/>*/}
<button
type="button"
onClick={handleApplyGeoMeta}
disabled={isEntitySubmitting}
style={primaryGeometryButtonStyle}
>
Apply
</button>
{onReplayEdit && selectedFeatures.length > 0 && (
<button
type="button"
onClick={() => onReplayEdit(selectedFeatures[0].properties.id)}
style={{
...primaryGeometryButtonStyle,
background: "#1e293b",
border: "1px solid #334155",
color: "#38bdf8",
}}
>
Replay Edit
</button>
</select>
{selectedTypeOption ? (
<div style={{ color: "#cbd5e1", fontSize: "12px" }}>
Đang chọn: <b>{selectedTypeOption.label}</b> ({selectedTypeOption.groupLabel})
</div>
) : geometryMetaForm.type_key ? (
<div style={{ color: "#cbd5e1", fontSize: "12px" }}>
Đang chọn: <b>{geometryMetaForm.type_key}</b>
</div>
) : null}
<input
value={geometryMetaForm.time_start}
onChange={(event) =>
setGeometryMetaForm((prev) => ({
...prev,
time_start: event.target.value,
}))
}
placeholder="time_start"
disabled={isEntitySubmitting}
style={entityInputStyle}
/>
<input
value={geometryMetaForm.time_end}
onChange={(event) =>
setGeometryMetaForm((prev) => ({
...prev,
time_end: event.target.value,
}))
}
placeholder="time_end"
disabled={isEntitySubmitting}
style={entityInputStyle}
/>
<button
type="button"
onClick={handleApplyGeoMeta}
disabled={isEntitySubmitting}
style={primaryGeometryButtonStyle}
>
{isBulkMode ? `Apply cho ${selectedFeatures.length} geo` : "Apply"}
</button>
{onReplayEdit && !isBulkMode && selectedFeatures.length > 0 && (
<button
type="button"
onClick={() => onReplayEdit(selectedFeatures[0].properties.id)}
style={{
...primaryGeometryButtonStyle,
background: "#1e293b",
border: "1px solid #334155",
color: "#38bdf8",
}}
>
Replay Edit
</button>
)}
{visibleGeoApplyFeedback ? (
<div
style={{
fontSize: "12px",
color:
visibleGeoApplyFeedback.kind === "ok" ? "#22c55e" : "#fca5a5",
}}
>
{visibleGeoApplyFeedback.text}
</div>
) : null}
</>
)}
{visibleGeoApplyFeedback ? (
<div
style={{
fontSize: "12px",
color:
visibleGeoApplyFeedback.kind === "ok" ? "#22c55e" : "#fca5a5",
}}
>
{visibleGeoApplyFeedback.text}
</div>
) : null}
</div>
{changeCount > 0 ? (
@@ -254,7 +338,7 @@ export default function SelectedGeometryPanel({
Thay đi sẽ vào lịch sử khi Commit.
</div>
) : null}
</div>
</div>
)}
</div>
);