add somenew UI editor feature for more effêcncy
This commit is contained in:
@@ -8,6 +8,9 @@ import { useEditorStore } from "@/uhm/store/editorStore";
|
||||
type GeometryChoice = {
|
||||
id: string;
|
||||
label?: string;
|
||||
time_start?: number | null;
|
||||
time_end?: number | null;
|
||||
isTimelineVisible?: boolean;
|
||||
isNew?: boolean;
|
||||
};
|
||||
|
||||
@@ -31,16 +34,16 @@ export default function GeometryBindingPanel({
|
||||
statusText,
|
||||
bindingFilterEnabled,
|
||||
setGeometryBindingFilterEnabled,
|
||||
hoveredGeometryId,
|
||||
setHoveredGeometryId,
|
||||
geometryVisibility,
|
||||
setGeometryVisibility,
|
||||
} = useEditorStore(
|
||||
useShallow((state) => ({
|
||||
selectedFeatureIds: state.selectedFeatureIds,
|
||||
statusText: state.geoBindingStatus,
|
||||
bindingFilterEnabled: state.geometryBindingFilterEnabled,
|
||||
setGeometryBindingFilterEnabled: state.setGeometryBindingFilterEnabled,
|
||||
hoveredGeometryId: state.hoveredGeometryId,
|
||||
setHoveredGeometryId: state.setHoveredGeometryId,
|
||||
geometryVisibility: state.geometryVisibility,
|
||||
setGeometryVisibility: state.setGeometryVisibility,
|
||||
}))
|
||||
);
|
||||
const effectiveSelectedGeometryId =
|
||||
@@ -55,7 +58,14 @@ export default function GeometryBindingPanel({
|
||||
const rows = useMemo(() => {
|
||||
const cleaned = (geometries || [])
|
||||
.filter((g) => g && typeof g.id === "string" && g.id.trim().length > 0)
|
||||
.map((g) => ({ id: g.id.trim(), label: (g.label || "").trim(), isNew: Boolean(g.isNew) }));
|
||||
.map((g) => ({
|
||||
id: g.id.trim(),
|
||||
label: (g.label || "").trim(),
|
||||
time_start: typeof g.time_start === "number" ? g.time_start : null,
|
||||
time_end: typeof g.time_end === "number" ? g.time_end : null,
|
||||
isTimelineVisible: Boolean(g.isTimelineVisible),
|
||||
isNew: Boolean(g.isNew),
|
||||
}));
|
||||
cleaned.sort((a, b) => a.id.localeCompare(b.id));
|
||||
return cleaned;
|
||||
}, [geometries]);
|
||||
@@ -80,15 +90,20 @@ export default function GeometryBindingPanel({
|
||||
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);
|
||||
};
|
||||
|
||||
const toggleGeometryVisibility = (geometryId: string) => {
|
||||
setGeometryVisibility((prev) => ({
|
||||
...prev,
|
||||
[geometryId]: prev[geometryId] === false,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -97,7 +112,6 @@ 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 }}>
|
||||
@@ -148,31 +162,28 @@ export default function GeometryBindingPanel({
|
||||
</div>
|
||||
|
||||
{collapsed ? null : selectedGeometry ? (
|
||||
(() => {
|
||||
const isHidden = geometryVisibility[selectedGeometry.id] === false;
|
||||
const idColor = getGeometryIdColor(selectedGeometry);
|
||||
const labelColor = selectedGeometry.isTimelineVisible ? "#22c55e" : "#e5e7eb";
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
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)",
|
||||
"1px solid rgba(59, 130, 246, 0.45)",
|
||||
background: "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",
|
||||
opacity: isHidden ? 0.58 : 1,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
title={selectedGeometry.id}
|
||||
role={canFocusGeometry ? "button" : undefined}
|
||||
tabIndex={canFocusGeometry ? 0 : undefined}
|
||||
onClick={() => handleFocusGeometry(selectedGeometry.id)}
|
||||
onKeyDown={(event) => handleFocusKeyDown(event, selectedGeometry.id)}
|
||||
onMouseEnter={() => setHoveredGeometryId(selectedGeometry.id)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -190,7 +201,7 @@ export default function GeometryBindingPanel({
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "#e5e7eb",
|
||||
color: labelColor,
|
||||
fontWeight: 700,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
@@ -199,13 +210,26 @@ export default function GeometryBindingPanel({
|
||||
>
|
||||
{selectedGeometry.label || selectedGeometry.id}
|
||||
</span>
|
||||
{isHidden ? <span style={hiddenBadgeStyle}>hidden</span> : null}
|
||||
{selectedGeometry.isNew ? <NewBadge /> : null}
|
||||
<button
|
||||
type="button"
|
||||
title={isHidden ? "Show geometry on map" : "Hide geometry on map"}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleGeometryVisibility(selectedGeometry.id);
|
||||
}}
|
||||
style={iconButtonStyle}
|
||||
aria-label={isHidden ? `Show geometry ${selectedGeometry.id}` : `Hide geometry ${selectedGeometry.id}`}
|
||||
>
|
||||
{isHidden ? <EyeOffIcon /> : <EyeIcon />}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 3,
|
||||
fontSize: "11px",
|
||||
color: "#94a3b8",
|
||||
color: idColor,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
@@ -214,6 +238,8 @@ export default function GeometryBindingPanel({
|
||||
{selectedGeometry.id}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : null}
|
||||
|
||||
{collapsed ? null : rows.length ? (
|
||||
@@ -221,36 +247,33 @@ export default function GeometryBindingPanel({
|
||||
{visibleRows
|
||||
.map((g) => {
|
||||
const isBound = bindingSet.has(g.id);
|
||||
const isHovered = hoveredGeometryId === g.id;
|
||||
const isHidden = geometryVisibility[g.id] === false;
|
||||
const idColor = getGeometryIdColor(g);
|
||||
const labelColor = g.isTimelineVisible ? "#22c55e" : "#e5e7eb";
|
||||
return (
|
||||
<div
|
||||
key={g.id}
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: isHovered
|
||||
? "1px solid rgba(245, 158, 11, 0.95)"
|
||||
: isBound
|
||||
border: isBound
|
||||
? "1px solid rgba(20, 184, 166, 0.65)"
|
||||
: "1px solid #1f2937",
|
||||
background: isHovered
|
||||
? "rgba(245, 158, 11, 0.18)"
|
||||
: isBound
|
||||
background: 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",
|
||||
opacity: isHidden ? 0.55 : canBindToggle ? 1 : 0.75,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
title={g.id}
|
||||
role={canFocusGeometry ? "button" : undefined}
|
||||
tabIndex={canFocusGeometry ? 0 : undefined}
|
||||
onClick={() => handleFocusGeometry(g.id)}
|
||||
onKeyDown={(event) => handleFocusKeyDown(event, g.id)}
|
||||
onMouseEnter={() => setHoveredGeometryId(g.id)}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
@@ -264,7 +287,7 @@ export default function GeometryBindingPanel({
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "#e5e7eb",
|
||||
color: labelColor,
|
||||
fontWeight: 700,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
@@ -273,13 +296,14 @@ export default function GeometryBindingPanel({
|
||||
>
|
||||
{g.label || g.id}
|
||||
</span>
|
||||
{isHidden ? <span style={hiddenBadgeStyle}>hidden</span> : null}
|
||||
{isBound ? <span style={boundBadgeStyle}>bound</span> : null}
|
||||
{g.isNew ? <NewBadge /> : null}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "#94a3b8",
|
||||
color: idColor,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
@@ -289,6 +313,19 @@ export default function GeometryBindingPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title={isHidden ? "Show geometry on map" : "Hide geometry on map"}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleGeometryVisibility(g.id);
|
||||
}}
|
||||
style={iconButtonStyle}
|
||||
aria-label={isHidden ? `Show geometry ${g.id}` : `Hide geometry ${g.id}`}
|
||||
>
|
||||
{isHidden ? <EyeOffIcon /> : <EyeIcon />}
|
||||
</button>
|
||||
|
||||
{canBindToggle ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -356,6 +393,74 @@ const boundBadgeStyle: CSSProperties = {
|
||||
letterSpacing: 0,
|
||||
};
|
||||
|
||||
const hiddenBadgeStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "0 0 auto",
|
||||
height: 17,
|
||||
padding: "0 6px",
|
||||
borderRadius: 999,
|
||||
border: "1px solid rgba(148, 163, 184, 0.45)",
|
||||
background: "rgba(71, 85, 105, 0.32)",
|
||||
color: "#cbd5e1",
|
||||
fontSize: 10,
|
||||
fontWeight: 900,
|
||||
lineHeight: 1,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0,
|
||||
};
|
||||
|
||||
const iconButtonStyle: CSSProperties = {
|
||||
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",
|
||||
};
|
||||
|
||||
function getGeometryIdColor(geometry: GeometryChoice): string {
|
||||
const hasStart = typeof geometry.time_start === "number";
|
||||
const hasEnd = typeof geometry.time_end === "number";
|
||||
if (!hasStart && !hasEnd) return "#f87171";
|
||||
if (!hasStart || !hasEnd) return "#facc15";
|
||||
return "#94a3b8";
|
||||
}
|
||||
|
||||
function EyeIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"
|
||||
stroke="#cbd5e1"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" stroke="#cbd5e1" strokeWidth="2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function EyeOffIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M3 3l18 18" stroke="#fca5a5" strokeWidth="2" strokeLinecap="round" />
|
||||
<path
|
||||
d="M10.6 6.2A10.5 10.5 0 0 1 12 6c6 0 9.5 6 9.5 6a17 17 0 0 1-2.1 2.8M6.2 8.1A17 17 0 0 0 2.5 12s3.5 6 9.5 6c1.3 0 2.5-.3 3.5-.7"
|
||||
stroke="#fca5a5"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEvent } from "react";
|
||||
import type { MapImageOverlay } from "@/uhm/components/map/imageOverlay";
|
||||
|
||||
type Props = {
|
||||
overlay: MapImageOverlay | null;
|
||||
onPickImage: (file: File | null) => void;
|
||||
onPasteImage: () => void;
|
||||
keyboardEnabled: boolean;
|
||||
onKeyboardEnabledChange: (enabled: boolean) => void;
|
||||
onOpacityChange: (opacity: number) => void;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
export default function ImageOverlayPanel({
|
||||
overlay,
|
||||
onPickImage,
|
||||
onPasteImage,
|
||||
keyboardEnabled,
|
||||
onKeyboardEnabledChange,
|
||||
onOpacityChange,
|
||||
onRemove,
|
||||
}: Props) {
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0] || null;
|
||||
onPickImage(file);
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
<section style={{ padding: 10, background: "#0b1220", borderRadius: 8, border: "1px solid #1f2937" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 800, fontSize: 13, color: "white" }}>Trace Image</div>
|
||||
<div style={{ marginTop: 2, fontSize: 11, color: "#94a3b8" }}>
|
||||
Chuột phải kéo điểm vàng để di chuyển, điểm xanh để kéo dãn giữ ratio.
|
||||
{keyboardEnabled ? " WASD di chuyển, Q/E phóng to/thu nhỏ." : ""}
|
||||
</div>
|
||||
</div>
|
||||
{overlay ? (
|
||||
<button type="button" onClick={onRemove} style={dangerButtonStyle}>
|
||||
Remove
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10, display: "flex", flexWrap: "wrap", gap: 8 }}>
|
||||
<label style={uploadButtonStyle}>
|
||||
{overlay ? "Đổi ảnh" : "Thêm ảnh"}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" onClick={onPasteImage} style={uploadButtonStyle}>
|
||||
Paste ảnh
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onKeyboardEnabledChange(!keyboardEnabled)}
|
||||
disabled={!overlay}
|
||||
style={{
|
||||
...uploadButtonStyle,
|
||||
opacity: overlay ? 1 : 0.5,
|
||||
color: keyboardEnabled ? "#86efac" : "#93c5fd",
|
||||
cursor: overlay ? "pointer" : "not-allowed",
|
||||
}}
|
||||
title="Bật/tắt điều khiển ảnh bằng WASD và Q/E"
|
||||
>
|
||||
Keys: {keyboardEnabled ? "On" : "Off"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{overlay ? (
|
||||
<div style={{ marginTop: 10, display: "grid", gap: 8 }}>
|
||||
<div style={{ fontSize: 12, color: "#cbd5e1", overflowWrap: "anywhere" }}>
|
||||
{overlay.name}
|
||||
</div>
|
||||
<label style={{ display: "grid", gap: 6, fontSize: 12, color: "#cbd5e1" }}>
|
||||
<span>Opacity: {Math.round(overlay.opacity * 100)}%</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={overlay.opacity}
|
||||
onChange={(event) => onOpacityChange(Number(event.target.value))}
|
||||
style={{ width: "100%", accentColor: "#38bdf8" }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: "#94a3b8" }}>
|
||||
Chưa có ảnh overlay.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const uploadButtonStyle = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "1px solid #334155",
|
||||
borderRadius: 6,
|
||||
background: "#111827",
|
||||
color: "#93c5fd",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
padding: "7px 10px",
|
||||
} as const;
|
||||
|
||||
const dangerButtonStyle = {
|
||||
border: "1px solid #7f1d1d",
|
||||
borderRadius: 6,
|
||||
background: "#1f1111",
|
||||
color: "#fecaca",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
padding: "6px 8px",
|
||||
} as const;
|
||||
@@ -8,8 +8,9 @@ import { useEditorStore } from "@/uhm/store/editorStore";
|
||||
|
||||
type Props = {
|
||||
onCreateEntityOnly: () => void;
|
||||
onUpdateEntity?: (entityId: string, payload: { name: string; description: string | null }) => void;
|
||||
onUpdateEntity?: (entityId: string, payload: { name: string; description: string | null; time_start: string; time_end: string }) => void;
|
||||
hasSelectedGeometry?: boolean;
|
||||
selectedGeometryTime?: { time_start: number | null; time_end: number | null } | null;
|
||||
onToggleBindEntityForSelectedGeometry?: (entityId: string, nextChecked: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -17,6 +18,7 @@ export default function ProjectEntityRefsPanel({
|
||||
onCreateEntityOnly,
|
||||
onUpdateEntity,
|
||||
hasSelectedGeometry,
|
||||
selectedGeometryTime,
|
||||
onToggleBindEntityForSelectedGeometry,
|
||||
}: Props) {
|
||||
const {
|
||||
@@ -78,15 +80,35 @@ export default function ProjectEntityRefsPanel({
|
||||
);
|
||||
const [editName, setEditName] = useState("");
|
||||
const [editDescription, setEditDescription] = useState("");
|
||||
const [editTimeStart, setEditTimeStart] = useState("");
|
||||
const [editTimeEnd, setEditTimeEnd] = useState("");
|
||||
const canCopySelectedGeometryTime =
|
||||
selectedGeometryTime != null &&
|
||||
(selectedGeometryTime.time_start != null || selectedGeometryTime.time_end != null);
|
||||
|
||||
const openEntityEditor = (entity: EntitySnapshot) => {
|
||||
setActiveEntityId(String(entity.id));
|
||||
setEditName(typeof entity.name === "string" ? entity.name : "");
|
||||
setEditDescription(entity.description == null ? "" : String(entity.description));
|
||||
setEditTimeStart(entity.time_start != null ? String(entity.time_start) : "");
|
||||
setEditTimeEnd(entity.time_end != null ? String(entity.time_end) : "");
|
||||
};
|
||||
const handleEntityFormChange = (key: "name" | "description", value: string) => {
|
||||
const handleEntityFormChange = (key: "name" | "description" | "time_start" | "time_end", value: string) => {
|
||||
setEntityForm((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
const copySelectedGeometryTimeToCreateForm = () => {
|
||||
if (!canCopySelectedGeometryTime || !selectedGeometryTime) return;
|
||||
setEntityForm((prev) => ({
|
||||
...prev,
|
||||
time_start: selectedGeometryTime.time_start != null ? String(selectedGeometryTime.time_start) : "",
|
||||
time_end: selectedGeometryTime.time_end != null ? String(selectedGeometryTime.time_end) : "",
|
||||
}));
|
||||
};
|
||||
const copySelectedGeometryTimeToEditForm = () => {
|
||||
if (!canCopySelectedGeometryTime || !selectedGeometryTime) return;
|
||||
setEditTimeStart(selectedGeometryTime.time_start != null ? String(selectedGeometryTime.time_start) : "");
|
||||
setEditTimeEnd(selectedGeometryTime.time_end != null ? String(selectedGeometryTime.time_end) : "");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -237,27 +259,27 @@ export default function ProjectEntityRefsPanel({
|
||||
</span>
|
||||
{isNewEntityRef(activeEntity) ? <NewBadge /> : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveEntityId(null)}
|
||||
title="Dong"
|
||||
aria-label="Dong sua entity"
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 6,
|
||||
border: "1px solid #334155",
|
||||
background: "#0b1220",
|
||||
color: "#e2e8f0",
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copySelectedGeometryTimeToEditForm}
|
||||
disabled={!canCopySelectedGeometryTime || isEntitySubmitting}
|
||||
title="Lay nam cua GEO dang chon"
|
||||
aria-label="Lay nam cua GEO dang chon cho entity dang sua"
|
||||
style={iconButtonStyle(!canCopySelectedGeometryTime || isEntitySubmitting)}
|
||||
>
|
||||
<ClockIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveEntityId(null)}
|
||||
title="Dong"
|
||||
aria-label="Dong sua entity"
|
||||
style={iconButtonStyle(false)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 11, color: "#94a3b8", overflowWrap: "anywhere" }}>
|
||||
@@ -277,10 +299,31 @@ export default function ProjectEntityRefsPanel({
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<div style={timeInputGridStyle}>
|
||||
<input
|
||||
value={editTimeStart}
|
||||
onChange={(event) => setEditTimeStart(event.target.value)}
|
||||
placeholder="time_start"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={editTimeEnd}
|
||||
onChange={(event) => setEditTimeEnd(event.target.value)}
|
||||
placeholder="time_end"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateEntity!(String(activeEntity.id), { name: editName, description: editDescription.trim().length ? editDescription : null })}
|
||||
onClick={() => onUpdateEntity!(String(activeEntity.id), {
|
||||
name: editName,
|
||||
description: editDescription.trim().length ? editDescription : null,
|
||||
time_start: editTimeStart,
|
||||
time_end: editTimeEnd,
|
||||
})}
|
||||
disabled={isEntitySubmitting}
|
||||
style={{
|
||||
border: "none",
|
||||
@@ -315,29 +358,30 @@ export default function ProjectEntityRefsPanel({
|
||||
<div style={{ color: "#bfdbfe", fontWeight: 700, fontSize: "12px" }}>
|
||||
Tạo entity mới
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateOpen((v) => !v)}
|
||||
disabled={isEntitySubmitting}
|
||||
title={isCreateOpen ? "Dong" : "Mo"}
|
||||
aria-label={isCreateOpen ? "Dong tao entity" : "Mo tao entity"}
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 6,
|
||||
border: "1px solid #334155",
|
||||
background: "#0b1220",
|
||||
color: "#e2e8f0",
|
||||
cursor: isEntitySubmitting ? "not-allowed" : "pointer",
|
||||
opacity: isEntitySubmitting ? 0.6 : 1,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
{isCreateOpen ? <CloseIcon /> : <PlusIcon />}
|
||||
</button>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
{isCreateOpen ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={copySelectedGeometryTimeToCreateForm}
|
||||
disabled={!canCopySelectedGeometryTime || isEntitySubmitting}
|
||||
title="Lay nam cua GEO dang chon"
|
||||
aria-label="Lay nam cua GEO dang chon cho entity moi"
|
||||
style={iconButtonStyle(!canCopySelectedGeometryTime || isEntitySubmitting)}
|
||||
>
|
||||
<ClockIcon />
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateOpen((v) => !v)}
|
||||
disabled={isEntitySubmitting}
|
||||
title={isCreateOpen ? "Dong" : "Mo"}
|
||||
aria-label={isCreateOpen ? "Dong tao entity" : "Mo tao entity"}
|
||||
style={iconButtonStyle(isEntitySubmitting)}
|
||||
>
|
||||
{isCreateOpen ? <CloseIcon /> : <PlusIcon />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCreateOpen ? (
|
||||
@@ -356,6 +400,22 @@ export default function ProjectEntityRefsPanel({
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<div style={timeInputGridStyle}>
|
||||
<input
|
||||
value={entityForm.time_start}
|
||||
onChange={(event) => handleEntityFormChange("time_start", event.target.value)}
|
||||
placeholder="time_start"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={entityForm.time_end}
|
||||
onChange={(event) => handleEntityFormChange("time_end", event.target.value)}
|
||||
placeholder="time_end"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -403,6 +463,29 @@ const entityInputStyle: CSSProperties = {
|
||||
fontSize: "13px",
|
||||
};
|
||||
|
||||
const timeInputGridStyle: CSSProperties = {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: 8,
|
||||
};
|
||||
|
||||
function iconButtonStyle(disabled: boolean): CSSProperties {
|
||||
return {
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 6,
|
||||
border: "1px solid #334155",
|
||||
background: "#0b1220",
|
||||
color: "#e2e8f0",
|
||||
cursor: disabled ? "not-allowed" : "pointer",
|
||||
opacity: disabled ? 0.55 : 1,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "0 0 auto",
|
||||
};
|
||||
}
|
||||
|
||||
const boundBadgeStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
@@ -488,3 +571,12 @@ function CloseIcon() {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ClockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="8" stroke="#fbbf24" strokeWidth="2" />
|
||||
<path d="M12 7v5l3 2" stroke="#fbbf24" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user