refactor: modularize editor UI by extracting components into individual files
This commit is contained in:
+49
-435
@@ -1,9 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState } from "react";
|
||||||
import type { UndoAction } from "@/uhm/lib/useEditorState";
|
import type { UndoAction } from "@/uhm/lib/useEditorState";
|
||||||
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
|
|
||||||
|
import { ProjectPanel } from "./editor/ProjectPanel";
|
||||||
|
import { ToolsPanel } from "./editor/ToolsPanel";
|
||||||
|
import { CommitPanel } from "./editor/CommitPanel";
|
||||||
|
import { CommitHistoryPanel } from "./editor/CommitHistoryPanel";
|
||||||
|
import { UndoListPanel } from "./editor/UndoListPanel";
|
||||||
|
import { SessionPanel } from "./editor/SessionPanel";
|
||||||
|
import { SubmitModal } from "./editor/SubmitModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mode: EditorMode;
|
mode: EditorMode;
|
||||||
setMode: (mode: EditorMode) => void;
|
setMode: (mode: EditorMode) => void;
|
||||||
@@ -68,14 +76,6 @@ export default function Editor({
|
|||||||
createdGeometries,
|
createdGeometries,
|
||||||
width = 280,
|
width = 280,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const toggleMode = (newMode: EditorMode) => {
|
|
||||||
if (mode === newMode) {
|
|
||||||
setMode("idle");
|
|
||||||
} else {
|
|
||||||
setMode(newMode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false);
|
const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false);
|
||||||
const [submitContent, setSubmitContent] = useState("");
|
const [submitContent, setSubmitContent] = useState("");
|
||||||
|
|
||||||
@@ -93,46 +93,6 @@ export default function Editor({
|
|||||||
setIsSubmitModalOpen(false);
|
setIsSubmitModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const recentUndoLabels = (() => {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const labels: string[] = [];
|
|
||||||
for (let i = undoStack.length - 1; i >= 0 && labels.length < 8; i -= 1) {
|
|
||||||
const label = formatUndoLabel(undoStack[i]);
|
|
||||||
if (seen.has(label)) continue;
|
|
||||||
seen.add(label);
|
|
||||||
labels.push(label);
|
|
||||||
}
|
|
||||||
return labels.reverse();
|
|
||||||
})();
|
|
||||||
|
|
||||||
const formatCommitTitle = (commit: Props["commits"][number]) =>
|
|
||||||
commit.edit_summary?.trim() || `Commit ${commit.id.slice(0, 8)}`;
|
|
||||||
|
|
||||||
const modeButtonStyle = (btnMode: EditorMode) =>
|
|
||||||
({
|
|
||||||
padding: "8px 10px",
|
|
||||||
borderRadius: 6,
|
|
||||||
border: "1px solid #334155",
|
|
||||||
background: mode === btnMode ? "#16a34a" : "#111827",
|
|
||||||
color: "white",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontWeight: 800,
|
|
||||||
fontSize: 12,
|
|
||||||
minHeight: 34,
|
|
||||||
boxSizing: "border-box",
|
|
||||||
}) as const;
|
|
||||||
|
|
||||||
const primaryButtonStyle =
|
|
||||||
({
|
|
||||||
width: "100%",
|
|
||||||
padding: "8px 10px",
|
|
||||||
borderRadius: 6,
|
|
||||||
border: "none",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontWeight: 850,
|
|
||||||
fontSize: 12,
|
|
||||||
}) as const;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -146,77 +106,19 @@ export default function Editor({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ position: "sticky", top: 0, zIndex: 5, background: "#0b1220", paddingBottom: 10 }}>
|
<div style={{ position: "sticky", top: 0, zIndex: 5, background: "#0b1220", paddingBottom: 10 }}>
|
||||||
<div style={{ fontWeight: 950, fontSize: 14, marginBottom: 10 }}>Editor</div>
|
|
||||||
|
|
||||||
<Panel title="Project" defaultOpen>
|
<ProjectPanel
|
||||||
<div style={{ fontSize: 12, color: "#cbd5e1", lineHeight: 1.4 }}>
|
sectionTitle={sectionTitle}
|
||||||
<div style={{ color: "white", fontWeight: 850, overflowWrap: "anywhere" }}>{sectionTitle}</div>
|
sectionStatus={sectionStatus}
|
||||||
<div style={{ marginTop: 6 }}>
|
commitCount={commitCount}
|
||||||
Status: <span style={{ color: "#e2e8f0" }}>{sectionStatus}</span>
|
latestCommitLabel={latestCommitLabel}
|
||||||
</div>
|
/>
|
||||||
<div style={{ marginTop: 6 }}>
|
|
||||||
Commits: <span style={{ color: "#e2e8f0" }}>{commitCount}</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: 6 }}>
|
|
||||||
{latestCommitLabel ? (
|
|
||||||
<span style={{ color: "#e2e8f0" }}>{latestCommitLabel}</span>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: "#94a3b8" }}>Chưa có head commit</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Panel title="Tools" defaultOpen>
|
<ToolsPanel
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
|
mode={mode}
|
||||||
<button style={modeButtonStyle("select")} onClick={() => toggleMode("select")} title="Select">
|
setMode={setMode}
|
||||||
Select
|
onUndo={onUndo}
|
||||||
</button>
|
/>
|
||||||
<button style={modeButtonStyle("draw")} onClick={() => toggleMode("draw")} title="Draw polygon">
|
|
||||||
Draw
|
|
||||||
</button>
|
|
||||||
<button style={modeButtonStyle("add-point")} onClick={() => setMode("add-point")} title="Add point">
|
|
||||||
Point
|
|
||||||
</button>
|
|
||||||
<button style={modeButtonStyle("add-line")} onClick={() => setMode("add-line")} title="Add line">
|
|
||||||
Line
|
|
||||||
</button>
|
|
||||||
<button style={modeButtonStyle("add-path")} onClick={() => setMode("add-path")} title="Add path">
|
|
||||||
Path
|
|
||||||
</button>
|
|
||||||
<button style={modeButtonStyle("add-circle")} onClick={() => setMode("add-circle")} title="Add circle">
|
|
||||||
Circle
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: 10, fontSize: 12, color: "#94a3b8" }}>
|
|
||||||
Mode: <span style={{ color: "white", fontWeight: 850 }}>{mode}</span>
|
|
||||||
</div>
|
|
||||||
<ModeHint mode={mode} />
|
|
||||||
|
|
||||||
<div style={{ marginTop: 10, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...modeButtonStyle("idle"),
|
|
||||||
background: "#111827",
|
|
||||||
}}
|
|
||||||
onClick={() => setMode("idle")}
|
|
||||||
title="Tắt tool hiện tại"
|
|
||||||
>
|
|
||||||
Idle
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...modeButtonStyle("idle"),
|
|
||||||
background: "#334155",
|
|
||||||
}}
|
|
||||||
onClick={onUndo}
|
|
||||||
title="Undo thao tác gần nhất"
|
|
||||||
>
|
|
||||||
Undo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
{entityStatus ? (
|
{entityStatus ? (
|
||||||
<div
|
<div
|
||||||
@@ -236,327 +138,39 @@ export default function Editor({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Panel title="Commit" defaultOpen>
|
<CommitPanel
|
||||||
<input
|
commitTitle={commitTitle}
|
||||||
value={commitTitle}
|
onCommitTitleChange={onCommitTitleChange}
|
||||||
onChange={(event) => onCommitTitleChange(event.target.value)}
|
isSaving={isSaving}
|
||||||
placeholder="Edit Summary (Commit Title)"
|
isSubmitting={isSubmitting}
|
||||||
disabled={isSaving || isSubmitting}
|
changesCount={changesCount}
|
||||||
style={textInputStyle}
|
onCommit={onCommit}
|
||||||
/>
|
hasHeadCommit={hasHeadCommit}
|
||||||
<button
|
handleOpenSubmitModal={handleOpenSubmitModal}
|
||||||
style={{
|
/>
|
||||||
...primaryButtonStyle,
|
|
||||||
marginTop: 8,
|
|
||||||
background: isSaving || isSubmitting || changesCount <= 0 ? "#475569" : "#0f766e",
|
|
||||||
cursor: isSaving || isSubmitting || changesCount <= 0 ? "not-allowed" : "pointer",
|
|
||||||
opacity: changesCount <= 0 ? 0.75 : 1,
|
|
||||||
}}
|
|
||||||
onClick={onCommit}
|
|
||||||
disabled={isSaving || isSubmitting || changesCount <= 0}
|
|
||||||
title={changesCount <= 0 ? "Khong co thay doi de commit" : undefined}
|
|
||||||
>
|
|
||||||
Commit ({changesCount})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...primaryButtonStyle,
|
|
||||||
marginTop: 8,
|
|
||||||
background: isSubmitting || !hasHeadCommit ? "#475569" : "#16a34a",
|
|
||||||
cursor: isSubmitting || !hasHeadCommit ? "not-allowed" : "pointer",
|
|
||||||
opacity: !hasHeadCommit ? 0.6 : 1,
|
|
||||||
}}
|
|
||||||
onClick={handleOpenSubmitModal}
|
|
||||||
disabled={isSubmitting || !hasHeadCommit}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Panel title="Commit History" badge={String(commits.length)} defaultOpen={false}>
|
<CommitHistoryPanel
|
||||||
{commits.length === 0 ? (
|
commits={commits}
|
||||||
<div style={{ color: "#64748b", fontSize: 12 }}>Chưa có commit</div>
|
headCommitId={headCommitId}
|
||||||
) : (
|
onRestoreCommit={onRestoreCommit}
|
||||||
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 12 }}>
|
isSaving={isSaving}
|
||||||
{commits.slice(0, 8).map((commit) => {
|
isSubmitting={isSubmitting}
|
||||||
const isHead = Boolean(headCommitId && commit.id === headCommitId);
|
/>
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={commit.id}
|
|
||||||
style={{
|
|
||||||
padding: "8px 0",
|
|
||||||
borderBottom: "1px solid #1f2937",
|
|
||||||
color: "#e2e8f0",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{flex:1}}>
|
|
||||||
<div
|
|
||||||
title={formatCommitTitle(commit)}
|
|
||||||
style={{
|
|
||||||
fontWeight: 750,
|
|
||||||
color: "#f8fafc",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatCommitTitle(commit)}
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: 3, color: "#94a3b8" }}>
|
|
||||||
{commit.created_at ? new Date(commit.created_at).toLocaleString() : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<UndoListPanel undoStack={undoStack} />
|
||||||
style={{
|
|
||||||
marginTop: 6,
|
|
||||||
padding: "6px 8px",
|
|
||||||
borderRadius: 6,
|
|
||||||
border: "1px solid #334155",
|
|
||||||
background: isHead ? "#0b1220" : "#334155",
|
|
||||||
color: "white",
|
|
||||||
cursor: isSaving || isSubmitting || isHead ? "not-allowed" : "pointer",
|
|
||||||
opacity: isHead ? 0.65 : 1,
|
|
||||||
fontWeight: 800,
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
onClick={() => onRestoreCommit(commit.id)}
|
|
||||||
disabled={isSaving || isSubmitting || isHead}
|
|
||||||
title={isHead ? "Đang là head commit" : "Restore snapshot từ commit này (FE-only)"}
|
|
||||||
>
|
|
||||||
Restore
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Panel title="Undo List" badge={String(recentUndoLabels.length)} defaultOpen={false}>
|
<SessionPanel
|
||||||
{recentUndoLabels.length === 0 ? (
|
createdEntities={createdEntities}
|
||||||
<div style={{ color: "#94a3b8", fontSize: 13 }}>Chưa có thao tác</div>
|
createdGeometries={createdGeometries}
|
||||||
) : (
|
/>
|
||||||
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 13, color: "#e2e8f0" }}>
|
|
||||||
{recentUndoLabels.map((label, idx) => (
|
|
||||||
<li key={`${label}-${idx}`} style={{ padding: "6px 0", borderBottom: "1px solid #1f2937" }}>
|
|
||||||
{label}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Panel title="This Session" defaultOpen={false}>
|
<SubmitModal
|
||||||
<div style={{ fontSize: 13, color: "#cbd5e1", marginBottom: 6 }}>
|
isSubmitModalOpen={isSubmitModalOpen}
|
||||||
Entities ({createdEntities.length})
|
submitContent={submitContent}
|
||||||
</div>
|
setSubmitContent={setSubmitContent}
|
||||||
{createdEntities.length === 0 ? (
|
handleCancelSubmit={handleCancelSubmit}
|
||||||
<div style={{ color: "#64748b", fontSize: 12, marginBottom: 10 }}>Chưa tạo entity mới</div>
|
handleConfirmSubmit={handleConfirmSubmit}
|
||||||
) : (
|
/>
|
||||||
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 12, marginBottom: 10 }}>
|
|
||||||
{createdEntities.map((entity) => (
|
|
||||||
<li
|
|
||||||
key={entity.id}
|
|
||||||
style={{ padding: "6px 0", borderBottom: "1px solid #1f2937", color: "#e2e8f0" }}
|
|
||||||
title={entity.id}
|
|
||||||
>
|
|
||||||
{entity.name}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ fontSize: 13, color: "#cbd5e1", marginBottom: 6 }}>
|
|
||||||
Geometries mới chưa commit ({createdGeometries.length})
|
|
||||||
</div>
|
|
||||||
{createdGeometries.length === 0 ? (
|
|
||||||
<div style={{ color: "#64748b", fontSize: 12 }}>Chưa có geometry mới chờ commit</div>
|
|
||||||
) : (
|
|
||||||
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 12 }}>
|
|
||||||
{createdGeometries.map((geometry) => (
|
|
||||||
<li
|
|
||||||
key={String(geometry.id)}
|
|
||||||
style={{ padding: "6px 0", borderBottom: "1px solid #1f2937", color: "#e2e8f0" }}
|
|
||||||
>
|
|
||||||
#{geometry.id} [{geometry.geometryType}]{" "}
|
|
||||||
{geometry.semanticType ? `- ${geometry.semanticType}` : ""}
|
|
||||||
{geometry.entityNames.length ? ` | ${geometry.entityNames.join(", ")}` : ""}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
{isSubmitModalOpen && (
|
|
||||||
<div style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: 0, left: 0, right: 0, bottom: 0,
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
zIndex: 1000
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
background: "#0b1220",
|
|
||||||
padding: 20,
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid #334155",
|
|
||||||
width: 400,
|
|
||||||
color: "white"
|
|
||||||
}}>
|
|
||||||
<h3 style={{ marginTop: 0 }}>Nội dung Submit</h3>
|
|
||||||
<textarea
|
|
||||||
value={submitContent}
|
|
||||||
onChange={(e) => setSubmitContent(e.target.value)}
|
|
||||||
placeholder="Nhập nội dung submit..."
|
|
||||||
style={{ ...textAreaStyle, height: 100 }}
|
|
||||||
/>
|
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 10, marginTop: 15 }}>
|
|
||||||
<button onClick={handleCancelSubmit} style={{ padding: "8px 16px", borderRadius: 6, cursor: "pointer", border: "1px solid #334155", background: "transparent", color: "white" }}>Hủy</button>
|
|
||||||
<button onClick={handleConfirmSubmit} style={{ padding: "8px 16px", borderRadius: 6, cursor: "pointer", border: "none", background: "#16a34a", color: "white", fontWeight: "bold" }}>Gửi Submit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const textInputStyle = {
|
|
||||||
width: "100%",
|
|
||||||
marginTop: 0,
|
|
||||||
padding: "8px 10px",
|
|
||||||
borderRadius: 6,
|
|
||||||
border: "1px solid #334155",
|
|
||||||
background: "#0b1220",
|
|
||||||
color: "white",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
fontSize: 13,
|
|
||||||
outline: "none",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const textAreaStyle = {
|
|
||||||
...textInputStyle,
|
|
||||||
marginTop: 8,
|
|
||||||
resize: "vertical",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function Panel({
|
|
||||||
title,
|
|
||||||
badge,
|
|
||||||
defaultOpen,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
badge?: string | null;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<details
|
|
||||||
open={Boolean(defaultOpen)}
|
|
||||||
style={{
|
|
||||||
marginTop: 10,
|
|
||||||
padding: 10,
|
|
||||||
background: "#111827",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid #1f2937",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<summary
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
listStyle: "none",
|
|
||||||
fontWeight: 900,
|
|
||||||
fontSize: 13,
|
|
||||||
color: "white",
|
|
||||||
userSelect: "none",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{title}</span>
|
|
||||||
{badge ? (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
padding: "2px 8px",
|
|
||||||
borderRadius: 999,
|
|
||||||
border: "1px solid #334155",
|
|
||||||
background: "#0b1220",
|
|
||||||
color: "#cbd5e1",
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 850,
|
|
||||||
flex: "0 0 auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{badge}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</summary>
|
|
||||||
<div style={{ marginTop: 10 }}>{children}</div>
|
|
||||||
</details>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModeHint({ mode }: { mode: EditorMode }) {
|
|
||||||
if (mode === "add-line" || mode === "add-path") {
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
|
||||||
Click để thêm điểm, Enter để hoàn tất, Esc để hủy.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (mode === "add-circle") {
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
|
||||||
Giữ chuột trái kéo để mở bán kính, thả chuột để hoàn tất.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (mode === "add-point") {
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
|
||||||
Chọn 1 điểm trên bản đồ để đặt địa điểm.
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (mode === "select") {
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
|
||||||
Chọn 1 hình, đường, điểm trên bản đồ để xem chi tiết.
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (mode === "draw") {
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
|
||||||
Chọn các điểm trên bản đồ để vẽ hình, ENTER để kết thúc, ESC để hủy.
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUndoLabel(action: UndoAction) {
|
|
||||||
switch (action.type) {
|
|
||||||
case "create":
|
|
||||||
return `Thêm mới #${action.id}`;
|
|
||||||
case "delete":
|
|
||||||
return `Xóa #${action.feature.properties.id}`;
|
|
||||||
case "update":
|
|
||||||
return `Chỉnh sửa #${action.id}`;
|
|
||||||
case "properties":
|
|
||||||
return `Cập nhật thuộc tính #${action.id}`;
|
|
||||||
case "snapshot_entities":
|
|
||||||
case "snapshot_wikis":
|
|
||||||
case "snapshot_entity_wiki":
|
|
||||||
return action.label;
|
|
||||||
default:
|
|
||||||
return "Tác vụ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Panel } from "./Panel";
|
||||||
|
|
||||||
|
type Commit = {
|
||||||
|
id: string;
|
||||||
|
created_at?: string;
|
||||||
|
edit_summary: string;
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommitHistoryPanelProps = {
|
||||||
|
commits: Commit[];
|
||||||
|
headCommitId: string | null;
|
||||||
|
onRestoreCommit: (commitId: string) => void;
|
||||||
|
isSaving: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CommitHistoryPanel({
|
||||||
|
commits,
|
||||||
|
headCommitId,
|
||||||
|
onRestoreCommit,
|
||||||
|
isSaving,
|
||||||
|
isSubmitting,
|
||||||
|
}: CommitHistoryPanelProps) {
|
||||||
|
const formatCommitTitle = (commit: Commit) =>
|
||||||
|
commit.edit_summary?.trim() || `Commit ${commit.id.slice(0, 8)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel title="Commit History" badge={String(commits.length)} defaultOpen={false}>
|
||||||
|
{commits.length === 0 ? (
|
||||||
|
<div style={{ color: "#64748b", fontSize: 12 }}>Chưa có commit</div>
|
||||||
|
) : (
|
||||||
|
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 12 }}>
|
||||||
|
{commits.slice(0, 8).map((commit) => {
|
||||||
|
const isHead = Boolean(headCommitId && commit.id === headCommitId);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={commit.id}
|
||||||
|
style={{
|
||||||
|
padding: "8px 0",
|
||||||
|
borderBottom: "1px solid #1f2937",
|
||||||
|
color: "#e2e8f0",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{flex:1}}>
|
||||||
|
<div
|
||||||
|
title={formatCommitTitle(commit)}
|
||||||
|
style={{
|
||||||
|
fontWeight: 750,
|
||||||
|
color: "#f8fafc",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCommitTitle(commit)}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 3, color: "#94a3b8" }}>
|
||||||
|
{commit.created_at ? new Date(commit.created_at).toLocaleString() : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
marginTop: 6,
|
||||||
|
padding: "6px 8px",
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "1px solid #334155",
|
||||||
|
background: isHead ? "#0b1220" : "#334155",
|
||||||
|
color: "white",
|
||||||
|
cursor: isSaving || isSubmitting || isHead ? "not-allowed" : "pointer",
|
||||||
|
opacity: isHead ? 0.65 : 1,
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
onClick={() => onRestoreCommit(commit.id)}
|
||||||
|
disabled={isSaving || isSubmitting || isHead}
|
||||||
|
title={isHead ? "Đang là head commit" : "Restore snapshot từ commit này (FE-only)"}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { Panel } from "./Panel";
|
||||||
|
|
||||||
|
type CommitPanelProps = {
|
||||||
|
commitTitle: string;
|
||||||
|
onCommitTitleChange: (title: string) => void;
|
||||||
|
isSaving: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
changesCount: number;
|
||||||
|
onCommit: () => void;
|
||||||
|
hasHeadCommit: boolean;
|
||||||
|
handleOpenSubmitModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CommitPanel({
|
||||||
|
commitTitle,
|
||||||
|
onCommitTitleChange,
|
||||||
|
isSaving,
|
||||||
|
isSubmitting,
|
||||||
|
changesCount,
|
||||||
|
onCommit,
|
||||||
|
hasHeadCommit,
|
||||||
|
handleOpenSubmitModal,
|
||||||
|
}: CommitPanelProps) {
|
||||||
|
const primaryButtonStyle = {
|
||||||
|
width: "100%",
|
||||||
|
padding: "8px 10px",
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontWeight: 850,
|
||||||
|
fontSize: 12,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const textInputStyle = {
|
||||||
|
width: "100%",
|
||||||
|
marginTop: 0,
|
||||||
|
padding: "8px 10px",
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "1px solid #334155",
|
||||||
|
background: "#0b1220",
|
||||||
|
color: "white",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
fontSize: 13,
|
||||||
|
outline: "none",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel title="Commit" defaultOpen>
|
||||||
|
<input
|
||||||
|
value={commitTitle}
|
||||||
|
onChange={(event) => onCommitTitleChange(event.target.value)}
|
||||||
|
placeholder="Edit Summary (Commit Title)"
|
||||||
|
disabled={isSaving || isSubmitting}
|
||||||
|
style={textInputStyle}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
...primaryButtonStyle,
|
||||||
|
marginTop: 8,
|
||||||
|
background: isSaving || isSubmitting || changesCount <= 0 ? "#475569" : "#0f766e",
|
||||||
|
cursor: isSaving || isSubmitting || changesCount <= 0 ? "not-allowed" : "pointer",
|
||||||
|
opacity: changesCount <= 0 ? 0.75 : 1,
|
||||||
|
}}
|
||||||
|
onClick={onCommit}
|
||||||
|
disabled={isSaving || isSubmitting || changesCount <= 0}
|
||||||
|
title={changesCount <= 0 ? "Khong co thay doi de commit" : undefined}
|
||||||
|
>
|
||||||
|
Commit ({changesCount})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
...primaryButtonStyle,
|
||||||
|
marginTop: 8,
|
||||||
|
background: isSubmitting || !hasHeadCommit ? "#475569" : "#16a34a",
|
||||||
|
cursor: isSubmitting || !hasHeadCommit ? "not-allowed" : "pointer",
|
||||||
|
opacity: !hasHeadCommit ? 0.6 : 1,
|
||||||
|
}}
|
||||||
|
onClick={handleOpenSubmitModal}
|
||||||
|
disabled={isSubmitting || !hasHeadCommit}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
|
|
||||||
|
export function ModeHint({ mode }: { mode: EditorMode }) {
|
||||||
|
if (mode === "add-line" || mode === "add-path") {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
||||||
|
Click để thêm điểm, Enter để hoàn tất, Esc để hủy.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mode === "add-circle") {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
||||||
|
Giữ chuột trái kéo để mở bán kính, thả chuột để hoàn tất.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mode === "add-point") {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
||||||
|
Chọn 1 điểm trên bản đồ để đặt địa điểm.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (mode === "select") {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
||||||
|
Chọn 1 hình, đường, điểm trên bản đồ để xem chi tiết.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (mode === "draw") {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
|
||||||
|
Chọn các điểm trên bản đồ để vẽ hình, ENTER để kết thúc, ESC để hủy.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
type PanelProps = {
|
||||||
|
title: string;
|
||||||
|
badge?: string | null;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Panel({
|
||||||
|
title,
|
||||||
|
badge,
|
||||||
|
defaultOpen,
|
||||||
|
children,
|
||||||
|
}: PanelProps) {
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
open={Boolean(defaultOpen)}
|
||||||
|
style={{
|
||||||
|
marginTop: 10,
|
||||||
|
padding: 10,
|
||||||
|
background: "#111827",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid #1f2937",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
listStyle: "none",
|
||||||
|
fontWeight: 900,
|
||||||
|
fontSize: 13,
|
||||||
|
color: "white",
|
||||||
|
userSelect: "none",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{title}</span>
|
||||||
|
{badge ? (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
padding: "2px 8px",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: "1px solid #334155",
|
||||||
|
background: "#0b1220",
|
||||||
|
color: "#cbd5e1",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 850,
|
||||||
|
flex: "0 0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</summary>
|
||||||
|
<div style={{ marginTop: 10 }}>{children}</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Panel } from "./Panel";
|
||||||
|
|
||||||
|
type ProjectPanelProps = {
|
||||||
|
sectionTitle: string;
|
||||||
|
sectionStatus: string;
|
||||||
|
commitCount: number;
|
||||||
|
latestCommitLabel: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProjectPanel({
|
||||||
|
sectionTitle,
|
||||||
|
sectionStatus,
|
||||||
|
commitCount,
|
||||||
|
latestCommitLabel,
|
||||||
|
}: ProjectPanelProps) {
|
||||||
|
return (
|
||||||
|
<Panel title="Project" defaultOpen>
|
||||||
|
<div style={{ fontSize: 12, color: "#cbd5e1", lineHeight: 1.4 }}>
|
||||||
|
<div style={{ color: "white", fontWeight: 850, overflowWrap: "anywhere" }}>{sectionTitle}</div>
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
Status: <span style={{ color: "#e2e8f0" }}>{sectionStatus}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
Commits: <span style={{ color: "#e2e8f0" }}>{commitCount}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
{latestCommitLabel ? (
|
||||||
|
<span style={{ color: "#e2e8f0" }}>{latestCommitLabel}</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: "#94a3b8" }}>Chưa có head commit</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Panel } from "./Panel";
|
||||||
|
|
||||||
|
type SessionPanelProps = {
|
||||||
|
createdEntities: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
createdGeometries: Array<{
|
||||||
|
id: string | number;
|
||||||
|
geometryType: string;
|
||||||
|
semanticType?: string | null;
|
||||||
|
entityNames: string[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SessionPanel({
|
||||||
|
createdEntities,
|
||||||
|
createdGeometries,
|
||||||
|
}: SessionPanelProps) {
|
||||||
|
return (
|
||||||
|
<Panel title="This Session" defaultOpen={false}>
|
||||||
|
<div style={{ fontSize: 13, color: "#cbd5e1", marginBottom: 6 }}>
|
||||||
|
Entities ({createdEntities.length})
|
||||||
|
</div>
|
||||||
|
{createdEntities.length === 0 ? (
|
||||||
|
<div style={{ color: "#64748b", fontSize: 12, marginBottom: 10 }}>Chưa tạo entity mới</div>
|
||||||
|
) : (
|
||||||
|
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 12, marginBottom: 10 }}>
|
||||||
|
{createdEntities.map((entity) => (
|
||||||
|
<li
|
||||||
|
key={entity.id}
|
||||||
|
style={{ padding: "6px 0", borderBottom: "1px solid #1f2937", color: "#e2e8f0" }}
|
||||||
|
title={entity.id}
|
||||||
|
>
|
||||||
|
{entity.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ fontSize: 13, color: "#cbd5e1", marginBottom: 6 }}>
|
||||||
|
Geometries mới chưa commit ({createdGeometries.length})
|
||||||
|
</div>
|
||||||
|
{createdGeometries.length === 0 ? (
|
||||||
|
<div style={{ color: "#64748b", fontSize: 12 }}>Chưa có geometry mới chờ commit</div>
|
||||||
|
) : (
|
||||||
|
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 12 }}>
|
||||||
|
{createdGeometries.map((geometry) => (
|
||||||
|
<li
|
||||||
|
key={String(geometry.id)}
|
||||||
|
style={{ padding: "6px 0", borderBottom: "1px solid #1f2937", color: "#e2e8f0" }}
|
||||||
|
>
|
||||||
|
#{geometry.id} [{geometry.geometryType}]{" "}
|
||||||
|
{geometry.semanticType ? `- ${geometry.semanticType}` : ""}
|
||||||
|
{geometry.entityNames.length ? ` | ${geometry.entityNames.join(", ")}` : ""}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
type SubmitModalProps = {
|
||||||
|
isSubmitModalOpen: boolean;
|
||||||
|
submitContent: string;
|
||||||
|
setSubmitContent: (content: string) => void;
|
||||||
|
handleCancelSubmit: () => void;
|
||||||
|
handleConfirmSubmit: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SubmitModal({
|
||||||
|
isSubmitModalOpen,
|
||||||
|
submitContent,
|
||||||
|
setSubmitContent,
|
||||||
|
handleCancelSubmit,
|
||||||
|
handleConfirmSubmit,
|
||||||
|
}: SubmitModalProps) {
|
||||||
|
if (!isSubmitModalOpen) return null;
|
||||||
|
|
||||||
|
const textAreaStyle = {
|
||||||
|
width: "100%",
|
||||||
|
marginTop: 8,
|
||||||
|
padding: "8px 10px",
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "1px solid #334155",
|
||||||
|
background: "#0b1220",
|
||||||
|
color: "white",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
fontSize: 13,
|
||||||
|
outline: "none",
|
||||||
|
resize: "vertical",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
height: 100,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
zIndex: 1000
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: "#0b1220",
|
||||||
|
padding: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid #334155",
|
||||||
|
width: 400,
|
||||||
|
color: "white"
|
||||||
|
}}>
|
||||||
|
<h3 style={{ marginTop: 0 }}>Nội dung Submit</h3>
|
||||||
|
<textarea
|
||||||
|
value={submitContent}
|
||||||
|
onChange={(e) => setSubmitContent(e.target.value)}
|
||||||
|
placeholder="Nhập nội dung submit..."
|
||||||
|
style={textAreaStyle}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", justifyContent: "flex-end", gap: 10, marginTop: 15 }}>
|
||||||
|
<button onClick={handleCancelSubmit} style={{ padding: "8px 16px", borderRadius: 6, cursor: "pointer", border: "1px solid #334155", background: "transparent", color: "white" }}>Hủy</button>
|
||||||
|
<button onClick={handleConfirmSubmit} style={{ padding: "8px 16px", borderRadius: 6, cursor: "pointer", border: "none", background: "#16a34a", color: "white", fontWeight: "bold" }}>Gửi Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
|
||||||
|
import { Panel } from "./Panel";
|
||||||
|
import { ModeHint } from "./ModeHint";
|
||||||
|
|
||||||
|
type ToolsPanelProps = {
|
||||||
|
mode: EditorMode;
|
||||||
|
setMode: (mode: EditorMode) => void;
|
||||||
|
onUndo: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ToolsPanel({ mode, setMode, onUndo }: ToolsPanelProps) {
|
||||||
|
const toggleMode = (newMode: EditorMode) => {
|
||||||
|
if (mode === newMode) {
|
||||||
|
setMode("idle");
|
||||||
|
} else {
|
||||||
|
setMode(newMode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const modeButtonStyle = (btnMode: EditorMode) =>
|
||||||
|
({
|
||||||
|
padding: "8px 10px",
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "1px solid #334155",
|
||||||
|
background: mode === btnMode ? "#16a34a" : "#111827",
|
||||||
|
color: "white",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: 12,
|
||||||
|
minHeight: 34,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}) as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel title="Tools" defaultOpen>
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
|
||||||
|
<button style={modeButtonStyle("select")} onClick={() => toggleMode("select")} title="Select">
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
|
<button style={modeButtonStyle("draw")} onClick={() => toggleMode("draw")} title="Draw polygon">
|
||||||
|
Draw
|
||||||
|
</button>
|
||||||
|
<button style={modeButtonStyle("add-point")} onClick={() => setMode("add-point")} title="Add point">
|
||||||
|
Point
|
||||||
|
</button>
|
||||||
|
<button style={modeButtonStyle("add-line")} onClick={() => setMode("add-line")} title="Add line">
|
||||||
|
Line
|
||||||
|
</button>
|
||||||
|
<button style={modeButtonStyle("add-path")} onClick={() => setMode("add-path")} title="Add path">
|
||||||
|
Path
|
||||||
|
</button>
|
||||||
|
<button style={modeButtonStyle("add-circle")} onClick={() => setMode("add-circle")} title="Add circle">
|
||||||
|
Circle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 10, fontSize: 12, color: "#94a3b8" }}>
|
||||||
|
Mode: <span style={{ color: "white", fontWeight: 850 }}>{mode}</span>
|
||||||
|
</div>
|
||||||
|
<ModeHint mode={mode} />
|
||||||
|
|
||||||
|
<div style={{ marginTop: 10, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
...modeButtonStyle("idle"),
|
||||||
|
background: "#111827",
|
||||||
|
}}
|
||||||
|
onClick={() => setMode("idle")}
|
||||||
|
title="Tắt tool hiện tại"
|
||||||
|
>
|
||||||
|
Idle
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
...modeButtonStyle("idle"),
|
||||||
|
background: "#334155",
|
||||||
|
}}
|
||||||
|
onClick={onUndo}
|
||||||
|
title="Undo thao tác gần nhất"
|
||||||
|
>
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { UndoAction } from "@/uhm/lib/useEditorState";
|
||||||
|
import { Panel } from "./Panel";
|
||||||
|
|
||||||
|
type UndoListPanelProps = {
|
||||||
|
undoStack: UndoAction[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UndoListPanel({ undoStack }: UndoListPanelProps) {
|
||||||
|
const recentUndoLabels = (() => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const labels: string[] = [];
|
||||||
|
for (let i = undoStack.length - 1; i >= 0 && labels.length < 8; i -= 1) {
|
||||||
|
const label = formatUndoLabel(undoStack[i]);
|
||||||
|
if (seen.has(label)) continue;
|
||||||
|
seen.add(label);
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
return labels.reverse();
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel title="Undo List" badge={String(recentUndoLabels.length)} defaultOpen={false}>
|
||||||
|
{recentUndoLabels.length === 0 ? (
|
||||||
|
<div style={{ color: "#94a3b8", fontSize: 13 }}>Chưa có thao tác</div>
|
||||||
|
) : (
|
||||||
|
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: 13, color: "#e2e8f0" }}>
|
||||||
|
{recentUndoLabels.map((label, idx) => (
|
||||||
|
<li key={`${label}-${idx}`} style={{ padding: "6px 0", borderBottom: "1px solid #1f2937" }}>
|
||||||
|
{label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUndoLabel(action: UndoAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "create":
|
||||||
|
return `Thêm mới #${action.id}`;
|
||||||
|
case "delete":
|
||||||
|
return `Xóa #${action.feature.properties.id}`;
|
||||||
|
case "update":
|
||||||
|
return `Chỉnh sửa #${action.id}`;
|
||||||
|
case "properties":
|
||||||
|
return `Cập nhật thuộc tính #${action.id}`;
|
||||||
|
case "snapshot_entities":
|
||||||
|
case "snapshot_wikis":
|
||||||
|
case "snapshot_entity_wiki":
|
||||||
|
return action.label;
|
||||||
|
default:
|
||||||
|
return "Tác vụ";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user