feat: working Insert Row + smart cell editors
Insert Row now stages an editable pending row at the top of the data grid (with an accent outline) instead of an invisible all-null insert that failed on commit. Committing inserts only the filled columns so serial/identity/generated defaults apply; empty-column inserts fall back to DEFAULT VALUES. Cell editors are now type-aware: date/time/datetime pickers, a boolean select, and number inputs for the matching column types (enums and FKs already had dropdowns). Applies to both new rows and inline editing. Datetime values commit in the canonical space-separated format accepted by both PostgreSQL and MySQL.
This commit is contained in:
@@ -231,6 +231,7 @@ export function DbViewerScreen({
|
||||
const changesQueue = useDbViewerStore((s) => s.changesQueue);
|
||||
const tables = useDbViewerStore((s) => s.tables);
|
||||
const stageCellEdit = useDbViewerStore((s) => s.stageCellEdit);
|
||||
const updateChange = useDbViewerStore((s) => s.updateChange);
|
||||
|
||||
const isRunning = activeTab?.tabType === "query" && !!activeTab?.loading;
|
||||
|
||||
@@ -339,6 +340,19 @@ export function DbViewerScreen({
|
||||
[activeTab, stageCellEdit],
|
||||
);
|
||||
|
||||
const handleStageInsertCell = useCallback(
|
||||
(changeId: string, column: string, value: string | null) => {
|
||||
const change = useDbViewerStore
|
||||
.getState()
|
||||
.changesQueue.find((c) => c.id === changeId);
|
||||
if (!change) return;
|
||||
const newData = { ...(change.newData ?? {}) };
|
||||
newData[column] = value;
|
||||
updateChange(changeId, { newData });
|
||||
},
|
||||
[updateChange],
|
||||
);
|
||||
|
||||
const handleOpenRowDetail = useCallback((rowIndex: number) => {
|
||||
setRowDetailIdx(rowIndex);
|
||||
}, []);
|
||||
@@ -832,6 +846,24 @@ export function DbViewerScreen({
|
||||
// Data is already filtered and sorted server-side; no client-side transform needed.
|
||||
const processedRows = rawRows;
|
||||
|
||||
// Pending insert rows (from the changes queue) are prepended to the grid as
|
||||
// editable rows. Only pending inserts for the active table tab count.
|
||||
const pendingInserts =
|
||||
activeTab?.tabType === "table"
|
||||
? changesQueue.filter(
|
||||
(c) =>
|
||||
c.type === "insert" &&
|
||||
c.status === "pending" &&
|
||||
c.schema === activeTab.schema &&
|
||||
c.table === activeTab.table,
|
||||
)
|
||||
: [];
|
||||
const pendingInsertChangeIds = pendingInserts.map((c) => c.id);
|
||||
const pendingInsertRows = pendingInserts.map((c) =>
|
||||
columns.map((col) => c.newData?.[col.name] ?? null),
|
||||
);
|
||||
const displayRows = [...pendingInsertRows, ...processedRows];
|
||||
|
||||
const onPanelResizeStart = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -1184,7 +1216,7 @@ const onQueriesPanelResizeStart = useCallback(
|
||||
connectionId={connectionId}
|
||||
schema={activeSchema}
|
||||
table={activeTable}
|
||||
rows={processedRows}
|
||||
rows={displayRows}
|
||||
columns={columns}
|
||||
hiddenColumns={hiddenColumns}
|
||||
selectedRows={selectedRows}
|
||||
@@ -1192,6 +1224,7 @@ const onQueriesPanelResizeStart = useCallback(
|
||||
tabType={activeTab?.tabType ?? "table"}
|
||||
getLocator={getLocator}
|
||||
onStageEdit={readOnlyTable ? undefined : handleStageEdit}
|
||||
onStageInsertCell={readOnlyTable ? undefined : handleStageInsertCell}
|
||||
onOpenRowDetail={handleOpenRowDetail}
|
||||
readOnly={readOnlyTable}
|
||||
enumValues={editorOptions?.enums}
|
||||
@@ -1199,6 +1232,7 @@ const onQueriesPanelResizeStart = useCallback(
|
||||
fkPlaceholders={editorOptions?.fkPlaceholders}
|
||||
stagedValues={stagedValues}
|
||||
pendingKeys={pendingKeys}
|
||||
pendingInsertChangeIds={pendingInsertChangeIds}
|
||||
onToggleRow={(rowIndex) => {
|
||||
setSelectedRows(
|
||||
(prev) => {
|
||||
@@ -1348,7 +1382,7 @@ const onQueriesPanelResizeStart = useCallback(
|
||||
connectionId={connectionId}
|
||||
schema={activeSchema}
|
||||
table={activeTable}
|
||||
rows={processedRows}
|
||||
rows={displayRows}
|
||||
columns={columns}
|
||||
hiddenColumns={hiddenColumns}
|
||||
selectedRows={selectedRows}
|
||||
@@ -1356,6 +1390,7 @@ const onQueriesPanelResizeStart = useCallback(
|
||||
tabType={activeTab?.tabType ?? "table"}
|
||||
getLocator={getLocator}
|
||||
onStageEdit={readOnlyTable ? undefined : handleStageEdit}
|
||||
onStageInsertCell={readOnlyTable ? undefined : handleStageInsertCell}
|
||||
onOpenRowDetail={handleOpenRowDetail}
|
||||
readOnly={readOnlyTable}
|
||||
enumValues={editorOptions?.enums}
|
||||
@@ -1363,6 +1398,7 @@ const onQueriesPanelResizeStart = useCallback(
|
||||
fkPlaceholders={editorOptions?.fkPlaceholders}
|
||||
stagedValues={stagedValues}
|
||||
pendingKeys={pendingKeys}
|
||||
pendingInsertChangeIds={pendingInsertChangeIds}
|
||||
onToggleRow={(rowIndex) => {
|
||||
setSelectedRows((prev) => {
|
||||
const next = new Set(
|
||||
|
||||
@@ -325,6 +325,21 @@ describe("TableControls", () => {
|
||||
expect(screen.queryByLabelText(/insert row/i)).toBeNull();
|
||||
});
|
||||
|
||||
it("stages an insert with only editable columns when Insert Row is clicked", () => {
|
||||
const cols = [
|
||||
{ name: "id", data_type: "integer", is_nullable: false, is_pk: true, is_fk: false, fk_ref: null, default_value: "nextval('users_id_seq')", editable: false, is_generated: false },
|
||||
{ name: "name", data_type: "text", is_nullable: false, is_pk: false, is_fk: false, fk_ref: null, default_value: null, editable: true, is_generated: false },
|
||||
{ name: "created_at", data_type: "timestamp", is_nullable: true, is_pk: false, is_fk: false, fk_ref: null, default_value: "now()", editable: true, is_generated: false },
|
||||
];
|
||||
seed([makeTab({ data: { columns: cols, rows: [], total_rows: 0, page: 1, page_size: 50 } })], "tab-1");
|
||||
renderControls({ columns: cols, rows: [] });
|
||||
fireEvent.click(screen.getByLabelText(/insert row/i));
|
||||
const queue = useDbViewerStore.getState().changesQueue;
|
||||
expect(queue).toHaveLength(1);
|
||||
expect(queue[0].type).toBe("insert");
|
||||
expect(queue[0].newData).toEqual({ name: null, created_at: null });
|
||||
});
|
||||
|
||||
it("renders the column filter popover with the + Add filter action", () => {
|
||||
seed([makeTab()], "tab-1");
|
||||
renderControls();
|
||||
|
||||
@@ -547,8 +547,14 @@ export function TableControls({
|
||||
};
|
||||
|
||||
const handleInsertRow = () => {
|
||||
// Build the insert template from editable columns only. PK / generated /
|
||||
// identity columns are omitted so their defaults (serial, identity,
|
||||
// generated) apply on commit — an explicit NULL would bypass them and
|
||||
// fail with a NOT NULL violation.
|
||||
const newData: Record<string, unknown> = {};
|
||||
columns.forEach((c) => { newData[c.name] = null; });
|
||||
columns.forEach((c) => {
|
||||
if (c.editable && !c.is_pk && !c.is_generated) newData[c.name] = null;
|
||||
});
|
||||
addChange({
|
||||
type: "insert",
|
||||
schema,
|
||||
|
||||
Reference in New Issue
Block a user