feat(i18n): add "save_changes" translations for English and Indonesian
fix(validation): improve Indonesian validation messages for clarity refactor(ui): optimize useAsyncPaginate hook for better readability fix(data-table): enhance error handling with detailed messages in EnterpriseDataTable feat(enterprise-module): add form control to EnterpriseFormPageConfig and implement save confirmation modal refactor(form-page): streamline form page context and improve save handling logic feat(index-page): set document title based on config or translation key refactor(module-provider): clean up comments and improve code organization
This commit is contained in:
+105
-70
@@ -3,9 +3,13 @@ import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Button, Paper, Title, Group, Stack, Code, Divider } from '@repo/ui/components';
|
||||
import {
|
||||
FieldTextInput, FieldPasswordInput, FieldNumberInput,
|
||||
FieldLocalSelect, FieldAsyncSelect, FieldRichTextEditor
|
||||
import {
|
||||
FieldTextInput,
|
||||
FieldPasswordInput,
|
||||
FieldNumberInput,
|
||||
FieldLocalSelect,
|
||||
FieldAsyncSelect,
|
||||
FieldRichTextEditor,
|
||||
} from '@repo/ui/form';
|
||||
import type { LoadOptionsFn } from '@repo/ui/form';
|
||||
|
||||
@@ -30,16 +34,16 @@ const mockFetchUsers: LoadOptionsFn<Assignee> = async (search, page) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const allUsers = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
email: `user${i + 1}@company.com`
|
||||
email: `user${i + 1}@company.com`,
|
||||
}));
|
||||
const filtered = allUsers.filter(u => u.email.toLowerCase().includes(search.toLowerCase()));
|
||||
const filtered = allUsers.filter((u) => u.email.toLowerCase().includes(search.toLowerCase()));
|
||||
const pageSize = 5;
|
||||
const start = (page - 1) * pageSize;
|
||||
const paginated = filtered.slice(start, start + pageSize);
|
||||
|
||||
|
||||
return {
|
||||
options: paginated,
|
||||
hasMore: start + pageSize < filtered.length
|
||||
hasMore: start + pageSize < filtered.length,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,34 +54,53 @@ const MOCK_VENDORS = [
|
||||
|
||||
const mockFetchVendors: LoadOptionsFn<any> = async (search, _page) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const filtered = MOCK_VENDORS.filter(v => v.name.toLowerCase().includes(search.toLowerCase()) || v.code.toLowerCase().includes(search.toLowerCase()));
|
||||
const filtered = MOCK_VENDORS.filter(
|
||||
(v) => v.name.toLowerCase().includes(search.toLowerCase()) || v.code.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
return { options: filtered, hasMore: false };
|
||||
};
|
||||
import {
|
||||
compose, required, rangeLength,
|
||||
positiveNumber, simplePassword,
|
||||
complexPassword, phoneValidator, rangeValue
|
||||
import {
|
||||
compose,
|
||||
required,
|
||||
rangeLength,
|
||||
positiveNumber,
|
||||
simplePassword,
|
||||
complexPassword,
|
||||
phoneValidator,
|
||||
rangeValue,
|
||||
} from '@repo/ui/validators';
|
||||
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
|
||||
|
||||
export default function ValidationBankDemo() {
|
||||
const t = useFormDemoTranslation();
|
||||
|
||||
|
||||
// Compose the Zod schema using the atomic validators
|
||||
const validationSchema = useMemo(() => z.object({
|
||||
username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)),
|
||||
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
|
||||
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
|
||||
age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)),
|
||||
score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)),
|
||||
phone: compose(z.string(), required(t.validation.phone), phoneValidator()),
|
||||
department: z.object({ code: z.string(), name: z.string() }, { required_error: t.errors.departmentRequired }),
|
||||
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, t.errors.min2Assignees),
|
||||
prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
|
||||
emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
|
||||
prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, t.errors.min1Vendor),
|
||||
richTextNotes: z.string().min(15, t.errors.notesMin15),
|
||||
}), [t]);
|
||||
const validationSchema = useMemo(
|
||||
() =>
|
||||
z.object({
|
||||
username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)),
|
||||
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
|
||||
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
|
||||
age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)),
|
||||
score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)),
|
||||
phone: compose(z.string(), required(t.validation.phone), phoneValidator()),
|
||||
department: z.object({ code: z.string(), name: z.string() }, { required_error: t.errors.departmentRequired }),
|
||||
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, t.errors.min2Assignees),
|
||||
prefilledVendor: z.object(
|
||||
{ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() },
|
||||
{ required_error: t.errors.vendorRequired },
|
||||
),
|
||||
emptyVendor: z.object(
|
||||
{ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() },
|
||||
{ required_error: t.errors.vendorRequired },
|
||||
),
|
||||
prefilledAsyncMulti: z
|
||||
.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }))
|
||||
.min(1, t.errors.min1Vendor),
|
||||
richTextNotes: z.string().min(15, t.errors.notesMin15),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
type ValidationFormValues = z.infer<typeof validationSchema>;
|
||||
|
||||
@@ -96,10 +119,10 @@ export default function ValidationBankDemo() {
|
||||
emptyVendor: null as any,
|
||||
prefilledAsyncMulti: [
|
||||
{ id: 'V1', code: 'VN-01', name: 'Vendor One' },
|
||||
{ id: 'V2', code: 'VN-02', name: 'Vendor Two' }
|
||||
{ id: 'V2', code: 'VN-02', name: 'Vendor Two' },
|
||||
] as any,
|
||||
richTextNotes: '',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: ValidationFormValues) => console.log('Validation Passed:', data);
|
||||
@@ -110,62 +133,66 @@ export default function ValidationBankDemo() {
|
||||
<Paper p="xl" withBorder radius="md">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="md">
|
||||
<Title order={5} c="brand">{t.sections.validationBankTitle}</Title>
|
||||
<Title order={5} c="brand">
|
||||
{t.sections.validationBankTitle}
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
<FieldTextInput
|
||||
name="username"
|
||||
control={control}
|
||||
label={t.fields.customerName}
|
||||
|
||||
<FieldTextInput
|
||||
name="username"
|
||||
control={control}
|
||||
label={t.fields.customerName}
|
||||
description={t.validation.usernameRange}
|
||||
withAsterisk
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<FieldPasswordInput
|
||||
name="simplePass"
|
||||
control={control}
|
||||
label={t.validation.simplePassword}
|
||||
<FieldPasswordInput
|
||||
name="simplePass"
|
||||
control={control}
|
||||
label={t.validation.simplePassword}
|
||||
description={t.descriptions.min6Chars}
|
||||
withAsterisk
|
||||
withAsterisk
|
||||
/>
|
||||
<FieldPasswordInput
|
||||
name="complexPass"
|
||||
control={control}
|
||||
label={t.validation.complexPassword}
|
||||
<FieldPasswordInput
|
||||
name="complexPass"
|
||||
control={control}
|
||||
label={t.validation.complexPassword}
|
||||
description={t.descriptions.min8Complex}
|
||||
withAsterisk
|
||||
withAsterisk
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<FieldNumberInput
|
||||
name="age"
|
||||
control={control}
|
||||
label={t.fields.age}
|
||||
<FieldNumberInput
|
||||
name="age"
|
||||
control={control}
|
||||
label={t.fields.age}
|
||||
description={t.validation.ageRange}
|
||||
withAsterisk
|
||||
withAsterisk
|
||||
/>
|
||||
<FieldNumberInput
|
||||
name="score"
|
||||
control={control}
|
||||
label={t.validation.score}
|
||||
<FieldNumberInput
|
||||
name="score"
|
||||
control={control}
|
||||
label={t.validation.score}
|
||||
description={t.descriptions.mustBePositive}
|
||||
withAsterisk
|
||||
withAsterisk
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<FieldTextInput
|
||||
name="phone"
|
||||
control={control}
|
||||
label={t.validation.phone}
|
||||
<FieldTextInput
|
||||
name="phone"
|
||||
control={control}
|
||||
label={t.validation.phone}
|
||||
description={t.descriptions.formatPhone}
|
||||
withAsterisk
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Title order={5} c="brand" mt="md">{t.sections.objectLevelValidations}</Title>
|
||||
<Title order={5} c="brand" mt="md">
|
||||
{t.sections.objectLevelValidations}
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
|
||||
<FieldLocalSelect<Department>
|
||||
name="department"
|
||||
control={control as any}
|
||||
@@ -176,7 +203,7 @@ export default function ValidationBankDemo() {
|
||||
clearable
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
|
||||
<FieldAsyncSelect<Assignee>
|
||||
multiple
|
||||
name="assignees"
|
||||
@@ -190,9 +217,11 @@ export default function ValidationBankDemo() {
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Title order={5} c="brand" mt="lg">{t.sections.validatedPrefilledObjects}</Title>
|
||||
<Title order={5} c="brand" mt="lg">
|
||||
{t.sections.validatedPrefilledObjects}
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<FieldAsyncSelect
|
||||
name="emptyVendor"
|
||||
@@ -229,7 +258,9 @@ export default function ValidationBankDemo() {
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Title order={5} c="brand" mt="lg">{t.sections.richTextValidations}</Title>
|
||||
<Title order={5} c="brand" mt="lg">
|
||||
{t.sections.richTextValidations}
|
||||
</Title>
|
||||
<Divider mb="sm" />
|
||||
|
||||
<FieldRichTextEditor
|
||||
@@ -240,13 +271,17 @@ export default function ValidationBankDemo() {
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<Button type="submit" mt="md">{t.common.submit}</Button>
|
||||
<Button type="submit" mt="md">
|
||||
{t.common.submit}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" withBorder radius="md" bg="var(--mantine-color-gray-0)">
|
||||
<Title order={6} mb="xs">{t.common.submittedData}</Title>
|
||||
<Title order={6} mb="xs">
|
||||
{t.common.submittedData}
|
||||
</Title>
|
||||
<Code block>{JSON.stringify(data, null, 2)}</Code>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user