Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save wildfrontend/dc4404d005b13ff13479b9099aaffb93 to your computer and use it in GitHub Desktop.
Save wildfrontend/dc4404d005b13ff13479b9099aaffb93 to your computer and use it in GitHub Desktop.
tab form vaildation error switch
import { AddAuthor } from '@gosugamers/backoffice-api-types';
import { DevTool } from '@hookform/devtools';
import { Button, Card, Space, Tabs, TabsProps } from 'antd';
import { Base64 } from 'js-base64';
import _ from 'lodash';
import React, {
ComponentProps,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import {
FormProvider,
UseFormReturn,
useForm,
useFormContext,
} from 'react-hook-form';
import { toast } from 'react-toastify';
import { createArticle, updapteArticle } from 'apis/articles';
import ErrorMessage from 'components/error/message';
import { ApiErrorCode } from 'constants/api-code';
import { EXPIRATION_TIME_MS } from 'constants/editorial';
import { ArticleFormValues, ArticlePermission } from 'types/pages/articles';
import { axiosError } from 'utils/global/axios';
import dayjs from 'utils/global/day-js';
import LoadPreviousButton from '../editor-form/load-previous';
import ContentForm from './content';
import ExtraForm from './extra';
import RatingForm from './rating';
import SEOForm from './seo';
import SettingsForm from './settings';
/**
* ANCHOR Form Panel
*/
const items: TabsProps['items'] = [
{
key: '1',
label: `Content`,
children: <ContentForm />,
},
{
key: '2',
label: `Article settings`,
children: <SettingsForm />,
},
{
key: '3',
label: `Related contents`,
children: <ExtraForm />,
},
{
key: '4',
label: `Reviews & ratings`,
children: <RatingForm />,
},
{
key: '5',
label: `SEO tools`,
children: <SEOForm />,
},
];
type ArticleFormKey = ObjectDotNotation<ArticleFormValues>;
export enum ArticleFormMode {
create = 'create',
edit = 'edit',
view = 'view',
}
const contentFormKeys: ArticleFormKey[] = [
'title',
'teaser',
'headlineImageText',
'headlineImgFile',
'content',
];
const contentFormRequiredKeys: ArticleFormKey[] = [
'title',
'teaser',
'content',
];
const settingsFormKeys: ArticleFormKey[] = [
// 'subTypeId',
'frontendId',
'localeId',
'siteSectionIds',
'isFeatured',
'isPublished',
'isSectionSticky',
'publishedAt',
// 'timeSpent',
'isTgPush',
'authors',
];
const settingsFormRequiredKeys: ArticleFormKey[] = [
// 'subTypeId',
'frontendId',
'localeId',
'siteSectionIds',
'isFeatured',
'isSectionSticky',
// 'timeSpent',
'authors',
];
const extraFormKey: ArticleFormKey[] = [
'teamIds',
'tournamentIds',
// 'quickpoll',
];
const seoFormKey: ArticleFormKey[] = ['metadata'];
const formatDefaultValues = (
initialValues?: Partial<ArticleFormValues>
): Partial<ArticleFormValues> => {
if (initialValues) {
return {
// subTypeId: initialValues?.subTypeId,
title: initialValues?.title,
teaser: initialValues?.teaser,
content: initialValues?.content,
frontendId: initialValues?.frontendId,
localeId: initialValues?.localeId,
headlineImageText: initialValues?.headlineImageText,
headlineImageCaption: initialValues?.headlineImageCaption,
siteSectionIds: initialValues?.siteSectionIds,
isSectionSticky: initialValues?.isSectionSticky,
isFeatured: initialValues?.isFeatured,
isPublished: initialValues?.isPublished,
publishedAt: initialValues.publishedAt,
isProofread: initialValues?.isProofread,
// timeSpentProofreading: initialValues?.timeSpentProofreading,
// timeSpent: initialValues?.timeSpent,
teamIds: initialValues?.teamIds,
playerIds: initialValues?.playerIds,
authors: initialValues?.authors,
// quickpoll: initialValues?.quickpoll,
metadata: initialValues?.metadata,
isTgPush: initialValues?.isTgPush,
ratings: initialValues?.ratings,
recommendation: initialValues?.recommendation,
};
}
return {
isFeatured: false,
isSectionSticky: false,
isTgPush: true,
};
};
const useKeepPreviousValues = (
methods: UseFormReturn<ArticleFormValues, any, undefined>
) => {
const formData = methods.watch();
const articleStoreKey = `articles_create_${localStorage.getItem('userId')}`;
const storeFormValues = useCallback(
(data: ArticleFormValues) => {
const metadata = data.metadata?.filter(
(value) => value.id && value.content
);
localStorage.setItem(
articleStoreKey,
JSON.stringify({
data: {
...data,
// filter empty
metadata: (metadata?.length ?? 0) > 0 ? metadata : undefined,
},
expirationTime: dayjs()
.add(EXPIRATION_TIME_MS, 'millisecond')
.toISOString(),
})
);
},
[articleStoreKey]
);
const loadPreviousValues = useCallback(() => {
const previousValues = localStorage.getItem(articleStoreKey);
if (previousValues) {
methods.reset(JSON.parse(previousValues).data);
}
}, [articleStoreKey, methods]);
const removePreviousValues = useCallback(() => {
console.log('remove');
localStorage.removeItem(articleStoreKey);
}, [articleStoreKey]);
const [hasPreviousValues, setHasPreviousValues] = useState(false);
useEffect(() => {
if (!_.isEmpty(methods.formState.dirtyFields)) {
storeFormValues(formData);
}
}, [formData, methods.formState.dirtyFields, storeFormValues]);
// when expired
useEffect(() => {
const interval = setInterval(() => {
const previousValues = localStorage.getItem(articleStoreKey);
if (previousValues) {
const { expirationTime } = JSON.parse(previousValues);
if (dayjs(expirationTime).isBefore(dayjs())) {
removePreviousValues();
}
}
setHasPreviousValues(!!previousValues);
}, 1000);
return () => clearInterval(interval);
}, [removePreviousValues, articleStoreKey]);
return {
hasPreviousValues,
loadPreviousValues,
removePreviousValues,
};
};
const useArticleForm = ({
initialValues,
formMode,
permission,
}: {
initialValues: ComponentProps<typeof ArticleForm>['initialValues'];
formMode: ComponentProps<typeof ArticleForm>['formMode'];
permission: ComponentProps<typeof ArticleForm>['permission'];
}) => {
const methods = useForm<ArticleFormValues>({
defaultValues: formatDefaultValues(initialValues),
});
const hasFormError = useMemo(
() => (list: ArticleFormKey[]) => {
return list.some((key) =>
Object.keys(methods.formState.errors).includes(key)
);
},
[methods.formState.errors]
);
const needRequired = useMemo(
() => (list: ArticleFormKey[]) => {
return !list.every((key) => {
return Object.keys(methods.getValues()).includes(key);
});
},
[methods]
);
const isCreate = useMemo(
() => formMode === ArticleFormMode.create,
[formMode]
);
const isEdit = useMemo(() => formMode === ArticleFormMode.edit, [formMode]);
const { hasPreviousValues, loadPreviousValues, removePreviousValues } =
useKeepPreviousValues(methods);
return {
...methods,
formMode,
initialValues,
isCreate,
isEdit,
permission,
hasPreviousValues,
hasFormError,
needRequired,
loadPreviousValues,
removePreviousValues,
};
};
export const useArticleFormContext = () => {
const methods = useFormContext<ArticleFormValues>();
return { ...methods } as ReturnType<typeof useArticleForm>;
};
/**
* ANCHOR Form
*/
type ArticleCreateFormProps = {
initialValues?: undefined;
formMode: ArticleFormMode.create;
permission: Partial<ArticlePermission>;
onSuccess?: (value: Awaited<ReturnType<typeof createArticle>>) => void;
onError?: (error: any) => void;
onCancel?: () => void;
};
type ArticleEditFormProps = {
initialValues?: Partial<ArticleFormValues>;
formMode: ArticleFormMode.edit;
permission: Partial<ArticlePermission>;
onSuccess?: (value: Awaited<ReturnType<typeof updapteArticle>>) => void;
onError?: (error: any) => void;
onCancel?: () => void;
};
type ArticleViewFormProps = {
initialValues?: Partial<ArticleFormValues>;
formMode: ArticleFormMode.view;
permission: Partial<ArticlePermission>;
onSuccess?: (value: Awaited<ReturnType<typeof updapteArticle>>) => void;
onError?: (error: any) => void;
onCancel?: () => void;
};
const ArticleForm: React.FC<
ArticleCreateFormProps | ArticleEditFormProps | ArticleViewFormProps
> = ({ initialValues, formMode, permission, onSuccess, onError, onCancel }) => {
const methods = useArticleForm({ formMode, permission, initialValues });
const [tab, setTab] = useState<string>(items[0]['key']);
const onSubmit = useCallback(async () => {
await methods.trigger();
const isUnfinishContentValue = methods.needRequired(
contentFormRequiredKeys
);
const isUnfinishSettingsValue = methods.needRequired(
settingsFormRequiredKeys
);
const isContentFormError = methods.hasFormError(contentFormKeys);
const isSettingsFormError = methods.hasFormError(settingsFormKeys);
const isExtraFormError = methods.hasFormError(extraFormKey);
const isSeoFormError = methods.hasFormError(seoFormKey);
if (isUnfinishContentValue || isContentFormError) {
setTab(items[0]['key']);
} else if (isUnfinishSettingsValue || isSettingsFormError) {
setTab(items[1]['key']);
} else if (isExtraFormError) {
setTab(items[2]['key']);
} else if (isSeoFormError) {
setTab(items[4]['key']);
} else {
methods.handleSubmit(async (data) => {
if (formMode === ArticleFormMode.edit) {
try {
const isClearAllPlayers =
data.playerIds && data.playerIds.length === 0 ? true : false;
const isClearAllTeams =
data.teamIds && data.teamIds.length === 0 ? true : false;
const isClearAllTournaments =
data.tournamentIds && data.tournamentIds.length === 0
? true
: false;
const result = await updapteArticle({
...data,
id: initialValues?.id,
isClearAllPlayers,
isClearAllTeams,
isClearAllTournaments,
metadata:
data.metadata && data.metadata.length > 0
? JSON.stringify(data.metadata)
: undefined,
content: Base64.encode(data.content),
authors: data.authors.map<AddAuthor>((o) => ({
userId: o.userId!,
order: o.order!,
})),
});
console.log('success', result);
toast.success('Updated successfully');
onSuccess?.(result);
} catch (error) {
toast.error(
<ErrorMessage
error={axiosError<ApiErrorCode>(error)}
message={{ moduleName: 'Article', action: 'update' }}
/>
);
onError?.(error);
}
} else {
console.log('data', data.metadata);
try {
const result = await createArticle({
...data,
metadata:
data.metadata && data.metadata.length > 0
? JSON.stringify(data.metadata)
: undefined,
content: Base64.encode(data.content),
authors: data.authors.map<AddAuthor>((o) => ({
userId: o.userId!,
order: o.order!,
})),
});
console.log('success', result);
methods.removePreviousValues();
toast.success('Create article successfully');
onSuccess?.(result);
} catch (error) {
toast.error(
<ErrorMessage
error={axiosError<ApiErrorCode>(error)}
message={{ moduleName: 'Article', action: 'create' }}
/>
);
onError?.(error);
}
}
})();
}
}, [methods, initialValues?.id, formMode, onSuccess, onError]);
return (
<FormProvider {...methods}>
<Card>
<Tabs
activeKey={tab}
className="mx-auto mt-[20px] max-w-[890px]"
items={items}
onChange={(activeKey) => {
setTab(activeKey);
}}
tabBarExtraContent={
<div className="relative">
{formMode === ArticleFormMode.create && (
<div className="absolute -top-[40px] mb-2">
<LoadPreviousButton />
</div>
)}
<Space wrap>
<Button
disabled={formMode === ArticleFormMode.view}
loading={methods.formState.isSubmitting}
onClick={onSubmit}
type="primary"
>
Save & Close
</Button>
<Button
disabled={formMode === ArticleFormMode.view}
onClick={onCancel}
>
Cancel
</Button>
</Space>
</div>
}
/>
</Card>
<DevTool control={methods.control} />
</FormProvider>
);
};
export default ArticleForm;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment