Skip to content

Commit

Permalink
feat: nav form on strapi (#458)
Browse files Browse the repository at this point in the history
  • Loading branch information
cyp3rius authored Oct 12, 2024
1 parent 9e99195 commit f14568b
Show file tree
Hide file tree
Showing 12 changed files with 800 additions and 389 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type Footer = React.FC<{
onSubmit: VoidEffect;
setState: SetState;
state: State;
disabled?: boolean;
isLoading: boolean;
}>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Navigation } from '../types';

const formSchema = ({ alreadyUsedNames }: { alreadyUsedNames: string[] }) =>
export const formSchema = ({ alreadyUsedNames }: { alreadyUsedNames: string[] }) =>
z.object({
name: z
.string()
.min(2)
.and(z.string().refine((name) => !alreadyUsedNames.includes(name), 'name already used')),
.min(2) // TODO: add translation
.and(z.string().refine((name) => !alreadyUsedNames.includes(name), 'Name already used')), // TODO: add translation
visible: z.boolean(),
});

export const useNavigationForm = <T extends Partial<Navigation>>({
alreadyUsedNames,
navigation: { name, visible },
}: {
alreadyUsedNames: string[];
navigation: T;
}) => {
return useForm<z.infer<ReturnType<typeof formSchema>>>({
resolver: zodResolver(formSchema({ alreadyUsedNames })),
defaultValues: {
name: name ?? '',
visible: visible ?? false,
},
});
};
197 changes: 128 additions & 69 deletions admin/src/pages/HomePage/components/NavigationManager/Form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Field } from '@strapi/design-system';
import { InputRenderer } from '@strapi/strapi/admin';
import { debounce } from 'lodash';
import { useEffect } from 'react';
import { Controller } from 'react-hook-form';
import { Grid, TextInput, Toggle } from '@strapi/design-system';
import { Form as StrapiForm } from '@strapi/strapi/admin';
import { get, isEmpty, isNil, isObject, isString, set } from 'lodash';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { Field } from '@sensinum/strapi-utils';

import { Checkbox, Grid } from '@strapi/design-system';
import { getTrad } from '../../../../../translations';
import { Effect } from '../../../../../types';
import { Effect, FormChangeEvent, FormItemErrorSchema } from '../../../../../types';
import { Navigation } from '../types';
import { useNavigationForm } from './hooks';
import { TextInput } from '@strapi/design-system';
import { formSchema } from './hooks';

interface Props<T extends Partial<Navigation>> {
navigation: T;
Expand All @@ -25,74 +23,135 @@ export const Form = <T extends Partial<Navigation>>({
alreadyUsedNames = [],
isLoading,
}: Props<T>) => {
const { control, watch } = useNavigationForm({ alreadyUsedNames, navigation });
const [name, visible] = watch(['name', 'visible']);

const onChangeLimited = debounce(onChange, 300);
const [formValue, setFormValue] = useState<T>({} as T);
const [formError, setFormError] = useState<FormItemErrorSchema<T>>();

const { formatMessage } = useIntl();

const {
name,
visible,
} = formValue;

const handleChange = (eventOrPath: FormChangeEvent, value?: any, nativeOnChange?: (eventOrPath: FormChangeEvent, value?: any) => void) => {
if (nativeOnChange) {

let fieldName = eventOrPath;
let fieldValue = value;

if (isObject(eventOrPath)) {
const { name: targetName, value: targetValue } = eventOrPath.target;
fieldName = targetName;
fieldValue = isNil(fieldValue) ? targetValue : fieldValue;
}

if (isString(fieldName)) {
setFormValueItem(fieldName, fieldValue);
}

return nativeOnChange(eventOrPath as FormChangeEvent, fieldValue);
}
};

const setFormValueItem = (path: string, value: any) => {
setFormValue(set({
...formValue,
}, path, value));
};

const renderError = (error: string): string | undefined => {
const errorOccurence = get(formError, error);
if (errorOccurence) {
return errorOccurence;
}
return undefined;
};

useEffect(() => {
if (navigation) {
if (navigation.name) {
setFormValue({
...navigation
} as T);
} else {
setFormValue({
name: 'New navigation',
visible: true,
} as T);

onChange({
name: 'New navigation',
visible: true,
disabled: true,
} as unknown as T);
}
}
}, []);

useEffect(() => {
if (`${name}-${visible}` !== `${navigation.name}-${navigation.visible}`) {
onChangeLimited({
if ((`${name}-${visible}` !== `${navigation.name}-${navigation.visible}`)) {
const { error } = formSchema({ alreadyUsedNames }).safeParse(formValue);

onChange({
...navigation,
name,
visible,
disabled: !isEmpty(error?.issues),
});
if (error) {
setFormError(error.issues.reduce((acc, err) => {
return {
...acc,
[err.path.join('.')]: err.message
}
}, {} as FormItemErrorSchema<T>));
} else {
setFormError(undefined);
}
}
}, [name, visible, navigation]);

return (
<Grid.Root gap={5}>
<Grid.Item col={6}>
<Controller
control={control}
name="name"
render={({ field: { value, onChange }, fieldState }) => (
<Field.Root width="100%" error={fieldState.error?.message}>
<Field.Label>
{formatMessage(getTrad('popup.navigation.form.name.label', 'Name'))}
</Field.Label>

<TextInput
name="name"
type="string"
placeholder={formatMessage(
getTrad('popup.navigation.form.name.placeholder', "Navigations's name")
)}
onChange={onChange}
value={value}
disabled={isLoading}
/>

<Field.Error />
</Field.Root>
)}
/>
</Grid.Item>
<Grid.Item col={6}>
<Controller
control={control}
name="visible"
render={({ field: { onChange, value }, fieldState }) => (
<Field.Root error={fieldState.error?.message}>
<Field.Label>
{formatMessage(getTrad('popup.navigation.form.visible.label', 'Visibility'))}
</Field.Label>

<Checkbox
name="visible"
checked={value}
onCheckedChange={onChange}
disabled={isLoading}
/>

<Field.Hint>{formatMessage(getTrad('popup.item.form.visible.label'))}</Field.Hint>
<Field.Error />
</Field.Root>
)}
/>
</Grid.Item>
</Grid.Root>
}, [name, visible]);

return (<StrapiForm
method="POST"
initialValues={formValue}
>
{({ values, onChange }) => {
return (<Grid.Root gap={5}>
<Grid.Item col={6}>
<Field
name="name"
label={formatMessage(getTrad('popup.navigation.form.name.label', 'Name'))}
error={renderError('name')}>
<TextInput
name="name"
type="string"
placeholder={formatMessage(
getTrad('popup.navigation.form.name.placeholder', "Navigations's name")
)}
onChange={(eventOrPath: FormChangeEvent, value?: any) => handleChange(eventOrPath, value, onChange)}
value={values.name}
disabled={isLoading}
/>
</Field>
</Grid.Item>
<Grid.Item col={6}>
<Field
name="visible"
label={formatMessage(getTrad('popup.navigation.form.visible.label', 'Visibility'))}
error={renderError('visible')}>
<Toggle
name="visible"
checked={values.visible}
onChange={(eventOrPath: FormChangeEvent) => handleChange(eventOrPath, !values.visible, onChange)}
onLabel={formatMessage(getTrad('popup.navigation.form.visible.toggle.visible'))}
offLabel={formatMessage(getTrad('popup.navigation.form.visible.toggle.hidden'))}
disabled={isLoading}
width="100%"
/>
</Field>
</Grid.Item>
</Grid.Root>);
}}
</StrapiForm>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ export const NavigationUpdate = ({
const navigation: Navigation = useMemo(() => current ?? initialValue, [current]);

const onChange: Effect<Navigation> = useCallback(
(updated) => {
({disabled, ...updated}: Navigation & { disabled?: boolean }) => {
setState({
view: 'EDIT',
alreadyUsedNames,
current: updated,
disabled,
navigation: initialValue,
});
},
Expand All @@ -40,7 +41,7 @@ export const NavigationUpdate = ({
);
};

export const NavigationUpdateFooter: Footer = ({ onSubmit, onReset, isLoading }) => {
export const NavigationUpdateFooter: Footer = ({ onSubmit, onReset, disabled, isLoading }) => {
const { formatMessage } = useIntl();

return (
Expand All @@ -53,7 +54,7 @@ export const NavigationUpdateFooter: Footer = ({ onSubmit, onReset, isLoading })
}}
end={{
children: formatMessage(getTrad('popup.navigation.manage.button.save')),
disabled: isLoading,
disabled: isLoading || disabled,
onClick: onSubmit,
variant: 'secondary',
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import { CommonProps, CreateState, Navigation, NewNavigation as NewNavigationTyp
interface Props extends CreateState, CommonProps {}

export const INITIAL_NAVIGATION = {
name: 'Navigation',
name: '',
items: [],
visible: true,
visible: false,
} as unknown as Navigation;

export const NewNavigation = ({ setState, current, isLoading, alreadyUsedNames }: Props) => {
const onSubmit = useCallback(
(updated: NewNavigationType) => {
({disabled, ...updated}: NewNavigationType & { disabled?: boolean }) => {
setState({
view: 'CREATE',
current: updated,
alreadyUsedNames,
disabled,
});
},
[setState]
Expand All @@ -36,7 +37,7 @@ export const NewNavigation = ({ setState, current, isLoading, alreadyUsedNames }
);
};

export const NewNavigationFooter: Footer = ({ onSubmit, onReset, isLoading }) => {
export const NewNavigationFooter: Footer = ({ onSubmit, onReset, disabled, isLoading }) => {
const { formatMessage } = useIntl();

return (
Expand All @@ -50,7 +51,7 @@ export const NewNavigationFooter: Footer = ({ onSubmit, onReset, isLoading }) =>
end={{
children: formatMessage(getTrad('popup.navigation.manage.button.save')),
variant: 'default',
disabled: isLoading,
disabled: isLoading || disabled,
onClick: onSubmit,
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,10 @@ const renderFooter: Footer = (props) => {
return <AllNavigationsFooter {...props} />;
}
case 'CREATE': {
return <NewNavigationFooter {...props} />;
return <NewNavigationFooter {...props} disabled={props.state.disabled} />;
}
case 'EDIT': {
return <NavigationUpdateFooter {...props} />;
return <NavigationUpdateFooter {...props} disabled={props.state.disabled} />;
}
case 'DELETE': {
return <DeleteConfirmFooter {...props} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ export interface EditState extends CommonState {
view: 'EDIT';
navigation: Navigation;
current: Navigation;
disabled?: boolean;
alreadyUsedNames: Array<string>;
}

export interface CreateState extends CommonState {
view: 'CREATE';
current: NewNavigation;
disabled?: boolean;
alreadyUsedNames: Array<string>;
}

Expand Down
Loading

0 comments on commit f14568b

Please sign in to comment.