Skip to content

Commit

Permalink
1.11.0 (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
robxbob committed Feb 11, 2025
1 parent 2bacddb commit 2756d47
Show file tree
Hide file tree
Showing 27 changed files with 509 additions and 424 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
## 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`: [email protected]

## 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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
75 changes: 75 additions & 0 deletions src/Actions/ActionContext.tsx
Original file line number Diff line number Diff line change
@@ -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<TParams, TReturn> =
| ((params: TParams) => Promise<TReturn>)
| ((params: TParams) => void);

export interface ActionParams<D extends DataType> {
data: D;
changedData: D;
onEdit: () => void;
onCancelEdit: () => void;
}

export type ActionsType<D extends DataType> = Partial<
Record<Action, OnActionTrigger<ActionParams<D>, void> | null>
>;

export type ActionsPropsType = Partial<
Record<Action, ComponentProps<typeof Button>>
>;

export interface ActionState<D extends DataType> {
showActions?: boolean;
actions?: ActionsType<D>;
actionProps?: ActionsPropsType;
}

export const ActionStoreContext = createContext<
StoreApi<ActionState<any>> | undefined
>(undefined);

export type ActionStoreProviderProps<D extends DataType> = ActionState<D> & {
children?: ReactNode;
};

export const ActionStoreProvider = <D extends DataType>({
children,
...actionState
}: ActionStoreProviderProps<D>) => {
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 (
<ActionStoreContext.Provider value={store}>
{children}
</ActionStoreContext.Provider>
);
};
40 changes: 40 additions & 0 deletions src/Actions/CancelEditAction.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button> {}

export const CancelEditAction = ({
size,
variant = size === 'icon' ? 'ghost' : 'outline',
children = size === 'icon' ? <X className="h-4 w-4" /> : '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 && (
<Button
variant={variant}
size={size}
onClick={onCancelEdit}
onKeyUp={(e) => e.key === 'Enter' && onCancelEdit()}
{...buttonProps}
>
{children}
</Button>
)
);
};
41 changes: 41 additions & 0 deletions src/Actions/DeleteAction.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button> {}

export const DeleteAction = ({
size,
variant = size === 'icon' ? 'ghost-destructive' : 'destructive',
children = size === 'icon' ? <Trash2 className="h-4 w-4" /> : '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 && (
<Button
variant={variant}
size={size}
onClick={onDeleteHandler}
onKeyUp={(e) => e.key === 'Enter' && onDeleteHandler()}
{...buttonProps}
>
{children}
</Button>
)
);
};
37 changes: 37 additions & 0 deletions src/Actions/EditAction.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button> {}

export const EditAction = ({
size,
variant = size === 'icon' ? 'ghost' : 'default',
children = size === 'icon' ? <SquarePen className="h-4 w-4" /> : '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 && (
<Button
variant={variant}
size={size}
onClick={onEdit}
onKeyUp={(e) => e.key === 'Enter' && onEdit()}
{...buttonProps}
>
{children}
</Button>
)
);
};
46 changes: 46 additions & 0 deletions src/Actions/SubmitAction.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Button> {}

export const SubmitAction = ({
size,
variant = size === 'icon' ? 'ghost-success' : 'default',
children = size === 'icon' ? <Save className="h-4 w-4" /> : '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 && (
<Button
variant={variant}
size={size}
onClick={onSubmitHandler}
onKeyUp={(e) => e.key === 'Enter' && onSubmitHandler()}
{...updateProps}
{...buttonProps}
>
{updateProps?.children ?? children}
</Button>
)
);
};
27 changes: 27 additions & 0 deletions src/Actions/useActionStore.tsx
Original file line number Diff line number Diff line change
@@ -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<D extends DataType>(): ActionState<D>;
export function useActionStore<D extends DataType, T>(
selector: StoreSelector<ActionState<D>, T>,
): T;

export function useActionStore<D extends DataType, T>(
selector?: StoreSelector<ActionState<D>, 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;
}
34 changes: 34 additions & 0 deletions src/Actions/useGetActionParams.tsx
Original file line number Diff line number Diff line change
@@ -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 = <D extends DataType>(): ((
formValues: D,
) => ActionParams<D>) => {
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,
});
};
10 changes: 6 additions & 4 deletions src/BasicInputs/EnumInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import {
forwardRef,
} from 'react';

import { humanizeText } from '@/utils';
import type { SelectOption } from '@/types';

import { SelectInput } from './SelectInput';

export const EnumInput = forwardRef<
ElementRef<typeof SelectInput>,
Omit<ComponentPropsWithoutRef<typeof SelectInput>, '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 (
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions src/BasicInputs/stories/EnumInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ export const MultiSelectWithValueInference: Story = {
value: [],
},
};

export const CustomOptions: Story = {
args: {
options: [
{ label: <span className="bg-red-400">APPLE</span>, value: 'apple' },
'banana',
],
},
};
Loading

0 comments on commit 2756d47

Please sign in to comment.