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 = { item?: T; onUpdate: (key: keyof z.infer, value: z.infer[typeof key]) => void; errors: Record; }; /** * 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 >; 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) => 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; pageSize?: number; }) { const { update } = useMutateField(props.updateMutation); const [showAdd, toggleAdd] = useToggle(false); return ( "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} > {props.columns.map((column, index) => ( ))} {props.items.map(item => ( update(item.id, k, v)} key={item.id}> {props.row({ item, onUpdate: (key, value) => update(item.id, key as string, value), errors: {}, })} {props.action({ item })} ))} {showAdd ? ( ) : ( {props.addLabel} )} ); } function NewItemRow(props: { schema: S; defaultValue: z.infer; connectionId: string; mutation: GraphQLTaggedNode; onSuccess: () => void; row: (props: EditableTableRowProps) => 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 ( {props.row({ errors, onUpdate: update })} ); } function EditableTableHead(props: { column: ColumnDefinition }) { if (typeof props.column === "string") { return {props.column}; } return ( {props.column.label} ); }