feat(bases): render editable property rows inside detail modal

This commit is contained in:
Philipinho
2026-05-24 16:10:31 +01:00
parent 9237e94769
commit f5602e9bcf
4 changed files with 128 additions and 49 deletions
@@ -0,0 +1,56 @@
import { IBaseProperty } from "@/features/base/types/base.types";
import { CellText } from "./cell-text";
import { CellNumber } from "./cell-number";
import { CellSelect } from "./cell-select";
import { CellStatus } from "./cell-status";
import { CellMultiSelect } from "./cell-multi-select";
import { CellDate } from "./cell-date";
import { CellCheckbox } from "./cell-checkbox";
import { CellUrl } from "./cell-url";
import { CellEmail } from "./cell-email";
import { CellPerson } from "./cell-person";
import { CellFile } from "./cell-file";
import { CellPage } from "./cell-page";
import { CellCreatedAt } from "./cell-created-at";
import { CellLastEditedAt } from "./cell-last-edited-at";
import { CellLastEditedBy } from "./cell-last-edited-by";
import { CellFormula } from "./cell-formula";
export type CellComponentProps = {
value: unknown;
property: IBaseProperty;
rowId: string;
isEditing: boolean;
onCommit: (value: unknown) => void;
onCancel: () => void;
};
export const cellComponents: Record<
string,
React.ComponentType<CellComponentProps>
> = {
text: CellText,
number: CellNumber,
select: CellSelect,
status: CellStatus,
multiSelect: CellMultiSelect,
date: CellDate,
checkbox: CellCheckbox,
url: CellUrl,
email: CellEmail,
person: CellPerson,
file: CellFile,
page: CellPage,
createdAt: CellCreatedAt,
lastEditedAt: CellLastEditedAt,
lastEditedBy: CellLastEditedBy,
formula: CellFormula,
};
type CellRendererProps = CellComponentProps;
export function CellRenderer(props: CellRendererProps) {
const Component = cellComponents[props.property.type];
if (!Component) return null;
return <Component {...props} />;
}
@@ -1,59 +1,13 @@
import { memo, useCallback } from "react";
import { Cell } from "@tanstack/react-table";
import { useAtom } from "jotai";
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
import { IBaseRow, EditingCell } from "@/features/base/types/base.types";
import { editingCellAtomFamily } from "@/features/base/atoms/base-atoms";
import { isSystemPropertyType } from "@/features/base/hooks/use-base-table";
import { CellText } from "@/features/base/components/cells/cell-text";
import { CellNumber } from "@/features/base/components/cells/cell-number";
import { CellSelect } from "@/features/base/components/cells/cell-select";
import { CellStatus } from "@/features/base/components/cells/cell-status";
import { CellMultiSelect } from "@/features/base/components/cells/cell-multi-select";
import { CellDate } from "@/features/base/components/cells/cell-date";
import { CellCheckbox } from "@/features/base/components/cells/cell-checkbox";
import { CellUrl } from "@/features/base/components/cells/cell-url";
import { CellEmail } from "@/features/base/components/cells/cell-email";
import { CellPerson } from "@/features/base/components/cells/cell-person";
import { CellFile } from "@/features/base/components/cells/cell-file";
import { CellPage } from "@/features/base/components/cells/cell-page";
import { CellCreatedAt } from "@/features/base/components/cells/cell-created-at";
import { CellLastEditedAt } from "@/features/base/components/cells/cell-last-edited-at";
import { CellLastEditedBy } from "@/features/base/components/cells/cell-last-edited-by";
import { CellFormula } from "@/features/base/components/cells/cell-formula";
import { cellComponents } from "@/features/base/components/cells/cell-renderer";
import { RowNumberCell } from "./row-number-cell";
import classes from "@/features/base/styles/grid.module.css";
type CellComponentProps = {
value: unknown;
property: IBaseProperty;
rowId: string;
isEditing: boolean;
onCommit: (value: unknown) => void;
onCancel: () => void;
};
const cellComponents: Record<
string,
React.ComponentType<CellComponentProps>
> = {
text: CellText,
number: CellNumber,
select: CellSelect,
status: CellStatus,
multiSelect: CellMultiSelect,
date: CellDate,
checkbox: CellCheckbox,
url: CellUrl,
email: CellEmail,
person: CellPerson,
file: CellFile,
page: CellPage,
createdAt: CellCreatedAt,
lastEditedAt: CellLastEditedAt,
lastEditedBy: CellLastEditedBy,
formula: CellFormula,
};
type RowDragProps = {
draggable: boolean;
onDragStart: (e: React.DragEvent) => void;
@@ -0,0 +1,53 @@
import { useState, useCallback } from "react";
import { Group, Text } from "@mantine/core";
import { IBaseProperty, IBaseRow } from "@/features/base/types/base.types";
import { CellRenderer } from "@/features/base/components/cells/cell-renderer";
type PropertyRowProps = {
property: IBaseProperty;
row: IBaseRow;
onUpdate: (propertyId: string, value: unknown) => void;
};
export function PropertyRow({ property, row, onUpdate }: PropertyRowProps) {
const value = (row.cells ?? {})[property.id];
// The cell components key their edit state off `isEditing`. In the
// modal we treat the cell as always-active: click to commit a new
// value, blur/escape to commit-or-cancel the same way the grid does.
const [editing, setEditing] = useState(false);
const handleCommit = useCallback(
(next: unknown) => {
setEditing(false);
onUpdate(property.id, next);
},
[onUpdate, property.id],
);
const handleCancel = useCallback(() => setEditing(false), []);
return (
<Group
align="flex-start"
wrap="nowrap"
gap="md"
onClick={() => setEditing(true)}
style={{ cursor: "text" }}
>
<div style={{ width: 140, flex: "0 0 140px", paddingTop: 6 }}>
<Text size="sm" c="dimmed">
{property.name}
</Text>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<CellRenderer
property={property}
rowId={row.id}
value={value}
isEditing={editing}
onCommit={handleCommit}
onCancel={handleCancel}
/>
</div>
</Group>
);
}
@@ -7,6 +7,7 @@ import {
} from "@/features/base/types/base.types";
import { useUpdateRowMutation } from "@/features/base/queries/base-row-query";
import { RowDetailTitle } from "./row-detail-title";
import { PropertyRow } from "./property-row";
type RowDetailModalProps = {
base: IBase;
@@ -63,7 +64,22 @@ export function RowDetailModal({
});
}}
/>
{/* Property list goes here in Task 19 */}
{base.properties
.filter((p) => !p.isPrimary)
.map((property) => (
<PropertyRow
key={property.id}
property={property}
row={row}
onUpdate={(propertyId, value) => {
updateRowMutation.mutate({
rowId: row.id,
pageId: base.id,
cells: { [propertyId]: value },
});
}}
/>
))}
{/* Add-property button goes here in Task 20 */}
</Stack>
) : (