diff --git a/CHANGELOG.md b/CHANGELOG.md index d89ece9cc..ef3ce2c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.11.0 +`feature`: `FieldVisibility` now accepts the `children` prop to allow a custom dropdown button. +`feature`: `EnumInput` can now be passed options of of type `SelectOption | string` for more customizability. +`feature`: Enhanced the way to customize actions for `ModelForm` and `ModelTable`. Can alter the underlying button by the `actionOptions.actionProps` prop or by expanding the model component down to its primitive components: `EditAction, CancelEditAction, SubmitAction, DeleteAction`. + ## 1.10.2 `bugfix`: Fixed delay on form values populating `FormDisplay`. `upgrade`: react-hook-form@7.54.2 @@ -5,7 +10,6 @@ ## 1.10.1 `bugfix`: z-index of select components were using `react-select`'s default z-index and not the override version. - ## 1.10.0 - `feature`: Can now set which rows are "selected" to trigger its selected-row background color. - `bugfix`: Table resizing now does not allow you to go less than the content width for a brief moment diff --git a/package.json b/package.json index ccb1a5b29..9e15b7805 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@autoinvent/conveyor", "type": "module", - "version": "1.10.2", + "version": "1.11.0", "description": "UI component library for magql", "license": "BlueOak-1.0.0", "author": "Moebius Solutions", diff --git a/src/Actions/ActionContext.tsx b/src/Actions/ActionContext.tsx new file mode 100644 index 000000000..105402075 --- /dev/null +++ b/src/Actions/ActionContext.tsx @@ -0,0 +1,75 @@ +import { + type ComponentProps, + type ReactNode, + createContext, + useEffect, + useRef, + useState, +} from 'react'; + +import { type StoreApi, createStore } from 'zustand'; + +import type { Button } from '@/lib/components/ui/button'; +import type { DataType } from '@/types'; + +export enum Action { + SUBMIT = 'SUBMIT', + DELETE = 'DELETE', + EDIT = 'EDIT', + CANCEL_EDIT = 'CANCEL_EDIT', +} + +export type OnActionTrigger = + | ((params: TParams) => Promise) + | ((params: TParams) => void); + +export interface ActionParams { + data: D; + changedData: D; + onEdit: () => void; + onCancelEdit: () => void; +} + +export type ActionsType = Partial< + Record, void> | null> +>; + +export type ActionsPropsType = Partial< + Record> +>; + +export interface ActionState { + showActions?: boolean; + actions?: ActionsType; + actionProps?: ActionsPropsType; +} + +export const ActionStoreContext = createContext< + StoreApi> | undefined +>(undefined); + +export type ActionStoreProviderProps = ActionState & { + children?: ReactNode; +}; + +export const ActionStoreProvider = ({ + children, + ...actionState +}: ActionStoreProviderProps) => { + const isMounted = useRef(false); + const [store] = useState(() => createStore(() => actionState)); + /* + biome-ignore lint/correctness/useExhaustiveDependencies: + The reference to tableState does not matter, only the contents. + */ + useEffect(() => { + if (isMounted.current) store.setState(actionState); + else isMounted.current = true; + }, [...Object.values(actionState), store]); + + return ( + + {children} + + ); +}; diff --git a/src/Actions/CancelEditAction.tsx b/src/Actions/CancelEditAction.tsx new file mode 100644 index 000000000..ded6eb5ef --- /dev/null +++ b/src/Actions/CancelEditAction.tsx @@ -0,0 +1,40 @@ +import type { ComponentProps } from 'react'; + +import { X } from 'lucide-react'; + +import { Button } from '@/lib/components/ui/button'; + +import { Action } from './ActionContext'; +import { useActionStore } from './useActionStore'; +import { useGetActionParams } from './useGetActionParams'; + +export interface CancelEditActionProps extends ComponentProps {} + +export const CancelEditAction = ({ + size, + variant = size === 'icon' ? 'ghost' : 'outline', + children = size === 'icon' ? : 'Cancel', + ...buttonProps +}: CancelEditActionProps) => { + const onCancelEditProp = useActionStore( + (state) => state.actions?.[Action.CANCEL_EDIT], + ); + const getActionParams = useGetActionParams(); + const { onCancelEdit } = getActionParams({}); + const onCancelEditHandler = + onCancelEditProp === undefined ? onCancelEdit : onCancelEditProp; + + return ( + onCancelEditHandler && ( + + ) + ); +}; diff --git a/src/Actions/DeleteAction.tsx b/src/Actions/DeleteAction.tsx new file mode 100644 index 000000000..659b22217 --- /dev/null +++ b/src/Actions/DeleteAction.tsx @@ -0,0 +1,41 @@ +import type { ComponentProps } from 'react'; + +import { Trash2 } from 'lucide-react'; + +import { useFormStore } from '@/Form'; +import { Button } from '@/lib/components/ui/button'; + +import { Action } from './ActionContext'; +import { useActionStore } from './useActionStore'; +import { useGetActionParams } from './useGetActionParams'; + +export interface DeleteActionProps extends ComponentProps {} + +export const DeleteAction = ({ + size, + variant = size === 'icon' ? 'ghost-destructive' : 'destructive', + children = size === 'icon' ? : 'Delete', + ...buttonProps +}: DeleteActionProps) => { + const getActionParams = useGetActionParams(); + const handleSubmit = useFormStore((state) => state.handleSubmit); + const onDelete = useActionStore((state) => state.actions?.[Action.DELETE]); + + const onDeleteHandler = handleSubmit(async () => { + await onDelete?.(getActionParams({})); + }); + + return ( + onDelete && ( + + ) + ); +}; diff --git a/src/Actions/EditAction.tsx b/src/Actions/EditAction.tsx new file mode 100644 index 000000000..7181f3696 --- /dev/null +++ b/src/Actions/EditAction.tsx @@ -0,0 +1,37 @@ +import type { ComponentProps } from 'react'; + +import { SquarePen } from 'lucide-react'; + +import { Button } from '@/lib/components/ui/button'; + +import { Action } from './ActionContext'; +import { useActionStore } from './useActionStore'; +import { useGetActionParams } from './useGetActionParams'; + +export interface EditActionProps extends ComponentProps {} + +export const EditAction = ({ + size, + variant = size === 'icon' ? 'ghost' : 'default', + children = size === 'icon' ? : 'Edit', + ...buttonProps +}: EditActionProps) => { + const onEditProp = useActionStore((state) => state.actions?.[Action.EDIT]); + const getActionParams = useGetActionParams(); + const { onEdit } = getActionParams({}); + const onEditHandler = onEditProp === undefined ? onEdit : onEditProp; + + return ( + onEditHandler && ( + + ) + ); +}; diff --git a/src/Actions/SubmitAction.tsx b/src/Actions/SubmitAction.tsx new file mode 100644 index 000000000..ca5f195d1 --- /dev/null +++ b/src/Actions/SubmitAction.tsx @@ -0,0 +1,46 @@ +import type { ComponentProps } from 'react'; + +import { Save } from 'lucide-react'; + +import { useFormStore } from '@/Form'; +import { Button } from '@/lib/components/ui/button'; +import type { DataType } from '@/types'; + +import { Action } from './ActionContext'; +import { useActionStore } from './useActionStore'; +import { useGetActionParams } from './useGetActionParams'; + +export interface SubmitActionProps extends ComponentProps {} + +export const SubmitAction = ({ + size, + variant = size === 'icon' ? 'ghost-success' : 'default', + children = size === 'icon' ? : 'Save', + ...buttonProps +}: SubmitActionProps) => { + const getActionParams = useGetActionParams(); + const handleSubmit = useFormStore((state) => state.handleSubmit); + const onSubmit = useActionStore((state) => state.actions?.[Action.SUBMIT]); + const updateProps = useActionStore( + (state) => state.actionProps?.[Action.SUBMIT], + ); + + const onSubmitHandler = handleSubmit(async (formData: DataType) => { + await onSubmit?.(getActionParams(formData)); + }); + + return ( + onSubmit && ( + + ) + ); +}; diff --git a/src/Actions/useActionStore.tsx b/src/Actions/useActionStore.tsx new file mode 100644 index 000000000..3d84b1318 --- /dev/null +++ b/src/Actions/useActionStore.tsx @@ -0,0 +1,27 @@ +import { useContext } from 'react'; + +import { useStore } from 'zustand'; + +import type { DataType, StoreSelector } from '@/types'; + +import { type ActionState, ActionStoreContext } from './ActionContext'; + +export function useActionStore(): ActionState; +export function useActionStore( + selector: StoreSelector, T>, +): T; + +export function useActionStore( + selector?: StoreSelector, T>, +) { + const actionStore = useContext(ActionStoreContext); + if (actionStore === undefined) { + throw new Error('useActionStore must be used within ActionStoreProvider'); + } + + const selected = selector + ? useStore(actionStore, selector) + : useStore(actionStore); + + return selected; +} diff --git a/src/Actions/useGetActionParams.tsx b/src/Actions/useGetActionParams.tsx new file mode 100644 index 000000000..43eb5a39c --- /dev/null +++ b/src/Actions/useGetActionParams.tsx @@ -0,0 +1,34 @@ +import { useFormStore } from '@/Form'; +import { useLensesStore } from '@/Lenses'; +import { DataLens, type DataType } from '@/types'; + +import type { ActionParams } from './ActionContext'; + +export const useGetActionParams = (): (( + formValues: D, +) => ActionParams) => { + const setLens = useLensesStore((state) => state.setLens); + + const defaultValues = useFormStore( + (state) => state.formState.defaultValues as D, + ); + const dirtyFields = useFormStore((state) => state.formState.dirtyFields); + const reset = useFormStore((state) => state.reset); + + const getChangedData = (formValues: D) => + Object.fromEntries( + Object.entries(formValues).filter((entry) => dirtyFields[entry[0]]), + ) as D; + const onEdit = () => setLens(DataLens.INPUT); + const onCancelEdit = () => { + setLens(DataLens.DISPLAY); + reset(); + }; + + return (formValues) => ({ + data: { ...defaultValues }, + changedData: getChangedData(formValues), + onEdit, + onCancelEdit, + }); +}; diff --git a/src/BasicInputs/EnumInput.tsx b/src/BasicInputs/EnumInput.tsx index de76c9d0f..2bedb3faa 100644 --- a/src/BasicInputs/EnumInput.tsx +++ b/src/BasicInputs/EnumInput.tsx @@ -4,7 +4,7 @@ import { forwardRef, } from 'react'; -import { humanizeText } from '@/utils'; +import type { SelectOption } from '@/types'; import { SelectInput } from './SelectInput'; @@ -12,11 +12,11 @@ export const EnumInput = forwardRef< ElementRef, Omit, 'value' | 'options'> & { value?: string | string[]; - options: string[]; + options: (string | SelectOption)[]; } >(({ value, onChange, options, ...selectInputProps }, ref) => { const stringToOption = (str: string) => ({ - label: humanizeText(str), + label: str, value: str, }); return ( @@ -28,7 +28,9 @@ export const EnumInput = forwardRef< ? value.map(stringToOption) : stringToOption(value)) } - options={options?.map(stringToOption)} + options={options?.map((option) => + typeof option === 'string' ? stringToOption(option) : option, + )} onChange={(newValue, actionMeta) => onChange?.( Array.isArray(newValue) diff --git a/src/BasicInputs/stories/EnumInput.stories.tsx b/src/BasicInputs/stories/EnumInput.stories.tsx index 257971f6a..6a9a65d3b 100644 --- a/src/BasicInputs/stories/EnumInput.stories.tsx +++ b/src/BasicInputs/stories/EnumInput.stories.tsx @@ -51,3 +51,12 @@ export const MultiSelectWithValueInference: Story = { value: [], }, }; + +export const CustomOptions: Story = { + args: { + options: [ + { label: APPLE, value: 'apple' }, + 'banana', + ], + }, +}; diff --git a/src/ModelForm/ModelForm.tsx b/src/ModelForm/ModelForm.tsx index d2c7cc739..9e676d5a7 100644 --- a/src/ModelForm/ModelForm.tsx +++ b/src/ModelForm/ModelForm.tsx @@ -2,6 +2,7 @@ import { type ComponentProps, useId } from 'react'; import { type UseFormProps, useForm } from 'react-hook-form'; +import { type ActionState, ActionStoreProvider } from '@/Actions/ActionContext'; import { FormStoreProvider } from '@/Form'; import { Lenses } from '@/Lenses'; import { cn } from '@/lib/utils'; @@ -20,23 +21,20 @@ export interface ModelFormProps< F extends string, DT extends D, FT extends F, -> extends ModelFormState, +> extends ModelFormState, Omit, 'onSubmit'>, - Partial> {} + Partial> { + actionOptions?: ActionState
; +} export const ModelForm = Object.assign( ({ data, id = data?.id || useId(), + actionOptions, model, fields, fieldOptions, - onCreate, - onDelete, - onUpdate, - onEdit, - onCancelEdit, - readOnly, initialLens, resolver, mode = 'onSubmit', @@ -70,33 +68,29 @@ export const ModelForm = Object.assign( fields={fields} fieldOptions={fieldOptions} data={data} - onCreate={onCreate} - onDelete={onDelete} - onUpdate={onUpdate} - onEdit={onEdit} - onCancelEdit={onCancelEdit} - readOnly={readOnly} initialLens={initialLens} > -
e.preventDefault()} - {...formProps} - > - - - {children === undefined ? ( - <> - - - - ) : ( - children - )} - - -
+ +
e.preventDefault()} + {...formProps} + > + + + {children === undefined ? ( + <> + + + + ) : ( + children + )} + + +
+
); }, diff --git a/src/ModelForm/ModelFormActions.tsx b/src/ModelForm/ModelFormActions.tsx index bb1bc2d99..ebb3d6f80 100644 --- a/src/ModelForm/ModelFormActions.tsx +++ b/src/ModelForm/ModelFormActions.tsx @@ -2,11 +2,16 @@ import type { ComponentProps, ReactNode } from 'react'; import { LoaderCircle } from 'lucide-react'; +import { CancelEditAction } from '@/Actions/CancelEditAction'; +import { DeleteAction } from '@/Actions/DeleteAction'; +import { EditAction } from '@/Actions/EditAction'; +import { SubmitAction } from '@/Actions/SubmitAction'; +import { useActionStore } from '@/Actions/useActionStore'; import { useFormStore } from '@/Form'; -import { Lens, useLensesStore } from '@/Lenses'; +import { Lens } from '@/Lenses'; import { Button } from '@/lib/components/ui/button'; import { cn } from '@/lib/utils'; -import { DataLens, type DataType } from '@/types'; +import { DataLens } from '@/types'; import { useModelFormStore } from './useModelFormStore'; @@ -18,103 +23,23 @@ export const ModelFormActions = ({ className, children, }: ModelFormActionsProps) => { - const setLens = useLensesStore((state) => state.setLens); - const defaultValues = useFormStore((state) => state.formState.defaultValues); const isSubmitting = useFormStore((state) => state.formState.isSubmitting); - const dirtyFields = useFormStore((state) => state.formState.dirtyFields); - const reset = useFormStore((state) => state.reset); - const handleSubmit = useFormStore((state) => state.handleSubmit); const fields = useModelFormStore((state) => state.fields); - const readOnly = useModelFormStore((state) => state.readOnly); - const onCreate = useModelFormStore((state) => state.onCreate); - const onUpdate = useModelFormStore((state) => state.onUpdate); - const onDelete = useModelFormStore((state) => state.onDelete); - const onEdit = useModelFormStore((state) => state.onEdit); - const onCancelEdit = useModelFormStore((state) => state.onCancelEdit); - const onSave = onCreate ?? onUpdate; - - const onEditHandler = () => setLens(DataLens.INPUT); - const onCancelEditHandler = () => { - setLens(DataLens.DISPLAY); - reset(); - }; - const onSaveHandler = handleSubmit(async (formData: DataType) => { - const changedData = Object.fromEntries( - Object.entries(formData).filter((entry) => dirtyFields[entry[0]]), - ); - await onSave?.({ - data: { ...defaultValues }, - changedData, - onEdit: onEditHandler, - onCancelEdit: onCancelEditHandler, - }); - }); - const onDeleteHandler = handleSubmit(async () => { - await onDelete?.({ - data: { ...defaultValues }, - changedData: {}, - onEdit: onEditHandler, - onCancelEdit: onCancelEditHandler, - }); - }); + const showActions = useActionStore((state) => state.showActions); return ( - !readOnly && + showActions !== false && fields.length > 0 && (
{children === undefined ? ( <> - {onUpdate && ( - - )} - {onDelete && ( - - )} + + - {onSave && ( - - )} - + + {isSubmitting && ( - )} - {onDelete && ( - - )} + + - {onUpdate && ( - - )} - + + {isSubmitting && ( - )} diff --git a/src/ModelTable/ModelTableCell.tsx b/src/ModelTable/ModelTableCell.tsx index 8f895ba15..880a48a02 100644 --- a/src/ModelTable/ModelTableCell.tsx +++ b/src/ModelTable/ModelTableCell.tsx @@ -1,3 +1,4 @@ +import { useActionStore } from '@/Actions/useActionStore'; import { useConveyorStore } from '@/Conveyor'; import { FormDisplay, useFormStore } from '@/Form'; import { FormControl } from '@/Form/FormControl'; @@ -18,7 +19,8 @@ export const ModelTableCell = ({ ...tableCellProps }: ModelTableCellProps) => { const { setLens, activeLens } = useLensesStore(); - const readOnly = useModelTableStore((state) => state.tableOptions?.readOnly); + const showActions = useActionStore((state) => state.showActions); + const onSubmit = useActionStore((state) => state.actions?.SUBMIT); const draggable = useModelTableStore( (state) => state.tableOptions?.draggable ?? true, ); @@ -31,7 +33,6 @@ export const ModelTableCell = ({ const editable = useModelTableStore( (state) => state.columnOptions?.[field]?.editable ?? true, ); - const onUpdate = useModelTableStore((state) => state.onUpdate); const rules = useModelTableStore( (state) => state.columnOptions?.[field]?.rules, ); @@ -58,11 +59,11 @@ export const ModelTableCell = ({ columnId={field} onDoubleClick={() => { if ( - !readOnly && + showActions !== false && editable && activeLens === DataLens.DISPLAY && !isSubmitting && - onUpdate + onSubmit ) { setLens(DataLens.INPUT); } diff --git a/src/ModelTable/ModelTableHeaderRow.tsx b/src/ModelTable/ModelTableHeaderRow.tsx index 1dbc07b99..c29269318 100644 --- a/src/ModelTable/ModelTableHeaderRow.tsx +++ b/src/ModelTable/ModelTableHeaderRow.tsx @@ -13,8 +13,10 @@ export const ModelTableHeaderRow = ({ className, ...tableHeaderRowProps }: ModelTableHeaderRowProps) => { + const showActions = useModelTableStore( + (state) => state.actionOptions?.showActions, + ); const fields = useModelTableStore((state) => state.fields); - const readOnly = useModelTableStore((state) => state.tableOptions?.readOnly); const fieldOrder = useModelTableStore((state) => state.fieldOrder); const draggable = useModelTableStore( (state) => state.tableOptions?.draggable ?? true, @@ -33,7 +35,7 @@ export const ModelTableHeaderRow = ({ {fields.map((field) => ( ))} - {!readOnly && } + {showActions !== false && } {children} ) : ( diff --git a/src/ModelTable/ModelTableRow.tsx b/src/ModelTable/ModelTableRow.tsx index 6eddf6f6b..f0f15e737 100644 --- a/src/ModelTable/ModelTableRow.tsx +++ b/src/ModelTable/ModelTableRow.tsx @@ -1,5 +1,6 @@ import { useForm } from 'react-hook-form'; +import { useActionStore } from '@/Actions/useActionStore'; import { useDataStore } from '@/Data'; import { FormStoreProvider } from '@/Form'; import { Lenses } from '@/Lenses'; @@ -21,7 +22,7 @@ export const ModelTableRow = ({ }: ModelTableRowProps) => { const fields = useModelTableStore((state) => state.fields); const fieldOrder = useModelTableStore((state) => state.fieldOrder); - const readOnly = useModelTableStore((state) => state.tableOptions?.readOnly); + const showActions = useActionStore((state) => state.showActions); const draggable = useModelTableStore( (state) => state.tableOptions?.draggable, ); @@ -55,7 +56,7 @@ export const ModelTableRow = ({ {fields.map((field) => ( ))} - {!readOnly && } + {showActions !== false && } {children} ) : ( diff --git a/src/ModelTable/ModelTableStoreContext.tsx b/src/ModelTable/ModelTableStoreContext.tsx index 0dca5ad58..66048314e 100644 --- a/src/ModelTable/ModelTableStoreContext.tsx +++ b/src/ModelTable/ModelTableStoreContext.tsx @@ -9,13 +9,7 @@ import { import type { UseFormProps } from 'react-hook-form'; import { type StoreApi, createStore } from 'zustand'; -import type { - DataType, - FieldOptions, - ID, - OnActionTrigger, - TableView, -} from '@/types'; +import type { DataType, FieldOptions, ID, TableView } from '@/types'; export interface ColumnOptions extends FieldOptions { sortable?: boolean; @@ -26,7 +20,6 @@ export interface ColumnOptions extends FieldOptions { export interface TableOptions { sortOrder?: TableView['sort']; // Order + value of the field sort - readOnly?: boolean; scrollable?: boolean | { className: string }; // Wraps the table with ScrollArea draggable?: boolean; // Wraps the table with DnDContext bordered?: boolean | { className: string }; // Wraps the table with div to add bordered styles @@ -43,7 +36,6 @@ export interface FormOptions export interface ModelTableState< D extends DataType, F extends string, - DT extends D, FT extends F, > { model: string; @@ -54,31 +46,27 @@ export interface ModelTableState< tableOptions?: TableOptions; columnOptions?: Partial>; formOptions?: FormOptions; - onUpdate?: OnActionTrigger
; - onDelete?: OnActionTrigger
; } export const ModelTableStoreContext = createContext< - StoreApi> | undefined + StoreApi> | undefined >(undefined); export interface ModelTableStoreProviderProps< D extends DataType, F extends string, - DT extends D, FT extends F, -> extends ModelTableState { +> extends ModelTableState { children?: ReactNode; } export const ModelTableStoreProvider = < D extends DataType, F extends string, - DT extends D, FT extends F, >({ children, ...modelTableState -}: ModelTableStoreProviderProps) => { +}: ModelTableStoreProviderProps) => { const isMounted = useRef(false); const [store] = useState(() => createStore(() => modelTableState)); /* diff --git a/src/ModelTable/stories/ModelIndexPage.stories.tsx b/src/ModelTable/stories/ModelIndexPage.stories.tsx index 5a569f38c..38f7338b5 100644 --- a/src/ModelTable/stories/ModelIndexPage.stories.tsx +++ b/src/ModelTable/stories/ModelIndexPage.stories.tsx @@ -4,6 +4,7 @@ import { Plus } from 'lucide-react'; import type { Meta, StoryObj } from '@storybook/react'; +import { Action, type ActionParams } from '@/Actions/ActionContext'; import { RawDisplay } from '@/BasicDisplays'; import { Conveyor } from '@/Conveyor'; import { FormDisplay } from '@/Form'; @@ -11,12 +12,7 @@ import { Header } from '@/Header'; import ModelTableStoryMeta from '@/ModelTable/stories/ModelTable.stories'; import { Pagination } from '@/Pagination'; import { Button } from '@/lib/components/ui/button'; -import { - type ActionParams, - type DataType, - ScalarType, - type TableView, -} from '@/types'; +import { type DataType, ScalarType, type TableView } from '@/types'; import { FieldVisibility } from '../FieldVisibility'; import { ModelTable } from '../ModelTable'; @@ -25,7 +21,6 @@ const meta = { title: 'Models/ModelTable/ModelIndexPage', component: ModelTable, tags: ['autodocs'], - argTypes: ModelTableStoryMeta.argTypes, args: ModelTableStoryMeta.args, render: ({ fields, @@ -33,8 +28,7 @@ const meta = { onFieldOrderChange: dummyOnFieldOrderChange, tableOptions, data, - onUpdate, - onDelete, + actionOptions, columnOptions, ...args }) => { @@ -44,8 +38,8 @@ const meta = { const [fieldOrder, onFieldOrderChange] = useState([...fields]); const [perPage, setPerPage] = useState(10); - const onUpdateHandler = async (params: ActionParams) => { - await onUpdate?.(params); + const onSubmitHandler = async (params: ActionParams) => { + await actionOptions?.actions?.[Action.SUBMIT]?.(params); const id = params?.data?.id; console.log(params); if (id) { @@ -63,7 +57,7 @@ const meta = { }; const onDeleteHandler = async (params: ActionParams) => { - await onDelete?.(params); + await actionOptions?.actions?.[Action.DELETE]?.(params); const id = params?.data?.id; if (id) { setCurrData((oldData) => { @@ -119,8 +113,12 @@ const meta = { }, }} columnOptions={columnOptions} - onUpdate={onUpdateHandler} - onDelete={onDeleteHandler} + actionOptions={{ + actions: { + [Action.SUBMIT]: onSubmitHandler, + [Action.DELETE]: onDeleteHandler, + }, + }} {...args} > diff --git a/src/ModelTable/stories/ModelTable.stories.tsx b/src/ModelTable/stories/ModelTable.stories.tsx index 629bd7d00..eec96fea7 100644 --- a/src/ModelTable/stories/ModelTable.stories.tsx +++ b/src/ModelTable/stories/ModelTable.stories.tsx @@ -2,12 +2,8 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { - type ActionParams, - type DataType, - FieldType, - type TableView, -} from '@/types'; +import { Action, type ActionParams } from '@/Actions/ActionContext'; +import { type DataType, FieldType, type TableView } from '@/types'; import { ModelTable } from '../ModelTable'; @@ -15,10 +11,6 @@ const meta = { title: 'Models/ModelTable/General', component: ModelTable, tags: ['autodocs'], - argTypes: { - onUpdate: { control: false }, - onDelete: { control: false }, - }, args: { model: 'Task', fields: ['id', 'message', 'user', 'created_at', 'points', 'done'], @@ -96,8 +88,14 @@ const meta = { hidable: false, }, }, - onUpdate: () => new Promise((resolve) => setTimeout(resolve, 2000)), - onDelete: () => new Promise((resolve) => setTimeout(resolve, 2000)), + actionOptions: { + actions: { + [Action.SUBMIT]: () => + new Promise((resolve) => setTimeout(resolve, 2000)), + [Action.DELETE]: () => + new Promise((resolve) => setTimeout(resolve, 2000)), + }, + }, }, render: ({ fields, @@ -105,8 +103,7 @@ const meta = { onFieldOrderChange: dummyOnFieldOrderChange, tableOptions, data, - onUpdate, - onDelete, + actionOptions, ...args }) => { const [currData, setCurrData] = useState(data); @@ -114,8 +111,8 @@ const meta = { useState(undefined); const [fieldOrder, onFieldOrderChange] = useState([...fields]); - const onUpdateHandler = async (params: ActionParams) => { - await onUpdate?.(params); + const onSubmitHandler = async (params: ActionParams) => { + await actionOptions?.actions?.[Action.SUBMIT]?.(params); const id = params?.data?.id; if (id) { setCurrData((oldData) => { @@ -132,7 +129,7 @@ const meta = { }; const onDeleteHandler = async (params: ActionParams) => { - await onDelete?.(params); + await actionOptions?.actions?.[Action.DELETE]?.(params); const id = params?.data?.id; if (id) { setCurrData((oldData) => { @@ -159,8 +156,12 @@ const meta = { sortOrder, onSortOrderChange, }} - onUpdate={onUpdateHandler} - onDelete={onDeleteHandler} + actionOptions={{ + actions: { + [Action.SUBMIT]: onSubmitHandler, + [Action.DELETE]: onDeleteHandler, + }, + }} {...args} /> ); @@ -186,8 +187,8 @@ export const NoData: Story = { export const ReadOnly: Story = { args: { - tableOptions: { - readOnly: true, + actionOptions: { + showActions: false, }, }, }; @@ -207,8 +208,7 @@ export const OnUpdateIsUndefined: Story = { onFieldOrderChange: dummyOnFieldOrderChange, tableOptions, data, - onUpdate, - onDelete, + actionOptions, ...args }) => { const [currData, setCurrData] = useState(data); @@ -217,7 +217,7 @@ export const OnUpdateIsUndefined: Story = { const [fieldOrder, onFieldOrderChange] = useState([...fields]); const onDeleteHandler = async (params: ActionParams) => { - await onDelete?.(); + await actionOptions?.actions?.[Action.DELETE]?.(params); const id = params?.data?.id; if (id) { setCurrData((oldData) => { @@ -244,7 +244,11 @@ export const OnUpdateIsUndefined: Story = { sortOrder, onSortOrderChange, }} - onDelete={onDeleteHandler} + actionOptions={{ + actions: { + [Action.DELETE]: onDeleteHandler, + }, + }} {...args} /> ); diff --git a/src/ModelTable/useModelTableStore.tsx b/src/ModelTable/useModelTableStore.tsx index 0fc3f75cc..c0fa900e0 100644 --- a/src/ModelTable/useModelTableStore.tsx +++ b/src/ModelTable/useModelTableStore.tsx @@ -12,24 +12,21 @@ import { export function useModelTableStore< D extends DataType, F extends string, - DT extends D, FT extends F, ->(): ModelTableState; +>(): ModelTableState; export function useModelTableStore< D extends DataType, F extends string, - DT extends D, FT extends F, S, ->(selector: StoreSelector, S>): S; +>(selector: StoreSelector, S>): S; export function useModelTableStore< D extends DataType, F extends string, - DT extends D, FT extends F, S, ->(selector?: StoreSelector, S>) { +>(selector?: StoreSelector, S>) { const modelTableStore = useContext(ModelTableStoreContext); if (modelTableStore === undefined) { throw new Error( diff --git a/src/types/common.ts b/src/types/common.ts index fd671479e..68615233e 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,23 +1,15 @@ +import type { ReactNode } from 'react'; + export type DataType = Record; export enum DataLens { - DISPLAY = 'DISPLAY', + DISPLAY = 'display', INPUT = 'input', } -export interface ActionParams { - data: D; - changedData: D; - onEdit: () => void; - onCancelEdit: () => void; -} -export type OnActionTrigger = - | ((params: ActionParams) => Promise) - | ((params: ActionParams) => void); - export type StoreSelector = (state: TState) => T; export type SelectOption = { - label: string; + label: ReactNode; value: any; }; diff --git a/src/types/conveyor.ts b/src/types/conveyor.ts index fa73f2a91..aff7ef5d0 100644 --- a/src/types/conveyor.ts +++ b/src/types/conveyor.ts @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'; import type { RegisterOptions } from 'react-hook-form'; -import type { DataType, SelectOption } from './common'; +import type { DataType } from './common'; import { ScalarType } from './magql'; export interface FieldOptions { @@ -15,7 +15,6 @@ export interface FieldOptions { RegisterOptions, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled' >; - valueOptions?: SelectOption[]; inputProps?: DataType; displayProps?: DataType; }