refactor state storge, UI editor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type CSSProperties } from "react";
|
||||
import { type CSSProperties, useMemo, useState } from "react";
|
||||
import { Entity } from "@/uhm/api/entities";
|
||||
import { Feature } from "@/uhm/lib/useEditorState";
|
||||
import {
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
findEntityTypeOption,
|
||||
groupEntityTypeOptions,
|
||||
} from "@/uhm/lib/entityTypeOptions";
|
||||
import type { EntityFormState, GeometryMetaFormState } from "@/uhm/lib/editor/session/sessionTypes";
|
||||
import type { GeometryMetaFormState } from "@/uhm/lib/editor/session/sessionTypes";
|
||||
|
||||
type Props = {
|
||||
selectedFeature: Feature | null;
|
||||
@@ -19,24 +19,12 @@ type Props = {
|
||||
entities: Entity[];
|
||||
selectedGeometryEntityIds: string[];
|
||||
onEntityIdsChange: (values: string[]) => void;
|
||||
entitySearchQuery: string;
|
||||
onEntitySearchQueryChange: (value: string) => void;
|
||||
entitySearchResults: Entity[];
|
||||
selectedSearchEntityId: string | null;
|
||||
onSelectSearchEntityId: (value: string | null) => void;
|
||||
onAddSelectedSearchEntity: () => void;
|
||||
isEntitySearchLoading: boolean;
|
||||
entityForm: EntityFormState;
|
||||
onEntityFormChange: (key: keyof EntityFormState, value: string) => void;
|
||||
entityTypeOptions: EntityTypeOption[];
|
||||
geometryMetaForm: GeometryMetaFormState;
|
||||
onGeometryMetaFormChange: (key: keyof GeometryMetaFormState, value: string) => void;
|
||||
isEntitySubmitting: boolean;
|
||||
onCreateEntityOnly: () => void;
|
||||
onApplyGeometryMetadata: () => void;
|
||||
onApplyEntitiesForSelectedGeometry: () => void;
|
||||
onApplyGeometryMetadata: () => Promise<{ ok: boolean; error?: string }>;
|
||||
changeCount: number;
|
||||
entityFormStatus: string | null;
|
||||
};
|
||||
|
||||
export default function SelectedGeometryPanel({
|
||||
@@ -46,41 +34,60 @@ export default function SelectedGeometryPanel({
|
||||
entities,
|
||||
selectedGeometryEntityIds,
|
||||
onEntityIdsChange,
|
||||
entitySearchQuery,
|
||||
onEntitySearchQueryChange,
|
||||
entitySearchResults,
|
||||
selectedSearchEntityId,
|
||||
onSelectSearchEntityId,
|
||||
onAddSelectedSearchEntity,
|
||||
isEntitySearchLoading,
|
||||
entityForm,
|
||||
onEntityFormChange,
|
||||
entityTypeOptions,
|
||||
geometryMetaForm,
|
||||
onGeometryMetaFormChange,
|
||||
isEntitySubmitting,
|
||||
onCreateEntityOnly,
|
||||
onApplyGeometryMetadata,
|
||||
onApplyEntitiesForSelectedGeometry,
|
||||
changeCount,
|
||||
entityFormStatus,
|
||||
}: Props) {
|
||||
const [geoApplyFeedback, setGeoApplyFeedback] = useState<
|
||||
| {
|
||||
kind: "ok" | "error";
|
||||
text: string;
|
||||
signature: string;
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
|
||||
const geoMetaSignature = useMemo(() => {
|
||||
return [
|
||||
geometryMetaForm.type_key,
|
||||
geometryMetaForm.time_start,
|
||||
geometryMetaForm.time_end,
|
||||
geometryMetaForm.binding,
|
||||
].join("|");
|
||||
}, [
|
||||
geometryMetaForm.binding,
|
||||
geometryMetaForm.time_end,
|
||||
geometryMetaForm.time_start,
|
||||
geometryMetaForm.type_key,
|
||||
]);
|
||||
|
||||
const handleApplyGeoMeta = async () => {
|
||||
setGeoApplyFeedback(null);
|
||||
const result = await onApplyGeometryMetadata();
|
||||
if (result.ok) {
|
||||
setGeoApplyFeedback({ kind: "ok", text: "đã apply thành công", signature: geoMetaSignature });
|
||||
} else if (result.error) {
|
||||
setGeoApplyFeedback({ kind: "error", text: result.error, signature: geoMetaSignature });
|
||||
}
|
||||
};
|
||||
|
||||
const visibleGeoApplyFeedback =
|
||||
geoApplyFeedback && geoApplyFeedback.signature === geoMetaSignature ? geoApplyFeedback : null;
|
||||
|
||||
if (!selectedFeature) return null;
|
||||
|
||||
const groupedEntityTypeOptions = groupEntityTypeOptions(entityTypeOptions);
|
||||
const featureGeometryPreset = selectedFeature
|
||||
? resolveFeatureGeometryPreset(selectedFeature)
|
||||
: null;
|
||||
const allowedGroupIds = featureGeometryPreset
|
||||
? getAllowedGroupIdsForPreset(featureGeometryPreset)
|
||||
: [];
|
||||
const visibleGroupedEntityTypeOptions = groupedEntityTypeOptions.filter((group) =>
|
||||
const featureGeometryPreset = resolveFeatureGeometryPreset(selectedFeature);
|
||||
const allowedGroupIds = getAllowedGroupIdsForPreset(featureGeometryPreset);
|
||||
const groupedGeoTypeOptions = groupedEntityTypeOptions.filter((group) =>
|
||||
allowedGroupIds.includes(group.id)
|
||||
);
|
||||
const groupedEntityTypeOptionsForCreate = selectedFeature
|
||||
? visibleGroupedEntityTypeOptions
|
||||
: groupedEntityTypeOptions;
|
||||
const selectedTypeOption = findEntityTypeOption(entityForm.type_id);
|
||||
const hasCurrentVisibleTypeOption = groupedEntityTypeOptionsForCreate.some((group) =>
|
||||
group.options.some((option) => option.value === entityForm.type_id)
|
||||
const selectedTypeOption = findEntityTypeOption(geometryMetaForm.type_key);
|
||||
const hasCurrentVisibleTypeOption = groupedGeoTypeOptions.some((group) =>
|
||||
group.options.some((option) => option.value === geometryMetaForm.type_key)
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -96,72 +103,67 @@ export default function SelectedGeometryPanel({
|
||||
Entity & Geometry
|
||||
</div>
|
||||
|
||||
{!selectedFeature ? (
|
||||
<div style={{ color: "#94a3b8", fontSize: "13px" }}>
|
||||
Chưa chọn geometry. Tạo entity mới ở khối bên dưới, hoặc vào mode Select để bind entity cho geometry.
|
||||
<div style={{ display: "grid", gap: "8px", fontSize: "13px" }}>
|
||||
<div style={{ color: "#e2e8f0" }}>
|
||||
ID: {String(selectedFeature.properties.id)}
|
||||
</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={{ display: "grid", gap: "8px", fontSize: "13px" }}>
|
||||
<div style={{ color: "#e2e8f0" }}>
|
||||
ID: {String(selectedFeature.properties.id)}
|
||||
</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;
|
||||
<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",
|
||||
}}
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
Bỏ
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: "#fca5a5", fontSize: "12px" }}>
|
||||
Chưa có entity nào được gắn.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
@@ -179,6 +181,42 @@ 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) => onGeometryMetaFormChange("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})`}
|
||||
>
|
||||
{group.options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</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) => onGeometryMetaFormChange("time_start", event.target.value)}
|
||||
@@ -195,81 +233,23 @@ export default function SelectedGeometryPanel({
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onApplyGeometryMetadata}
|
||||
onClick={handleApplyGeoMeta}
|
||||
disabled={isEntitySubmitting}
|
||||
style={primaryGeometryButtonStyle}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "8px",
|
||||
border: "1px solid #1f3b5a",
|
||||
borderRadius: "8px",
|
||||
padding: "8px",
|
||||
background: "#0f172a",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "#bfdbfe", fontWeight: 700, fontSize: "12px" }}>
|
||||
Bind entity có sẵn
|
||||
</div>
|
||||
<div style={{ color: "#93c5fd", fontSize: "11px" }}>
|
||||
Dùng khi entity đã tồn tại. Tìm kiếm, thêm vào danh sách rồi bấm nút áp dụng.
|
||||
</div>
|
||||
<input
|
||||
value={entitySearchQuery}
|
||||
onChange={(event) => onEntitySearchQueryChange(event.target.value)}
|
||||
placeholder="Search entity theo name..."
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<select
|
||||
value={selectedSearchEntityId || ""}
|
||||
onChange={(event) =>
|
||||
onSelectSearchEntityId(event.target.value ? event.target.value : null)
|
||||
}
|
||||
disabled={isEntitySubmitting || isEntitySearchLoading}
|
||||
style={entityInputStyle}
|
||||
>
|
||||
<option value="">-- Chọn entity từ kết quả search --</option>
|
||||
{entitySearchResults.map((entity) => (
|
||||
<option key={entity.id} value={entity.id}>
|
||||
{entity.name} ({entity.id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAddSelectedSearchEntity}
|
||||
disabled={isEntitySubmitting || isEntitySearchLoading}
|
||||
style={secondaryActionButtonStyle}
|
||||
>
|
||||
Thêm entity đã chọn vào danh sách gắn
|
||||
</button>
|
||||
{isEntitySearchLoading ? (
|
||||
<div style={{ color: "#93c5fd", fontSize: "12px" }}>
|
||||
Đang tìm entity...
|
||||
{visibleGeoApplyFeedback ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color:
|
||||
visibleGeoApplyFeedback.kind === "ok" ? "#22c55e" : "#fca5a5",
|
||||
}}
|
||||
>
|
||||
{visibleGeoApplyFeedback.text}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={onApplyEntitiesForSelectedGeometry}
|
||||
disabled={isEntitySubmitting}
|
||||
style={{
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "7px 8px",
|
||||
cursor: isEntitySubmitting ? "not-allowed" : "pointer",
|
||||
background: "#0f766e",
|
||||
color: "#ffffff",
|
||||
opacity: isEntitySubmitting ? 0.7 : 1,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Áp dụng danh sách entity
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{changeCount > 0 ? (
|
||||
@@ -277,107 +257,7 @@ export default function SelectedGeometryPanel({
|
||||
Thay đổi sẽ vào lịch sử khi Commit.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "8px",
|
||||
border: "1px solid #1e3a8a",
|
||||
borderRadius: "8px",
|
||||
padding: "8px",
|
||||
background: "#0f172a",
|
||||
marginTop: "10px",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "#bfdbfe", fontWeight: 700, fontSize: "12px" }}>
|
||||
Tạo entity mới (độc lập)
|
||||
</div>
|
||||
<div style={{ color: "#93c5fd", fontSize: "11px" }}>
|
||||
Chỉ tạo entity, không tự bind vào geometry.
|
||||
</div>
|
||||
{selectedFeature ? (
|
||||
<div style={{ color: "#93c5fd", fontSize: "11px" }}>
|
||||
Type đang bị giới hạn theo geometry: <b>{formatGeometryPresetLabel(featureGeometryPreset)}</b>.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
value={entityForm.name}
|
||||
onChange={(event) => onEntityFormChange("name", event.target.value)}
|
||||
placeholder="Tên entity mới"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={entityForm.slug}
|
||||
onChange={(event) => onEntityFormChange("slug", event.target.value)}
|
||||
placeholder="Slug"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<div style={{ color: "#e2e8f0", fontWeight: 600, fontSize: "12px" }}>
|
||||
Chọn loại entity
|
||||
</div>
|
||||
<select
|
||||
value={entityForm.type_id}
|
||||
onChange={(event) => onEntityFormChange("type_id", event.target.value)}
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
>
|
||||
{!selectedFeature && !hasCurrentVisibleTypeOption && entityForm.type_id ? (
|
||||
<option value={entityForm.type_id}>
|
||||
Custom Type ({entityForm.type_id})
|
||||
</option>
|
||||
) : null}
|
||||
{groupedEntityTypeOptionsForCreate.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>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{selectedTypeOption ? (
|
||||
<div style={{ color: "#cbd5e1", fontSize: "12px" }}>
|
||||
Type đang chọn: <b>{selectedTypeOption.label}</b> ({selectedTypeOption.groupLabel})
|
||||
</div>
|
||||
) : entityForm.type_id ? (
|
||||
<div style={{ color: "#cbd5e1", fontSize: "12px" }}>
|
||||
Type đang chọn: <b>{entityForm.type_id}</b>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
onClick={onCreateEntityOnly}
|
||||
disabled={isEntitySubmitting}
|
||||
style={{
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "7px 8px",
|
||||
cursor: isEntitySubmitting ? "not-allowed" : "pointer",
|
||||
background: "#2563eb",
|
||||
color: "#ffffff",
|
||||
opacity: isEntitySubmitting ? 0.7 : 1,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Tạo entity mới
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{entityFormStatus ? (
|
||||
<div style={{ color: "#93c5fd", fontSize: "12px", marginTop: "8px" }}>
|
||||
{entityFormStatus}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -402,15 +282,6 @@ const removeButtonStyle: CSSProperties = {
|
||||
fontSize: "12px",
|
||||
};
|
||||
|
||||
const secondaryActionButtonStyle: CSSProperties = {
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "7px 8px",
|
||||
cursor: "pointer",
|
||||
background: "#1d4ed8",
|
||||
color: "#ffffff",
|
||||
};
|
||||
|
||||
const primaryGeometryButtonStyle: CSSProperties = {
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
|
||||
Reference in New Issue
Block a user