refactor: simplify timeline stage creation form by removing redundant validation logic and improving layout
Build and Release / release (push) Successful in 36s

This commit is contained in:
taDuc
2026-05-25 08:19:53 +07:00
parent f38ae2c288
commit de91f8129e
6 changed files with 266 additions and 329 deletions
+1
View File
@@ -1801,6 +1801,7 @@ function EditorPageContent() {
type: "Feature", type: "Feature",
properties: { properties: {
id: geoId, id: geoId,
source: "ref",
type: typeKey, type: typeKey,
time_start: normalizeTimelineYearValue(geo.time_start), time_start: normalizeTimelineYearValue(geo.time_start),
time_end: normalizeTimelineYearValue(geo.time_end), time_end: normalizeTimelineYearValue(geo.time_end),
@@ -39,15 +39,15 @@ type ActionFieldConfig = {
name: string; name: string;
label: string; label: string;
kind: kind:
| "text" | "text"
| "textarea" | "textarea"
| "number" | "number"
| "boolean" | "boolean"
| "color" | "color"
| "select" | "select"
| "geometry" | "geometry"
| "geometry-multi" | "geometry-multi"
| "wiki"; | "wiki";
placeholder?: string; placeholder?: string;
options?: Array<{ label: string; value: string }>; options?: Array<{ label: string; value: string }>;
visibleWhen?: (values: ActionFormValues) => boolean; visibleWhen?: (values: ActionFormValues) => boolean;
@@ -207,9 +207,9 @@ export default function ReplayEffectsSidebar({
null; null;
const selectedStep = const selectedStep =
selectedStage && selectedStage &&
selectedStepIndex != null && selectedStepIndex != null &&
selectedStepIndex >= 0 && selectedStepIndex >= 0 &&
selectedStepIndex < selectedStage.steps.length selectedStepIndex < selectedStage.steps.length
? selectedStage.steps[selectedStepIndex] ? selectedStage.steps[selectedStepIndex]
: null; : null;
const mapCameraActions = useMemo( const mapCameraActions = useMemo(
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; import { useMemo, useState } from "react";
import type { import type {
BattleReplay, BattleReplay,
GeoFunctionName, GeoFunctionName,
@@ -43,46 +43,6 @@ type AnyStepAction =
| ReplayAction<GeoFunctionName> | ReplayAction<GeoFunctionName>
| ReplayAction<NarrativeFunctionName>; | ReplayAction<NarrativeFunctionName>;
function validateReplayTimeFormat(value: string): boolean {
const trimmed = value.trim();
if (!trimmed) return false;
// 1. Check DD/MM/YYYY
const dmyRegex = /^(\d{2})\/(\d{2})\/(-?\d{4})$/;
const dmyMatch = trimmed.match(dmyRegex);
if (dmyMatch) {
const day = parseInt(dmyMatch[1], 10);
const month = parseInt(dmyMatch[2], 10);
const year = parseInt(dmyMatch[3], 10);
if (month < 1 || month > 12) return false;
if (day < 1 || day > 31) return false;
if (month === 2) {
const isLeap = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
if (day > (isLeap ? 29 : 28)) return false;
} else if ([4, 6, 9, 11].includes(month)) {
if (day > 30) return false;
}
return true;
}
// 2. Check MM/YYYY
const myRegex = /^(\d{2})\/(-?\d{4})$/;
const myMatch = trimmed.match(myRegex);
if (myMatch) {
const month = parseInt(myMatch[1], 10);
if (month < 1 || month > 12) return false;
return true;
}
// 3. Check YYYY
const yRegex = /^(-?\d{4})$/;
if (yRegex.test(trimmed)) {
return true;
}
return false;
}
type StageFormState = { type StageFormState = {
title: string; title: string;
detail_time_start: string; detail_time_start: string;
@@ -143,13 +103,6 @@ export default function ReplayTimelineSidebar({
detail_time_stop: "", detail_time_stop: "",
}); });
const [createStagePanelKey, setCreateStagePanelKey] = useState(0); const [createStagePanelKey, setCreateStagePanelKey] = useState(0);
const isStartValid = validateReplayTimeFormat(createStageForm.detail_time_start);
const isStopValid = validateReplayTimeFormat(createStageForm.detail_time_stop);
const isCreateFormValid = createStageForm.title.trim() !== "" && isStartValid && isStopValid;
const showStartError = createStageForm.detail_time_start.length > 0 && !isStartValid;
const showStopError = createStageForm.detail_time_stop.length > 0 && !isStopValid;
const [openWeightEditorKey, setOpenWeightEditorKey] = useState<string | null>(null); const [openWeightEditorKey, setOpenWeightEditorKey] = useState<string | null>(null);
const [openActionDetailKey, setOpenActionDetailKey] = useState<string | null>(null); const [openActionDetailKey, setOpenActionDetailKey] = useState<string | null>(null);
@@ -401,26 +354,7 @@ export default function ReplayTimelineSidebar({
); );
}; };
const handleDuplicateAction = (
stageId: number,
stepIndex: number,
groupKey: ActionGroupKey,
actionIndex: number,
actionTitle: string
) => {
onMutateReplay(
`Replay: nhân bản ${actionTitle} ở step ${stepIndex + 1} của stage #${stageId}`,
(draftReplay) => {
const stage = draftReplay.detail.find((item) => item.id === stageId);
if (!stage || stepIndex < 0 || stepIndex >= stage.steps.length) return;
const step = stage.steps[stepIndex];
const actions = [...getStepActionGroup(step, groupKey)];
if (actionIndex < 0 || actionIndex >= actions.length) return;
actions.splice(actionIndex + 1, 0, cloneReplayAction(actions[actionIndex]) as AnyStepAction);
setStepActionGroup(step, groupKey, actions);
}
);
};
const handleUpdateActionParams = ( const handleUpdateActionParams = (
stageId: number, stageId: number,
@@ -603,63 +537,66 @@ export default function ReplayTimelineSidebar({
onChange={(event) => onChange={(event) =>
setCreateStageForm((prev) => ({ ...prev, title: event.target.value })) setCreateStageForm((prev) => ({ ...prev, title: event.target.value }))
} }
placeholder="Title (bắt buộc)" placeholder="Title"
style={inputStyle} style={inputStyle}
/> />
<div> <input
<input value={createStageForm.detail_time_start}
value={createStageForm.detail_time_start} onChange={(event) =>
onChange={(event) => setCreateStageForm((prev) => ({
setCreateStageForm((prev) => ({ ...prev,
...prev, detail_time_start: event.target.value,
detail_time_start: event.target.value, }))
})) }
} placeholder="detail_time_start (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)"
placeholder="Thời gian bắt đầu (detail_time_start)" style={{
style={{ ...inputStyle,
...inputStyle, border: createStageForm.detail_time_start && !validateReplayTimeFormat(createStageForm.detail_time_start)
borderColor: showStartError ? "#ef4444" : "#334155", ? "1px solid #ef4444"
}} : undefined,
/> }}
{showStartError ? ( />
<div style={{ color: "#ef4444", fontSize: 10, marginTop: 3 }}> <input
Đnh dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY) value={createStageForm.detail_time_stop}
</div> onChange={(event) =>
) : null} setCreateStageForm((prev) => ({
</div> ...prev,
<div> detail_time_stop: event.target.value,
<input }))
value={createStageForm.detail_time_stop} }
onChange={(event) => placeholder="detail_time_stop (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)"
setCreateStageForm((prev) => ({ style={{
...prev, ...inputStyle,
detail_time_stop: event.target.value, border: createStageForm.detail_time_stop && !validateReplayTimeFormat(createStageForm.detail_time_stop)
})) ? "1px solid #ef4444"
} : undefined,
placeholder="Thời gian kết thúc (detail_time_stop)" }}
style={{ />
...inputStyle, <div style={{ fontSize: 10, color: "#94a3b8", lineHeight: 1.3 }}>
borderColor: showStopError ? "#ef4444" : "#334155", * Đnh dạng bắt buộc: <strong>ngày/tháng/năm</strong> (00/00/0000), <strong>tháng/năm</strong> (00/0000) hoặc <strong>năm</strong> (0000).
}}
/>
{showStopError ? (
<div style={{ color: "#ef4444", fontSize: 10, marginTop: 3 }}>
Đnh dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY)
</div>
) : null}
</div>
<div style={{ fontSize: 10, color: "#94a3b8", lineHeight: "1.4" }}>
Cấu trúc bắt buộc: <strong>DD/MM/YYYY</strong>, <strong>MM/YYYY</strong>, hoặc <strong>YYYY</strong>.
</div> </div>
<button <button
type="button" type="button"
disabled={!isCreateFormValid}
onClick={handleCreateStage} onClick={handleCreateStage}
disabled={
!createStageForm.title.trim() ||
!validateReplayTimeFormat(createStageForm.detail_time_start) ||
!validateReplayTimeFormat(createStageForm.detail_time_stop)
}
style={{ style={{
...buttonStyle, ...buttonStyle,
background: isCreateFormValid ? "#1d4ed8" : "#1e293b", background:
color: isCreateFormValid ? "white" : "#64748b", createStageForm.title.trim() &&
cursor: isCreateFormValid ? "pointer" : "not-allowed", validateReplayTimeFormat(createStageForm.detail_time_start) &&
validateReplayTimeFormat(createStageForm.detail_time_stop)
? "#1d4ed8"
: "#475569",
cursor:
createStageForm.title.trim() &&
validateReplayTimeFormat(createStageForm.detail_time_start) &&
validateReplayTimeFormat(createStageForm.detail_time_stop)
? "pointer"
: "not-allowed",
border: "none", border: "none",
}} }}
> >
@@ -838,157 +775,142 @@ export default function ReplayTimelineSidebar({
const actionKey = `${stage.id}:${stepIndex}:${entry.groupKey}:${entry.actionIndex}:${entry.functionName}`; const actionKey = `${stage.id}:${stepIndex}:${entry.groupKey}:${entry.actionIndex}:${entry.functionName}`;
const isActionOpen = openActionDetailKey === actionKey; const isActionOpen = openActionDetailKey === actionKey;
return ( return (
<div <div
key={actionKey} key={actionKey}
style={{ style={{
display: "grid", display: "grid",
gap: 3, gap: 3,
padding: "5px 6px", padding: "5px 6px",
borderRadius: 6, borderRadius: 6,
background: "rgba(15, 23, 42, 0.92)", background: "rgba(15, 23, 42, 0.92)",
border: "1px solid rgba(51, 65, 85, 0.8)", border: "1px solid rgba(51, 65, 85, 0.8)",
}} }}
> >
<div style={{ display: "flex", alignItems: "stretch", gap: 6 }}> <div style={{ display: "flex", alignItems: "stretch", gap: 6 }}>
<button <button
type="button" type="button"
onClick={() => onClick={() =>
setOpenActionDetailKey((prev) => setOpenActionDetailKey((prev) =>
prev === actionKey ? null : actionKey prev === actionKey ? null : actionKey
) )
} }
style={{
flex: 1,
border: "none",
background: "transparent",
padding: 0,
margin: 0,
textAlign: "left",
color: "inherit",
cursor: "pointer",
}}
>
<div
style={{ style={{
display: "flex", flex: 1,
alignItems: "center", border: "none",
justifyContent: "space-between", background: "transparent",
gap: 8, padding: 0,
margin: 0,
textAlign: "left",
color: "inherit",
cursor: "pointer",
}} }}
> >
<div style={{ display: "grid", gap: 3, minWidth: 0 }}> <div
<div style={{ display: "flex", alignItems: "center", gap: 6, minWidth: 0 }}> style={{
<span display: "flex",
style={{ alignItems: "center",
display: "inline-flex", justifyContent: "space-between",
alignItems: "center", gap: 8,
padding: "1px 6px", }}
borderRadius: 999, >
background: entry.badgeBackground, <div style={{ display: "grid", gap: 3, minWidth: 0 }}>
color: entry.badgeColor, <div style={{ display: "flex", alignItems: "center", gap: 6, minWidth: 0 }}>
fontSize: 10, <span
fontWeight: 900, style={{
letterSpacing: 0.3, display: "inline-flex",
textTransform: "uppercase", alignItems: "center",
}} padding: "1px 6px",
> borderRadius: 999,
{entry.group} background: entry.badgeBackground,
</span> color: entry.badgeColor,
<span fontSize: 10,
fontWeight: 900,
letterSpacing: 0.3,
textTransform: "uppercase",
}}
>
{entry.group}
</span>
<span
style={{
fontSize: 11,
color: "#93c5fd",
fontWeight: 800,
overflowWrap: "anywhere",
}}
>
{entry.functionName}
</span>
</div>
<div
style={{ style={{
fontSize: 11, fontSize: 11,
color: "#93c5fd", color: "white",
fontWeight: 800, fontWeight: 700,
overflowWrap: "anywhere", overflowWrap: "anywhere",
}} }}
> >
{entry.functionName} {entry.title}
</span> </div>
</div> </div>
<div <span
style={{ style={{
fontSize: 11, fontSize: 11,
color: "white", color: "#94a3b8",
fontWeight: 700, fontWeight: 800,
overflowWrap: "anywhere", flex: "0 0 auto",
}} }}
> >
{entry.title} {isActionOpen ? "" : "+"}
</div> </span>
</div> </div>
<span </button>
style={{ <div style={{ display: "grid", gridTemplateColumns: "auto", gap: 4 }}>
fontSize: 11, <button
color: "#94a3b8", type="button"
fontWeight: 800, onClick={() =>
flex: "0 0 auto", handleDeleteAction(
}} stage.id,
stepIndex,
entry.groupKey,
entry.actionIndex,
entry.title
)
}
style={actionButtonStyle(false, "#7f1d1d")}
> >
{isActionOpen ? "" : "+"} Xóa
</span> </button>
</div> </div>
</button>
<div style={{ display: "grid", gridTemplateColumns: "repeat(2, auto)", gap: 4 }}>
<button
type="button"
onClick={() =>
handleDuplicateAction(
stage.id,
stepIndex,
entry.groupKey,
entry.actionIndex,
entry.title
)
}
style={actionButtonStyle(false, "#0f766e")}
>
Copy
</button>
<button
type="button"
onClick={() =>
handleDeleteAction(
stage.id,
stepIndex,
entry.groupKey,
entry.actionIndex,
entry.title
)
}
style={actionButtonStyle(false, "#7f1d1d")}
>
Xóa
</button>
</div> </div>
{isActionOpen ? (
<div
style={{
fontSize: 11,
color: "#94a3b8",
lineHeight: 1.3,
overflowWrap: "anywhere",
}}
>
{entry.summary}
<InlineActionParamsEditor
key={actionKey}
action={entry.action}
onApply={(nextParams) =>
handleUpdateActionParams(
stage.id,
stepIndex,
entry.groupKey,
entry.actionIndex,
entry.title,
nextParams
)
}
/>
</div>
) : null}
</div> </div>
{isActionOpen ? ( );
<div
style={{
fontSize: 11,
color: "#94a3b8",
lineHeight: 1.3,
overflowWrap: "anywhere",
}}
>
{entry.summary}
<InlineActionParamsEditor
key={actionKey}
action={entry.action}
onApply={(nextParams) =>
handleUpdateActionParams(
stage.id,
stepIndex,
entry.groupKey,
entry.actionIndex,
entry.title,
nextParams
)
}
/>
</div>
) : null}
</div>
);
})} })}
</div> </div>
) : null} ) : null}
@@ -1120,23 +1042,13 @@ function StageMetadataEditor({
detail_time_stop: stage.detail_time_stop || "", detail_time_stop: stage.detail_time_stop || "",
}); });
useEffect(() => {
setForm({
title: stage.title || "",
detail_time_start: stage.detail_time_start || "",
detail_time_stop: stage.detail_time_stop || "",
});
}, [stage]);
const isStartValid = validateReplayTimeFormat(form.detail_time_start); const isStartValid = validateReplayTimeFormat(form.detail_time_start);
const isStopValid = validateReplayTimeFormat(form.detail_time_stop); const isStopValid = validateReplayTimeFormat(form.detail_time_stop);
const isEditFormValid = form.title.trim() !== "" && isStartValid && isStopValid; const isTitleValid = form.title.trim().length > 0;
const isFormValid = isStartValid && isStopValid && isTitleValid;
const showStartError = form.detail_time_start.length > 0 && !isStartValid;
const showStopError = form.detail_time_stop.length > 0 && !isStopValid;
const handleApplyStageMetadata = () => { const handleApplyStageMetadata = () => {
if (!isEditFormValid) return; if (!isFormValid) return;
onMutateReplay(`Replay: cập nhật stage #${stage.id}`, (draftReplay) => { onMutateReplay(`Replay: cập nhật stage #${stage.id}`, (draftReplay) => {
const targetStage = draftReplay.detail.find((item) => item.id === stage.id); const targetStage = draftReplay.detail.find((item) => item.id === stage.id);
if (!targetStage) return; if (!targetStage) return;
@@ -1147,70 +1059,55 @@ function StageMetadataEditor({
}; };
return ( return (
<Panel title="Stage Metadata" badge={`#${stage.id}`} defaultOpen={false}> <Panel title="Stage Metadata" badge={`#${stage.id}`} defaultOpen={false}>
<div style={{ display: "grid", gap: 8 }}> <div style={{ display: "grid", gap: 8 }}>
<input <input
value={form.title} value={form.title}
onChange={(event) => onChange={(event) =>
setForm((prev) => ({ ...prev, title: event.target.value })) setForm((prev) => ({ ...prev, title: event.target.value }))
} }
placeholder="Title (bắt buộc)" placeholder="Title"
style={inputStyle} style={inputStyle}
/> />
<div> <input
<input value={form.detail_time_start}
value={form.detail_time_start} onChange={(event) =>
onChange={(event) => setForm((prev) => ({
setForm((prev) => ({ ...prev,
...prev, detail_time_start: event.target.value,
detail_time_start: event.target.value, }))
})) }
} placeholder="detail_time_start (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)"
placeholder="Thời gian bắt đầu (detail_time_start)" style={{
style={{ ...inputStyle,
...inputStyle, border: form.detail_time_start && !isStartValid ? "1px solid #ef4444" : undefined,
borderColor: showStartError ? "#ef4444" : "#334155", }}
}} />
/> <input
{showStartError ? ( value={form.detail_time_stop}
<div style={{ color: "#ef4444", fontSize: 10, marginTop: 3 }}> onChange={(event) =>
Đnh dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY) setForm((prev) => ({
</div> ...prev,
) : null} detail_time_stop: event.target.value,
</div> }))
<div> }
<input placeholder="detail_time_stop (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)"
value={form.detail_time_stop} style={{
onChange={(event) => ...inputStyle,
setForm((prev) => ({ border: form.detail_time_stop && !isStopValid ? "1px solid #ef4444" : undefined,
...prev, }}
detail_time_stop: event.target.value, />
})) <div style={{ fontSize: 10, color: "#94a3b8", lineHeight: 1.3 }}>
} * Đnh dạng bắt buộc: <strong>ngày/tháng/năm</strong> (00/00/0000), <strong>tháng/năm</strong> (00/0000) hoặc <strong>năm</strong> (0000).
placeholder="Thời gian kết thúc (detail_time_stop)"
style={{
...inputStyle,
borderColor: showStopError ? "#ef4444" : "#334155",
}}
/>
{showStopError ? (
<div style={{ color: "#ef4444", fontSize: 10, marginTop: 3 }}>
Đnh dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY)
</div>
) : null}
</div>
<div style={{ fontSize: 10, color: "#94a3b8", lineHeight: "1.4" }}>
Cấu trúc bắt buộc: <strong>DD/MM/YYYY</strong>, <strong>MM/YYYY</strong>, hoặc <strong>YYYY</strong>.
</div> </div>
<button <button
type="button" type="button"
disabled={!isEditFormValid}
onClick={handleApplyStageMetadata} onClick={handleApplyStageMetadata}
disabled={!isFormValid}
style={{ style={{
...buttonStyle, ...buttonStyle,
background: isEditFormValid ? "#0f766e" : "#1e293b", background: isFormValid ? "#0f766e" : "#475569",
color: isEditFormValid ? "white" : "#64748b", cursor: isFormValid ? "pointer" : "not-allowed",
cursor: isEditFormValid ? "pointer" : "not-allowed",
border: "none", border: "none",
}} }}
> >
@@ -1788,3 +1685,34 @@ function truncateText(value: string, maxLength: number) {
? `${value.slice(0, Math.max(0, maxLength - 1))}` ? `${value.slice(0, Math.max(0, maxLength - 1))}`
: value; : value;
} }
export function validateReplayTimeFormat(value: string): boolean {
const val = value.trim();
if (!val) return false;
// YYYY (e.g. 1945)
if (/^\d{4}$/.test(val)) {
return true;
}
// MM/YYYY (e.g. 05/1945)
if (/^(0[1-9]|1[0-2])\/\d{4}$/.test(val)) {
return true;
}
// DD/MM/YYYY (e.g. 15/05/1945)
if (/^(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/.test(val)) {
const parts = val.split("/");
const d = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10);
const y = parseInt(parts[2], 10);
const monthLengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if ((y % 4 === 0 && y % 100 !== 0) || y % 400 === 0) {
monthLengths[1] = 29;
}
return d <= monthLengths[m - 1];
}
return false;
}
+1
View File
@@ -43,6 +43,7 @@ export type FeatureId = string | number;
export type FeatureProperties = { export type FeatureProperties = {
id: FeatureId; id: FeatureId;
source?: SnapshotSource;
type?: string | null; type?: string | null;
geometry_preset?: GeometryPreset | null; geometry_preset?: GeometryPreset | null;
time_start?: number | null; time_start?: number | null;
@@ -395,6 +395,7 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
// Generate geometry metadata onto feature properties (optional in persisted snapshot). // Generate geometry metadata onto feature properties (optional in persisted snapshot).
const geo = geometryById.get(gid) || null; const geo = geometryById.get(gid) || null;
const existingSource = p.source === "inline" || p.source === "ref" ? p.source : undefined;
if (geo) { if (geo) {
const geoRecord = geo as unknown as UnknownRecord; const geoRecord = geo as unknown as UnknownRecord;
// type can arrive as numeric geo_type, numeric string, or semantic key depending on backend version. // type can arrive as numeric geo_type, numeric string, or semantic key depending on backend version.
@@ -406,6 +407,7 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
if (geo.bound_with !== undefined) { if (geo.bound_with !== undefined) {
p.bound_with = geo.bound_with; p.bound_with = geo.bound_with;
} }
p.source = geo.source || existingSource || "inline";
const timeStart = normalizeTimelineYearValue(geo.time_start); const timeStart = normalizeTimelineYearValue(geo.time_start);
const timeEnd = normalizeTimelineYearValue(geo.time_end); const timeEnd = normalizeTimelineYearValue(geo.time_end);
if (timeStart !== null) { if (timeStart !== null) {
@@ -418,8 +420,11 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
} else { } else {
delete p.time_end; delete p.time_end;
} }
} else if (!existingTypeKey) { } else {
p.type = fallbackTypeKey; p.source = existingSource || "inline";
if (!existingTypeKey) {
p.type = fallbackTypeKey;
}
} }
} }
return cloned; return cloned;
@@ -570,7 +575,7 @@ export function buildEditorSnapshot(options: {
return { return {
id, id,
operation, operation,
source: "inline", source: feature.properties.source || "inline",
type: typeKey, type: typeKey,
draw_geometry: feature.geometry, draw_geometry: feature.geometry,
bound_with: normalizeFeatureBoundWith(feature), bound_with: normalizeFeatureBoundWith(feature),
@@ -644,6 +649,7 @@ export function buildEditorSnapshot(options: {
delete p.time_end; delete p.time_end;
delete p.bound_with; delete p.bound_with;
delete p.binding; delete p.binding;
delete p.source;
delete p.entity_id; delete p.entity_id;
delete p.entity_ids; delete p.entity_ids;
delete p.entity_name; delete p.entity_name;
+1
View File
@@ -17,6 +17,7 @@ export type FeatureId = string | number;
export type FeatureProperties = { export type FeatureProperties = {
id: FeatureId; id: FeatureId;
source?: "inline" | "ref";
type?: string | null; type?: string | null;
geometry_preset?: GeometryPreset | null; geometry_preset?: GeometryPreset | null;
time_start?: number | null; time_start?: number | null;