Files
probo/apps/console/src/components/table/EditableTable.tsx
Émile Ré dd23449cc0 Fix apps/console lint issues
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-01-22 12:51:43 +04:00

168 lines
4.9 KiB
TypeScript

import {
SortableCellHead,
SortableDataTable,
} from "/components/table/SortableDataTable.tsx";
import type { GraphQLTaggedNode, OperationType } from "relay-runtime";
import type { KeyType, KeyTypeData } from "react-relay/relay-hooks/helpers";
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
import {
Button,
Cell,
CellHead,
EditableRow,
IconCheckmark1,
Row,
RowButton,
Spinner,
} from "@probo/ui";
import { z } from "zod";
import { useMutateField } from "/hooks/useMutateField.tsx";
import { type ReactNode } from "react";
import { useToggle } from "@probo/hooks";
import { useStateWithSchema } from "/hooks/useStateWithSchema.ts";
import clsx from "clsx";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts.ts";
import { defaultPageSize } from "/components/table/SortableDataTable.tsx";
type ColumnDefinition = { label: string; field: string } | string;
type EditableTableRowProps<T, S extends z.ZodSchema> = {
item?: T;
onUpdate: (key: keyof z.infer<S>, value: z.infer<S>[typeof key]) => void;
errors: Record<string, string>;
};
/**
* A "all-in-one" component to create a table with editable cells.
*/
export function EditableTable<
T extends { id: string },
S extends z.ZodSchema,
>(props: {
// Schema to create a new item
schema: S;
// GraphQL related props
connectionId: string;
pagination: usePaginationFragmentHookType<
OperationType,
KeyType,
KeyTypeData<KeyType>
>;
updateMutation: GraphQLTaggedNode;
createMutation: GraphQLTaggedNode;
items: T[];
// List of the columns
columns: ColumnDefinition[];
// Render a row for each item and to create a new item
row: (props: EditableTableRowProps<T, S>) => ReactNode;
// Render the content of the last cell
action: (props: { item: T }) => ReactNode;
// Label used when adding a new item
addLabel: string;
// Default value used when creating a new item
defaultValue: z.infer<S>;
pageSize?: number;
}) {
const { update } = useMutateField(props.updateMutation);
const [showAdd, toggleAdd] = useToggle(false);
return (
<SortableDataTable
columns={[...props.columns.map(() => "minmax(min-content, 1fr)"), "56px"]}
refetch={props.pagination.refetch}
hasNext={props.pagination.hasNext}
isLoadingNext={props.pagination.isLoadingNext}
loadNext={props.pagination.loadNext}
pageSize={props.pageSize ?? defaultPageSize}
>
<Row>
{props.columns.map((column, index) => (
<EditableTableHead column={column} key={index} />
))}
<CellHead />
</Row>
{props.items.map(item => (
<EditableRow onUpdate={(k, v) => update(item.id, k, v)} key={item.id}>
{props.row({
item,
onUpdate: (key, value) => update(item.id, key as string, value),
errors: {},
})}
<Cell>{props.action({ item })}</Cell>
</EditableRow>
))}
{showAdd
? (
<NewItemRow
schema={props.schema}
defaultValue={props.defaultValue}
connectionId={props.connectionId}
row={props.row}
mutation={props.createMutation}
onSuccess={toggleAdd}
/>
)
: (
<RowButton onClick={toggleAdd} type="button">{props.addLabel}</RowButton>
)}
</SortableDataTable>
);
}
function NewItemRow<T extends { id: string }, S extends z.ZodSchema>(props: {
schema: S;
defaultValue: z.infer<S>;
connectionId: string;
mutation: GraphQLTaggedNode;
onSuccess: () => void;
row: (props: EditableTableRowProps<T, S>) => ReactNode;
}) {
const { update, errors, value } = useStateWithSchema(
props.schema,
props.defaultValue,
);
const [mutate, isMutating] = useMutationWithToasts(props.mutation);
const isOk = Object.keys(errors ?? {}).length === 0;
const onSubmit = async () => {
// This should never happen, but we don't want to send bad data
if (!isOk) {
alert("Please fix the errors before submitting.");
return;
}
await mutate({
variables: {
input: value,
connections: [props.connectionId],
},
onSuccess: props.onSuccess,
});
};
return (
<EditableRow onUpdate={update} errors={errors}>
{props.row({ errors, onUpdate: update })}
<Cell>
<Button
disabled={!isOk || isMutating}
variant="tertiary"
className={clsx(isOk ? "text-txt-success" : "text-txt-secondary")}
onClick={() => void onSubmit()}
>
{isMutating ? <Spinner size={16} /> : <IconCheckmark1 size={16} />}
</Button>
</Cell>
</EditableRow>
);
}
function EditableTableHead(props: { column: ColumnDefinition }) {
if (typeof props.column === "string") {
return <CellHead>{props.column}</CellHead>;
}
return (
<SortableCellHead field={props.column.field}>
{props.column.label}
</SortableCellHead>
);
}