seti-ramesesv1
Version:
Reusable components and context for Next.js apps
262 lines (259 loc) • 12 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState, useEffect } from 'react';
import styles from '../../../styles/CompassEditor.module.css.js';
import RowItem from './CompassEditorRowItem.js';
import Pencil from '../../../node_modules/lucide-react/dist/esm/icons/pencil.js';
import Copy from '../../../node_modules/lucide-react/dist/esm/icons/copy.js';
import Trash from '../../../node_modules/lucide-react/dist/esm/icons/trash.js';
// Simple function to generate MongoDB-like ObjectId
const generateObjectId = () => Math.floor(Math.random() * 0xffffffff)
.toString(16)
.padStart(8, "0") +
Math.floor(Math.random() * 0xffffffff)
.toString(16)
.padStart(8, "0") +
Math.floor(Math.random() * 0xffffffff)
.toString(16)
.padStart(8, "0");
const CompassEditor = ({ data, onGetJson, }) => {
const [editMode, setEditMode] = useState(false);
const [rows, setRows] = useState([]);
const [previousRows, setPreviousRows] = useState([]);
const [expanded, setExpanded] = useState(new Map());
const [error, setError] = useState(null);
const convertToRows = (inputData, parentId) => {
const rows = [];
let currentId = parentId || Date.now();
if (!parentId && !inputData.hasOwnProperty("_id")) {
const id = currentId++;
rows.push({
id,
keyName: "_id",
value: generateObjectId(),
dataType: "string",
keyWidth: 50,
valWidth: 200,
});
}
for (const [key, value] of Object.entries(inputData)) {
const id = currentId++;
let dataType = typeof value === "object"
? "object"
: typeof value === "boolean"
? "boolean"
: typeof value === "number"
? "number"
: "string";
let rowValue = value.toString();
if (dataType === "object" && value !== null) {
const children = convertToRows(value, currentId);
currentId += children.length;
rows.push({ id, keyName: key, value: "Object", dataType, keyWidth: 50, valWidth: 200, children });
}
else {
rows.push({ id, keyName: key, value: rowValue, dataType, keyWidth: 50, valWidth: 50 });
}
}
return rows;
};
useEffect(() => {
const initialRows = data
? convertToRows(data)
: [
{
id: Date.now(),
keyName: "_id",
value: generateObjectId(),
dataType: "string",
keyWidth: 50,
valWidth: 200,
},
{
id: Date.now() + 1,
keyName: "",
value: "",
dataType: "string",
keyWidth: 50,
valWidth: 50,
},
];
setRows(initialRows);
setPreviousRows([...initialRows]);
// Initialize expanded state for all parent rows
const initialExpanded = new Map();
initialRows.forEach((row) => {
if (row.children && row.children.length > 0) {
initialExpanded.set(row.id, true); // Expand by default
}
});
setExpanded(initialExpanded);
}, [data]);
const handleUpdate = () => {
// ✅ Validation
const invalidRows = findInvalidRows(rows);
if (invalidRows.length > 0) {
setError("Unable to update, key and value cannot be empty.");
return;
}
setError(null);
// ✅ Build cleanData directly from rows in current UI order, including parents with children
const cleanData = rows.reduce((obj, r) => {
if (r.keyName.trim()) {
// Include parent rows if they have a key, even if value is "Object"
const convertValue = (row) => {
let val = row.value;
if (row.dataType === "number")
val = Number(row.value);
if (row.dataType === "boolean" && typeof row.value === "string")
val = row.value.toLowerCase() === "true";
if (row.dataType === "object" && row.children) {
// Recursively process children in their current order
val = row.children.reduce((childObj, child) => {
if (child.keyName.trim() && child.value.trim()) {
childObj[child.keyName.trim()] = convertValue(child);
}
return childObj;
}, {});
}
return val;
};
// Include the row if it has a key, and use convertValue to handle children
obj[r.keyName.trim()] = convertValue(r);
}
return obj;
}, {});
onGetJson?.(cleanData);
setPreviousRows([...rows]); // Save current state as new baseline
setEditMode(false);
};
const handleAddRow = (id, isParentLevel = false) => {
const newRow = { id: Date.now(), keyName: "", value: "", dataType: "string", keyWidth: 50, valWidth: 50 };
const insertRow = (rows, targetId, isParent, parentRows = rows) => {
if (isParent) {
const index = rows.findIndex((r) => r.id === targetId);
if (index !== -1) {
return [...rows.slice(0, index + 1), newRow, ...rows.slice(index + 1)];
}
return rows;
}
return rows.reduce((acc, r, index) => {
if (r.id === targetId) {
// Add a new row as a sibling immediately below the target row
return [...acc, r, newRow];
}
else if (r.children && r.children.length > 0) {
return [...acc, { ...r, children: insertRow(r.children, targetId, false, r.children) }];
}
return [...acc, r];
}, []);
};
setRows((prev) => {
const parentRow = findParentRow(prev, id);
return insertRow(prev, id, isParentLevel, parentRow ? parentRow.children || prev : prev);
});
};
const findParentRow = (rows, targetId) => {
for (const r of rows) {
if (r.children && r.children.some((child) => child.id === targetId)) {
return r;
}
if (r.children && r.children.length > 0) {
const parent = findParentRow(r.children, targetId);
if (parent)
return parent;
}
}
return undefined;
};
const findInvalidRows = (rows) => {
let invalid = [];
for (const r of rows) {
if (!r.keyName.trim() || (!r.children && !r.value.trim())) {
invalid.push(r);
}
if (r.children && r.children.length > 0) {
invalid = invalid.concat(findInvalidRows(r.children));
}
}
return invalid;
};
const deleteRow = (rows, targetId) => {
return rows.reduce((acc, r) => {
if (r.id === targetId) {
return acc; // Skip this row (delete it)
}
else if (r.children && r.children.length > 0) {
const updatedChildren = deleteRow(r.children, targetId);
if (updatedChildren.length === 0) {
updatedChildren.push({
id: Date.now(),
keyName: "",
value: "",
dataType: "string",
keyWidth: 50,
valWidth: 50,
});
}
return [...acc, { ...r, children: updatedChildren }];
}
return [...acc, r];
}, []);
};
const handleDeleteRow = (id) => {
setRows((prev) => {
const updatedRows = deleteRow(prev, id);
if (updatedRows.length === 1 && updatedRows[0].keyName === "_id") {
updatedRows.push({ id: Date.now(), keyName: "", value: "", dataType: "string", keyWidth: 50, valWidth: 50 });
}
return updatedRows;
});
};
const updateRow = (id, field, value, parentRows = rows) => {
return parentRows.map((r) => {
if (r.id === id) {
const updated = { ...r, [field]: value };
if (field === "dataType" && value === "object" && (!r.children || r.children.length === 0)) {
updated.children = [
{ id: Date.now() + 1, keyName: "", value: "", dataType: "string", keyWidth: 50, valWidth: 50 },
];
}
return updated;
}
if (r.children && r.children.length > 0) {
return { ...r, children: updateRow(id, field, value, r.children) };
}
return r;
});
};
const handleUpdateRow = (id, field, value) => {
setRows((prev) => updateRow(id, field, value, prev));
};
const isInvalid = (value, type) => {
if (value.trim() === "")
return false;
if (type === "number")
return !/^-?\d+$/.test(value.trim());
if (type === "boolean")
return value.toLowerCase() !== "true" && value.toLowerCase() !== "false";
return false;
};
const toggleExpand = (id) => {
setExpanded((prev) => {
const newExpanded = new Map(prev);
newExpanded.set(id, !prev.get(id));
return newExpanded;
});
};
const renderRows = (rows, depth = 0, counter = { value: 1 }) => rows.map((row) => (jsx(RowItem, { row: row, depth: depth, number: counter.value++, editMode: editMode, expanded: expanded, onToggleExpand: toggleExpand, onUpdateRow: handleUpdateRow, onAddRow: handleAddRow, onDeleteRow: handleDeleteRow, isInvalid: isInvalid, renderChildren: (children, d) => renderRows(children, d, counter) }, row.id)));
const handleCancel = () => {
setRows([...previousRows]); // Revert to the previous state
setEditMode(false);
setError(null);
};
return (jsxs("div", { className: styles.container, children: [jsx("div", { className: styles.actionWrapper, children: !editMode && (jsxs("div", { className: styles.actionGroup, children: [jsx("button", { onClick: () => {
setPreviousRows([...rows]);
setEditMode(true);
}, className: styles.iconButton, children: jsx(Pencil, { size: 16 }) }), jsx("button", { onClick: () => console.log("Copy clicked"), className: styles.iconButton, children: jsx(Copy, { size: 16 }) }), jsx("button", { onClick: () => console.log("Delete clicked"), className: styles.iconButton, children: jsx(Trash, { size: 16 }) })] })) }), renderRows(rows), jsxs("div", { className: `${styles.footer} ${error ? styles.footerError : ""}`, children: [error && jsx("div", { className: styles.errorText, children: error }), editMode && (jsxs("div", { className: styles.footerButtons, children: [jsx("button", { onClick: handleCancel, className: styles.cancelBtn, children: "Cancel" }), jsx("button", { onClick: handleUpdate, disabled: rows.some((r) => isInvalid(r.value, r.dataType)), className: `${styles.updateBtn} ${rows.some((r) => isInvalid(r.value, r.dataType)) ? styles.updateBtnDisabled : styles.updateBtnActive}`, children: "Update" })] }))] })] }));
};
export { CompassEditor as default };
//# sourceMappingURL=CompassEditor.js.map