reduce api | version control

This commit is contained in:
taDuc
2026-04-19 00:13:22 +07:00
parent c88a6497f7
commit bc98830871
9 changed files with 1141 additions and 449 deletions

View File

@@ -9,8 +9,42 @@ type Props = {
setMode: (mode: Mode) => void;
entityStatus?: string | null;
onUndo: () => void;
onSave: () => void;
onCommit: () => void;
onSubmit: () => void;
onRestoreCommit: (commitId: string) => void;
isSaving: boolean;
isSubmitting: boolean;
isOpeningSection: boolean;
sectionTitle: string;
sectionStatus: string;
selectedSectionId: string;
editorUserId: string;
sectionOptions: Array<{
id: string;
title: string;
state?: {
status?: string;
};
}>;
newSectionTitle: string;
commitTitle: string;
commitNote: string;
onEditorUserIdChange: (userId: string) => void;
onSelectedSectionIdChange: (sectionId: string) => void;
onNewSectionTitleChange: (title: string) => void;
onCommitTitleChange: (title: string) => void;
onCommitNoteChange: (note: string) => void;
onOpenSection: () => void;
onCreateSection: () => void;
commitCount: number;
latestCommitLabel: string | null;
commits: Array<{
id: string;
commit_no: number;
kind: string;
created_at: string;
title: string | null;
}>;
changesCount: number;
undoStack: UndoAction[];
createdEntities: Array<{
@@ -31,8 +65,30 @@ export default function Editor({
setMode,
entityStatus,
onUndo,
onSave,
onCommit,
onSubmit,
onRestoreCommit,
isSaving,
isSubmitting,
isOpeningSection,
sectionTitle,
sectionStatus,
selectedSectionId,
editorUserId,
sectionOptions,
newSectionTitle,
commitTitle,
commitNote,
onEditorUserIdChange,
onSelectedSectionIdChange,
onNewSectionTitleChange,
onCommitTitleChange,
onCommitNoteChange,
onOpenSection,
onCreateSection,
commitCount,
latestCommitLabel,
commits,
changesCount,
undoStack,
createdEntities,
@@ -83,6 +139,124 @@ export default function Editor({
}}
>
<h3 style={{ marginBottom: "10px" }}>Editor</h3>
<div
style={{
marginBottom: "12px",
padding: "10px",
background: "#0b1220",
borderRadius: "6px",
border: "1px solid #1f2937",
fontSize: "12px",
color: "#cbd5e1",
}}
>
<div style={{ color: "white", fontWeight: 600 }}>{sectionTitle}</div>
<div style={{ marginTop: "4px" }}>Status: {sectionStatus}</div>
<div>Commits: {commitCount}</div>
<div>{latestCommitLabel || "Chưa có commit"}</div>
</div>
<div
style={{
marginBottom: "12px",
padding: "10px",
background: "#0b1220",
borderRadius: "6px",
border: "1px solid #1f2937",
fontSize: "12px",
color: "#cbd5e1",
}}
>
<div style={{ marginBottom: "8px", fontWeight: 600, color: "white" }}>
Section
</div>
<input
value={editorUserId}
onChange={(event) => onEditorUserIdChange(event.target.value)}
placeholder="User ID"
style={{
width: "100%",
marginBottom: "8px",
padding: "7px",
borderRadius: "4px",
border: "1px solid #334155",
background: "#111827",
color: "white",
boxSizing: "border-box",
}}
disabled={isOpeningSection}
/>
<select
value={selectedSectionId}
onChange={(event) => onSelectedSectionIdChange(event.target.value)}
style={{
width: "100%",
padding: "7px",
borderRadius: "4px",
border: "1px solid #334155",
background: "#111827",
color: "white",
}}
disabled={isOpeningSection}
>
{sectionOptions.length === 0 ? (
<option value="">Chưa section</option>
) : null}
{sectionOptions.map((section) => (
<option key={section.id} value={section.id}>
{section.title} {section.state?.status ? `(${section.state.status})` : ""}
</option>
))}
</select>
<button
style={{
width: "100%",
marginTop: "8px",
padding: "8px",
borderRadius: "4px",
border: "none",
cursor: isOpeningSection || !selectedSectionId ? "not-allowed" : "pointer",
background: isOpeningSection || !selectedSectionId ? "#555" : "#2563eb",
color: "white",
}}
onClick={onOpenSection}
disabled={isOpeningSection || !selectedSectionId}
>
Mở section
</button>
<input
value={newSectionTitle}
onChange={(event) => onNewSectionTitleChange(event.target.value)}
placeholder="Tên section mới"
style={{
width: "100%",
marginTop: "10px",
padding: "7px",
borderRadius: "4px",
border: "1px solid #334155",
background: "#111827",
color: "white",
boxSizing: "border-box",
}}
disabled={isOpeningSection}
/>
<button
style={{
width: "100%",
marginTop: "8px",
padding: "8px",
borderRadius: "4px",
border: "none",
cursor: isOpeningSection || !newSectionTitle.trim() ? "not-allowed" : "pointer",
background: isOpeningSection || !newSectionTitle.trim() ? "#555" : "#0f766e",
color: "white",
}}
onClick={onCreateSection}
disabled={isOpeningSection || !newSectionTitle.trim()}
>
Tạo mở section
</button>
</div>
<button
style={getButtonStyle("draw")}
@@ -168,10 +342,10 @@ export default function Editor({
</div>
) : null}
<div style={{ marginTop: "12px", display: "flex", gap: "8px" }}>
<div style={{ marginTop: "12px" }}>
<button
style={{
flex: 1,
width: "100%",
padding: "8px",
borderRadius: "4px",
border: "none",
@@ -183,22 +357,125 @@ export default function Editor({
>
Undo
</button>
<button
style={{
flex: 1,
padding: "8px",
borderRadius: "4px",
border: "none",
cursor: isSaving ? "not-allowed" : "pointer",
background: isSaving ? "#555" : "#3b82f6",
color: "white",
opacity: changesCount === 0 ? 0.6 : 1,
}}
onClick={onSave}
disabled={isSaving || changesCount === 0}
>
Save ({changesCount})
</button>
</div>
<input
value={commitTitle}
onChange={(event) => onCommitTitleChange(event.target.value)}
placeholder="Commit title"
disabled={isSaving || isSubmitting || sectionStatus === "submitted"}
style={{
width: "100%",
marginTop: "8px",
padding: "7px",
borderRadius: "4px",
border: "1px solid #334155",
background: "#111827",
color: "white",
boxSizing: "border-box",
}}
/>
<textarea
value={commitNote}
onChange={(event) => onCommitNoteChange(event.target.value)}
placeholder="Commit note"
disabled={isSaving || isSubmitting || sectionStatus === "submitted"}
rows={3}
style={{
width: "100%",
marginTop: "8px",
padding: "7px",
borderRadius: "4px",
border: "1px solid #334155",
background: "#111827",
color: "white",
boxSizing: "border-box",
resize: "vertical",
fontFamily: "inherit",
}}
/>
<button
style={{
width: "100%",
marginTop: "8px",
padding: "8px",
borderRadius: "4px",
border: "none",
cursor: isSaving || isSubmitting || sectionStatus === "submitted" ? "not-allowed" : "pointer",
background: isSaving || isSubmitting || sectionStatus === "submitted" ? "#555" : "#0f766e",
color: "white",
}}
onClick={onCommit}
disabled={isSaving || isSubmitting || sectionStatus === "submitted"}
>
Commit ({changesCount})
</button>
<button
style={{
width: "100%",
marginTop: "8px",
padding: "8px",
borderRadius: "4px",
border: "none",
cursor: isSubmitting || commitCount === 0 || sectionStatus === "submitted" ? "not-allowed" : "pointer",
background: isSubmitting || commitCount === 0 || sectionStatus === "submitted" ? "#555" : "#16a34a",
color: "white",
opacity: commitCount === 0 ? 0.6 : 1,
}}
onClick={onSubmit}
disabled={isSubmitting || commitCount === 0 || sectionStatus === "submitted"}
>
Submit
</button>
<div
style={{
marginTop: "16px",
padding: "10px",
background: "#0b1220",
borderRadius: "6px",
border: "1px solid #1f2937",
}}
>
<div style={{ marginBottom: "8px", fontWeight: 600, fontSize: "14px" }}>
Commit history
</div>
{commits.length === 0 ? (
<div style={{ color: "#64748b", fontSize: "12px" }}>
Chưa commit
</div>
) : (
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: "12px" }}>
{commits.slice(0, 8).map((commit) => (
<li
key={commit.id}
style={{
padding: "6px 0",
borderBottom: "1px solid #1f2937",
color: "#e2e8f0",
}}
>
<div>
#{commit.commit_no} {commit.kind}
</div>
<button
style={{
marginTop: "4px",
padding: "4px 6px",
borderRadius: "4px",
border: "none",
background: "#334155",
color: "white",
cursor: isSaving || isSubmitting ? "not-allowed" : "pointer",
}}
onClick={() => onRestoreCommit(commit.id)}
disabled={isSaving || isSubmitting}
>
Restore
</button>
</li>
))}
</ul>
)}
</div>
<div
@@ -265,11 +542,11 @@ export default function Editor({
)}
<div style={{ fontSize: "13px", color: "#cbd5e1", marginBottom: "6px" }}>
Geometries mới chưa lưu ({createdGeometries.length})
Geometries mới chưa commit ({createdGeometries.length})
</div>
{createdGeometries.length === 0 ? (
<div style={{ color: "#64748b", fontSize: "12px" }}>
Chưa geometry mới chờ save
Chưa geometry mới chờ commit
</div>
) : (
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: "12px" }}>

View File

@@ -42,17 +42,9 @@ type Props = {
entityTypeOptions: EntityTypeOption[];
geometryMetaForm: GeometryMetaFormState;
onGeometryMetaFormChange: (key: keyof GeometryMetaFormState, value: string) => void;
bindingGeometrySearchQuery: string;
onBindingGeometrySearchQueryChange: (value: string) => void;
bindingGeometrySearchResults: Array<{
id: string;
label: string;
}>;
selectedBindingGeometryId: string | null;
onSelectBindingGeometryId: (value: string | null) => void;
onAddSelectedBindingGeometry: () => void;
isEntitySubmitting: boolean;
onCreateEntityOnly: () => void;
onApplyGeometryMetadata: () => void;
onApplyEntitiesForSelectedGeometry: () => void;
changeCount: number;
entityFormStatus: string | null;
@@ -77,14 +69,9 @@ export default function SelectedGeometryPanel({
entityTypeOptions,
geometryMetaForm,
onGeometryMetaFormChange,
bindingGeometrySearchQuery,
onBindingGeometrySearchQueryChange,
bindingGeometrySearchResults,
selectedBindingGeometryId,
onSelectBindingGeometryId,
onAddSelectedBindingGeometry,
isEntitySubmitting,
onCreateEntityOnly,
onApplyGeometryMetadata,
onApplyEntitiesForSelectedGeometry,
changeCount,
entityFormStatus,
@@ -187,10 +174,6 @@ export default function SelectedGeometryPanel({
</div>
)}
<div style={{ color: "#94a3b8", fontSize: "12px" }}>
Geometry phải ít nhất 1 entity đ Save.
</div>
<div
style={{
display: "grid",
@@ -202,10 +185,10 @@ export default function SelectedGeometryPanel({
}}
>
<div style={{ color: "#e2e8f0", fontWeight: 700, fontSize: "12px" }}>
Metadata geometry (chỉ áp dụng khi bind entity)
Thuộc tính GEO
</div>
<div style={{ color: "#94a3b8", fontSize: "11px" }}>
`time_start`, `time_end`, `binding` chỉ đưc áp dụng khi bấm nút bind entity cho geometry.
Các giá trị này thuộc về GEO đang chọn, không phụ thuộc entity.
</div>
<input
value={geometryMetaForm.time_start}
@@ -221,44 +204,13 @@ export default function SelectedGeometryPanel({
disabled={isEntitySubmitting}
style={entityInputStyle}
/>
<input
value={geometryMetaForm.binding}
onChange={(event) => onGeometryMetaFormChange("binding", event.target.value)}
placeholder="binding ids (vd: geo-id-1, geo-id-2)"
disabled={isEntitySubmitting}
style={entityInputStyle}
/>
<input
value={bindingGeometrySearchQuery}
onChange={(event) =>
onBindingGeometrySearchQueryChange(event.target.value)
}
placeholder="Search geometry để thêm vào binding..."
disabled={isEntitySubmitting}
style={entityInputStyle}
/>
<select
value={selectedBindingGeometryId || ""}
onChange={(event) =>
onSelectBindingGeometryId(event.target.value ? event.target.value : null)
}
disabled={isEntitySubmitting}
style={entityInputStyle}
>
<option value="">-- Chọn geometry từ kết quả search binding --</option>
{bindingGeometrySearchResults.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
<button
type="button"
onClick={onAddSelectedBindingGeometry}
onClick={onApplyGeometryMetadata}
disabled={isEntitySubmitting}
style={secondaryActionButtonStyle}
style={primaryGeometryButtonStyle}
>
Thêm geometry đã chọn vào binding
Apply
</button>
</div>
@@ -327,13 +279,13 @@ export default function SelectedGeometryPanel({
fontWeight: 600,
}}
>
Áp dụng danh sách entity + metadata
Áp dụng danh sách entity
</button>
</div>
{changeCount > 0 ? (
<div style={{ color: "#fca5a5", fontSize: "12px" }}>
Geometry mới sẽ lưu entity khi bấm Save.
Thay đi sẽ vào lịch sử khi Commit.
</div>
) : null}
</div>
@@ -470,6 +422,16 @@ const secondaryActionButtonStyle: CSSProperties = {
color: "#ffffff",
};
const primaryGeometryButtonStyle: CSSProperties = {
border: "none",
borderRadius: "6px",
padding: "7px 8px",
cursor: "pointer",
background: "#0f766e",
color: "#ffffff",
fontWeight: 600,
};
function resolveFeatureGeometryPreset(feature: Feature): EntityGeometryPreset {
const explicitPreset = normalizeGeometryPreset(feature.properties.geometry_preset);
if (explicitPreset) return explicitPreset;