Documentation
This commit is contained in:
@@ -148,6 +148,9 @@ interface ColumnDef {
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
// Centralized column metadata keeps render, filtering/sorting, keyboard traversal,
|
||||
// and persisted layout in sync. BMK/equipmentIdentifier stays locked as first column
|
||||
// because circuit identity and circuit drag handles depend on always-visible BMK context.
|
||||
const allColumns: ColumnDef[] = [
|
||||
{ key: "equipmentIdentifier", label: "Equipment identifier", defaultVisible: true, locked: true },
|
||||
{ key: "displayName", label: "Display name", defaultVisible: true },
|
||||
@@ -463,13 +466,24 @@ function isPrintableKey(event: KeyboardEvent<HTMLElement>) {
|
||||
}
|
||||
|
||||
export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) {
|
||||
/*
|
||||
Circuit-tree editor invariant overview:
|
||||
- Data model is circuit-first (section -> circuit -> device rows).
|
||||
- Rendered table rows are virtual projection rows, not direct DB rows.
|
||||
- A circuit may appear as compact row, summary+device rows, reserve row, or placeholder row.
|
||||
- Keyboard navigation and drag/drop targeting must use normalized visible rows, not raw tree nesting.
|
||||
*/
|
||||
const { projectId, circuitListId } = props;
|
||||
const [data, setData] = useState<CircuitTreeResponseDto | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// selectedCell is spreadsheet-style keyboard focus target within normalized grid.
|
||||
// It can exist without an active edit input.
|
||||
const [selectedCell, setSelectedCell] = useState<SelectedCell | null>(null);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
const [anchorRowKey, setAnchorRowKey] = useState<string | null>(null);
|
||||
// editingCell is mounted input session state (draft + mode + focus token).
|
||||
// It should only exist for currently editable grid cells.
|
||||
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
||||
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -478,6 +492,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [selectedProjectDeviceId, setSelectedProjectDeviceId] = useState<string | null>(null);
|
||||
const [targetSectionId, setTargetSectionId] = useState<string | null>(null);
|
||||
const [targetCircuitId, setTargetCircuitId] = useState<string | null>(null);
|
||||
// Drag intent states stay separated by domain intent to avoid cross-type accidental operations.
|
||||
// project-device drag => create/append linked rows, device-row drag => move rows,
|
||||
// circuit drag => reorder circuit blocks, column drag => layout-only changes.
|
||||
const [draggingProjectDeviceId, setDraggingProjectDeviceId] = useState<string | null>(null);
|
||||
const [dropIntent, setDropIntent] = useState<ProjectDeviceDropIntent | null>(null);
|
||||
const [draggingDeviceRowId, setDraggingDeviceRowId] = useState<string | null>(null);
|
||||
@@ -486,6 +503,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
||||
const [draggingCircuitIds, setDraggingCircuitIds] = useState<string[]>([]);
|
||||
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
||||
// Undo/redo history is session-local UI state (not persisted server-side).
|
||||
const [undoStack, setUndoStack] = useState<HistoryCommand[]>([]);
|
||||
const [redoStack, setRedoStack] = useState<HistoryCommand[]>([]);
|
||||
const [historyBusy, setHistoryBusy] = useState(false);
|
||||
@@ -539,10 +557,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return ["equipmentIdentifier" as CellKey, ...merged.filter((key) => key !== "equipmentIdentifier")];
|
||||
}
|
||||
|
||||
// Initial/identity-change tree load for current route context.
|
||||
useEffect(() => {
|
||||
void loadTree({ showLoading: true });
|
||||
}, [projectId, circuitListId]);
|
||||
|
||||
// Loads project-device palette used for sidebar drag/drop and quick inserts.
|
||||
useEffect(() => {
|
||||
async function loadProjectDeviceList() {
|
||||
try {
|
||||
@@ -609,6 +629,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
);
|
||||
|
||||
const distinctValuesByColumn = useMemo(() => {
|
||||
// Filter options are generated only for visible columns to match current header UI.
|
||||
// Hidden columns still exist in row model, but are intentionally not filterable from header.
|
||||
const result = {} as Record<CellKey, string[]>;
|
||||
for (const column of visibleColumns) {
|
||||
const set = new Set<string>();
|
||||
@@ -626,6 +648,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}, [data, visibleColumns]);
|
||||
|
||||
const filteredSortedSections = useMemo(() => {
|
||||
// Filtering and sorting are frontend view state. Backend sort order remains unchanged
|
||||
// until user explicitly applies sorted order.
|
||||
if (!data) {
|
||||
return [] as CircuitTreeResponseDto["sections"];
|
||||
}
|
||||
@@ -683,6 +707,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
.filter(Boolean) as CircuitTreeCircuitDto[];
|
||||
|
||||
if (sortState) {
|
||||
// Sort on circuit blocks so multi-device circuits keep row grouping integrity.
|
||||
circuits = [...circuits].sort((a, b) => {
|
||||
const cmp = compareSortValues(getBlockSortValue(a, sortState.key), getBlockSortValue(b, sortState.key));
|
||||
return sortState.direction === "asc" ? cmp : -cmp;
|
||||
@@ -695,6 +720,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return sections;
|
||||
}, [data, columnFilters, hasActiveFilters, sortState]);
|
||||
|
||||
// visibleRows is the single normalized grid used by render + navigation + editability.
|
||||
// This avoids selected/rendered/editable state drifting apart after filters/sorts/reloads.
|
||||
const visibleRows = useMemo(() => {
|
||||
if (!filteredSortedSections.length) {
|
||||
return [] as VisibleGridRow[];
|
||||
@@ -705,6 +732,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
rowKey: `section:${section.id}`,
|
||||
rowType: "section",
|
||||
sectionId: section.id,
|
||||
// Section headers are structural markers only: visible, but never editable/selectable.
|
||||
cells: allColumns.map((col) => ({ cellKey: col.key, editable: false, kind: "readonly", value: undefined })),
|
||||
});
|
||||
for (const circuit of section.circuits) {
|
||||
@@ -721,6 +749,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
rows.push(makeRow("deviceRow", section.id, circuit, device));
|
||||
}
|
||||
}
|
||||
// Placeholder row is virtual (-frei-) but intentionally part of editable grid because
|
||||
// editing/dropping here is the fast path to create a real circuit in this section.
|
||||
rows.push(makeRow("placeholder", section.id));
|
||||
}
|
||||
return rows;
|
||||
@@ -756,6 +786,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
circuit?: CircuitTreeCircuitDto,
|
||||
device?: CircuitTreeDeviceRowDto
|
||||
): VisibleGridRow {
|
||||
// Row keys are UI identities, not persisted identities. They are rebuilt on each tree refresh.
|
||||
const rowKey =
|
||||
rowType === "placeholder"
|
||||
? `placeholder:${sectionId}`
|
||||
@@ -764,6 +795,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
: `${rowType}:${circuit?.id ?? sectionId}`;
|
||||
const cells = allColumns.map((col) => {
|
||||
const kind = getCellKind(rowType, col.key);
|
||||
// Hidden columns remain in the normalized row data; only visibility controls rendering.
|
||||
const editable = kind === "circuitField" || kind === "deviceField";
|
||||
let value: string | number | boolean | undefined;
|
||||
if (rowType === "placeholder") {
|
||||
@@ -792,6 +824,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
[visibleRows]
|
||||
);
|
||||
|
||||
// Map each visible row to editable cells currently on screen.
|
||||
// This keeps Tab/arrow navigation aligned with hidden/reordered columns.
|
||||
const rowCellMap = useMemo(() => {
|
||||
const visibleKeySet = new Set(visibleColumnKeys);
|
||||
const map = new Map<string, CellKey[]>();
|
||||
@@ -868,6 +902,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setAnchorRowKey(anchor);
|
||||
}
|
||||
|
||||
// Applies cell/row selection semantics. Row selection is UI-only state and later interpreted
|
||||
// as either device-row batch selection or circuit batch selection depending on drag context.
|
||||
function handleRowSelectionClick(
|
||||
row: VisibleGridRow,
|
||||
cellKey: CellKey,
|
||||
@@ -897,6 +933,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}
|
||||
|
||||
// Captures semantic identity for post-reload focus restoration.
|
||||
function buildSelectionIntent(cell: SelectedCell): SelectionIntent | null {
|
||||
const row = findRow(cell.rowKey);
|
||||
if (!row) {
|
||||
@@ -912,6 +949,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
};
|
||||
}
|
||||
|
||||
// Resolves selection after writes with fallback chain:
|
||||
// exact cell -> same device -> same circuit rowType -> same circuit -> same section -> first editable.
|
||||
function resolveSelectionIntent(intent: SelectionIntent): SelectedCell | null {
|
||||
const direct = editableCells.find(
|
||||
(cell) => cell.rowKey === intent.rowKey && cell.cellKey === intent.cellKey
|
||||
@@ -958,6 +997,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return editableCells[0] ?? null;
|
||||
}
|
||||
|
||||
// Reload helper for write paths. Stores pending intent because row/cell keys can change after reload.
|
||||
async function reloadWithIntent(intent?: SelectionIntent | null) {
|
||||
if (intent) {
|
||||
pendingSelectionAfterReload.current = intent;
|
||||
@@ -968,12 +1008,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
// Runs a normal command and records it in session-local history.
|
||||
async function runCommand(command: HistoryCommand) {
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
const intent = await command.redo();
|
||||
await reloadWithIntent(intent ?? null);
|
||||
// Any new forward command invalidates redo history branch.
|
||||
setUndoStack((current) => [...current, command]);
|
||||
setRedoStack([]);
|
||||
} catch (err) {
|
||||
@@ -983,6 +1025,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
// Replays command in undo/redo mode. Commands encapsulate their own inverse logic.
|
||||
async function applyHistory(command: HistoryCommand, mode: "undo" | "redo") {
|
||||
try {
|
||||
setError(null);
|
||||
@@ -1005,6 +1048,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
// Undo command from session-local history stack.
|
||||
async function handleUndo() {
|
||||
if (historyBusy || isSaving || undoStack.length === 0) {
|
||||
return;
|
||||
@@ -1013,6 +1057,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await applyHistory(command, "undo");
|
||||
}
|
||||
|
||||
// Redo command from session-local history stack.
|
||||
async function handleRedo() {
|
||||
if (historyBusy || isSaving || redoStack.length === 0) {
|
||||
return;
|
||||
@@ -1021,6 +1066,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await applyHistory(command, "redo");
|
||||
}
|
||||
|
||||
// Persists currently sorted block order into section sortOrder values.
|
||||
async function handleApplySortedOrder() {
|
||||
if (!sortState || hasActiveFilters || !data) {
|
||||
return;
|
||||
@@ -1051,6 +1097,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
|
||||
// Sorting is view-only until explicitly applied; this avoids accidental persisted reorders.
|
||||
await runCommand({
|
||||
label: "Apply sorted order",
|
||||
redo: async () => {
|
||||
@@ -1070,6 +1117,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Toggles visible columns while keeping full column model intact for data/edit mapping.
|
||||
function toggleColumnVisibility(key: CellKey) {
|
||||
const column = allColumns.find((entry) => entry.key === key);
|
||||
if (!column || column.locked) {
|
||||
@@ -1109,6 +1157,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setOpenFilterColumn(null);
|
||||
}
|
||||
|
||||
// Column drag affects layout state only and never mutates circuit/device data.
|
||||
function moveColumnByDrag(dragKey: CellKey, targetKey: CellKey) {
|
||||
if (dragKey === "equipmentIdentifier" || targetKey === "equipmentIdentifier") {
|
||||
return;
|
||||
@@ -1167,6 +1216,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setColumnDropTargetKey(null);
|
||||
}
|
||||
|
||||
// Apply deferred selection after async reload only once visible grid is rebuilt.
|
||||
useEffect(() => {
|
||||
const intent = pendingSelectionAfterReload.current;
|
||||
if (!intent || !data) {
|
||||
@@ -1180,6 +1230,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}, [data, editableCells, visibleRows]);
|
||||
|
||||
// Restores multi-selected device rows after operations that rebuild row keys.
|
||||
useEffect(() => {
|
||||
const pending = pendingSelectedDeviceRowIdsAfterReload.current;
|
||||
if (!pending || pending.length === 0) {
|
||||
@@ -1198,6 +1249,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
pendingSelectedDeviceRowIdsAfterReload.current = null;
|
||||
}, [visibleRows]);
|
||||
|
||||
// Restores multi-selected circuits after reorder/delete/recreate operations.
|
||||
useEffect(() => {
|
||||
const pending = pendingSelectedCircuitIdsAfterReload.current;
|
||||
if (!pending || pending.length === 0) {
|
||||
@@ -1220,6 +1272,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
pendingSelectedCircuitIdsAfterReload.current = null;
|
||||
}, [visibleRows]);
|
||||
|
||||
// PendingFocus is used by toolbar actions that should immediately enter edit mode on a target cell.
|
||||
useEffect(() => {
|
||||
if (!pendingFocus) {
|
||||
return;
|
||||
@@ -1254,20 +1307,25 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}, [editingCell?.focusToken]);
|
||||
|
||||
// Finds row in normalized grid by virtual row key.
|
||||
function findRow(rowKey: string) {
|
||||
return visibleRows.find((row) => row.rowKey === rowKey);
|
||||
}
|
||||
|
||||
// Finds cell in normalized grid row.
|
||||
function findCell(rowKey: string, cellKey: CellKey) {
|
||||
const row = findRow(rowKey);
|
||||
return row?.cells.find((cell) => cell.cellKey === cellKey);
|
||||
}
|
||||
|
||||
// Opens edit session for selected cell.
|
||||
// Enter/F2/double-click use selectExisting; type-to-edit uses replaceWithTypedChar.
|
||||
function startEdit(cell: SelectedCell, mode: StartEditMode, typedChar?: string) {
|
||||
const visibleCell = findCell(cell.rowKey, cell.cellKey);
|
||||
if (!visibleCell || !visibleCell.editable) {
|
||||
return;
|
||||
}
|
||||
// Spreadsheet behavior: typing on a selected cell starts overwrite mode.
|
||||
const currentDisplay = mode === "replaceWithTypedChar" ? typedChar ?? "" : String(visibleCell.value ?? "");
|
||||
focusTokenRef.current += 1;
|
||||
setEditingCell({
|
||||
@@ -1278,6 +1336,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Horizontal keyboard navigation constrained to editable cells in current row.
|
||||
function moveHorizontal(direction: 1 | -1) {
|
||||
if (!selectedCell) {
|
||||
return;
|
||||
@@ -1291,6 +1350,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setSelectedCell({ rowKey: selectedCell.rowKey, cellKey: next });
|
||||
}
|
||||
|
||||
// Vertical keyboard navigation preserves nearest visible column when row cell sets differ.
|
||||
function moveVertical(direction: 1 | -1) {
|
||||
if (!selectedCell) {
|
||||
return;
|
||||
@@ -1322,6 +1382,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setSelectedCell({ rowKey: targetRowKey, cellKey: best });
|
||||
}
|
||||
|
||||
// Maps generic grid cell edits to circuit PATCH payload fields.
|
||||
async function patchCircuit(circuitId: string, key: CellKey, draft: string) {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (key === "protectionSummary" || key === "cableSummary") {
|
||||
@@ -1337,6 +1398,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await updateCircuitById(circuitId, payload);
|
||||
}
|
||||
|
||||
// Maps generic grid cell edits to device-row PATCH payload fields.
|
||||
async function patchDeviceRow(rowId: string, key: CellKey, draft: string) {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (["quantity", "powerPerUnit", "simultaneityFactor", "cosPhi"].includes(key)) {
|
||||
@@ -1362,6 +1424,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await updateCircuitDeviceRowById(rowId, payload);
|
||||
}
|
||||
|
||||
// Creates a real circuit from virtual placeholder row.
|
||||
// Device-field edits create circuit + first manual row; circuit-field edits patch circuit directly.
|
||||
async function createFromPlaceholder(
|
||||
sectionId: string,
|
||||
key: CellKey,
|
||||
@@ -1401,6 +1465,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
||||
}
|
||||
|
||||
// Reads current persisted value into editable draft text for reversible edit commands.
|
||||
function getCurrentDraftForCell(row: VisibleGridRow, cellKey: CellKey, kind: CellKind) {
|
||||
if (kind === "deviceField" && row.device) {
|
||||
return String(getDeviceValue(row.device, cellKey) ?? "");
|
||||
@@ -1411,6 +1476,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return "";
|
||||
}
|
||||
|
||||
// Commits the active editor input through command history so edits remain undoable.
|
||||
async function commitEdit(direction: SaveDirection = "stay") {
|
||||
if (!editingCell) {
|
||||
return;
|
||||
@@ -1438,6 +1504,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return buildSelectionIntent(selected);
|
||||
};
|
||||
|
||||
// Placeholder rows are not persisted entities; editing them materializes a real circuit first.
|
||||
if (row.rowType === "placeholder") {
|
||||
let createdCircuitId: string | null = null;
|
||||
const command: HistoryCommand = {
|
||||
@@ -1535,6 +1602,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
// Cancels current edit session and restores grid keyboard focus.
|
||||
function cancelEdit() {
|
||||
if (!editingCell) {
|
||||
return;
|
||||
@@ -1906,6 +1974,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
}
|
||||
|
||||
// Converts current row selection to device-row ids for bulk row moves.
|
||||
// Section and placeholder rows are intentionally ignored.
|
||||
function getSelectedEligibleDeviceRowIds(primaryRowId?: string | null) {
|
||||
const selectedSet = new Set(selectedRowKeys);
|
||||
const ids: string[] = [];
|
||||
@@ -1923,6 +1993,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return primaryRowId ? [primaryRowId] : ids;
|
||||
}
|
||||
|
||||
// Converts current row selection to unique circuit ids for circuit block reorder.
|
||||
function getSelectedEligibleCircuitIds(primaryCircuitId?: string | null) {
|
||||
const selectedSet = new Set(selectedRowKeys);
|
||||
const ids: string[] = [];
|
||||
@@ -1955,6 +2026,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
}
|
||||
|
||||
// Applies same-section circuit reorder as explicit id ordering.
|
||||
// No implicit renumbering is performed here.
|
||||
async function applyCircuitReorder(intent: CircuitReorderDropIntent, sourceCircuitIds: string[]) {
|
||||
const section = data?.sections.find((entry) => entry.id === intent.sectionId);
|
||||
if (!section) {
|
||||
@@ -1981,6 +2054,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await reorderSectionCircuits(section.id, nextIds);
|
||||
}
|
||||
|
||||
// Handles project-device drag intent. This path creates linked rows/circuits and
|
||||
// must remain separate from move handlers that operate on existing entities.
|
||||
async function handleDropWithIntent(event: DragEvent<HTMLElement>, intent: ProjectDeviceDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1998,6 +2073,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setError("Invalid project device drop source.");
|
||||
return;
|
||||
}
|
||||
// Project-device drop logic is isolated from row/circuit move logic because
|
||||
// it can create new rows/circuits instead of moving existing ones.
|
||||
if (intent.kind === "new-circuit") {
|
||||
let createdCircuitId: string | null = null;
|
||||
await runCommand({
|
||||
@@ -2047,6 +2124,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Handles device-row drag intent (single or bulk) and preserves enough source grouping
|
||||
// for deterministic undo back to original circuits.
|
||||
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -2077,6 +2156,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
|
||||
// Device-row drag supports bulk selection, but target intent is always explicit:
|
||||
// move into existing circuit or create new circuit in a target section.
|
||||
if (intent.kind === "move-to-circuit") {
|
||||
if (rowIds.length === 1 && intent.circuitId === sourceCircuitId) {
|
||||
return;
|
||||
@@ -2141,6 +2222,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Handles circuit drag intent and reorders whole circuit blocks within one section only.
|
||||
async function handleCircuitReorderDrop(event: DragEvent<HTMLElement>, intent: CircuitReorderDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -2197,6 +2279,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Delete undo recreates the row from captured snapshot because delete is destructive.
|
||||
async function handleDeleteDevice(rowId: string) {
|
||||
if (!confirm("Delete this device row?")) {
|
||||
return;
|
||||
@@ -2216,6 +2299,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
},
|
||||
undo: async () => {
|
||||
// Recreate instead of "undelete": backend ids are immutable once deleted.
|
||||
const created = (await createCircuitDeviceRow(sourceCircuitId, {
|
||||
linkedProjectDeviceId: rowSnapshot.linkedProjectDeviceId,
|
||||
name: rowSnapshot.name,
|
||||
@@ -2242,6 +2326,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Deletes a circuit and all child rows; undo recreates from captured snapshot.
|
||||
async function handleDeleteCircuit(circuitId: string) {
|
||||
if (!confirm("Delete this circuit and all assigned device rows?")) {
|
||||
return;
|
||||
@@ -2266,6 +2351,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Explicit renumber action. Undo restores previous BMKs via safe section identifier update.
|
||||
async function handleRenumberSection(sectionId: string) {
|
||||
if (!confirm("Renumber this section? Only circuits in this section will change.")) {
|
||||
return;
|
||||
@@ -2285,6 +2371,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
},
|
||||
undo: async () => {
|
||||
// Restore prior BMKs through safe bulk endpoint to avoid transient unique collisions.
|
||||
await updateSectionEquipmentIdentifiers(
|
||||
sectionId,
|
||||
before.map((entry) => ({
|
||||
@@ -2297,6 +2384,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
// Keeps container-level keyboard focus model valid when focus returns from toolbar/popovers.
|
||||
function handleContainerFocus() {
|
||||
if (editingCell) {
|
||||
const row = findRow(editingCell.rowKey);
|
||||
@@ -2311,6 +2399,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
// Spreadsheet-like keyboard dispatcher:
|
||||
// - Enter/F2 starts edit with current value selected
|
||||
// - printable key starts overwrite edit (type-to-edit)
|
||||
// - Arrow/Tab move selectedCell when not editing
|
||||
function handleContainerKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
||||
if (editingCell) {
|
||||
if (event.key === "Tab") {
|
||||
@@ -2389,6 +2481,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
if (isPrintableKey(event) && selectedCell) {
|
||||
// First printable key should become draft content directly; do not pre-select old text.
|
||||
event.preventDefault();
|
||||
startEdit(selectedCell, "replaceWithTypedChar", event.key);
|
||||
}
|
||||
@@ -2477,6 +2570,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
|
||||
const isSortedView = Boolean(sortState);
|
||||
// Renumbering is intentionally blocked while sort/filter overlays are active so users
|
||||
// cannot renumber against a transformed view that has not been persisted yet.
|
||||
const hasActiveSortOrFilter = isSortedView || hasActiveFilters;
|
||||
const draggingDeviceCount = draggingDeviceRowIds.length > 0 ? draggingDeviceRowIds.length : draggingDeviceRowId ? 1 : 0;
|
||||
const activeDraggedCircuitIds =
|
||||
|
||||
Reference in New Issue
Block a user