From e55c33069bf5a52c2489551c74c96b4ba56bc0a7 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:14:15 +0700 Subject: [PATCH] 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 --- .../components/validation-bank-demo.tsx | 175 +++++--- .../domain/validators/full-page.validator.ts | 17 +- .../full-page/presentation/factory/index.tsx | 6 +- .../presentation/languages/en/full-page.json | 2 + .../presentation/languages/id/full-page.json | 2 + .../pages/full-page.page.form.tsx | 171 ++------ .../core-i18n/src/languages/en/common.json | 1 + .../core-i18n/src/languages/id/common.json | 1 + .../src/languages/id/validation.json | 10 +- .../custom/selects/hooks/useAsyncPaginate.ts | 14 +- .../components/data-table/index.tsx | 13 +- .../enterprise-module/entities/entity.ts | 6 +- .../hooks/use-form-page.context.ts | 5 +- .../foundations/enterprise-module/index.ts | 2 + .../providers/detail-page.provider.tsx | 271 +++++++----- .../providers/form-page.provider.tsx | 406 ++++++++++++++++++ .../providers/index-page.provider.tsx | 17 +- .../providers/module.provider.tsx | 20 +- 18 files changed, 776 insertions(+), 363 deletions(-) diff --git a/apps/showcase/src/pages/forms/components/form-demo/components/validation-bank-demo.tsx b/apps/showcase/src/pages/forms/components/form-demo/components/validation-bank-demo.tsx index 029c089..50d0a92 100644 --- a/apps/showcase/src/pages/forms/components/form-demo/components/validation-bank-demo.tsx +++ b/apps/showcase/src/pages/forms/components/form-demo/components/validation-bank-demo.tsx @@ -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 = 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 = 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; @@ -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() {
- {t.sections.validationBankTitle} + + {t.sections.validationBankTitle} + - - - + - - - - - - {t.sections.objectLevelValidations} + + {t.sections.objectLevelValidations} + - + name="department" control={control as any} @@ -176,7 +203,7 @@ export default function ValidationBankDemo() { clearable withAsterisk /> - + multiple name="assignees" @@ -190,9 +217,11 @@ export default function ValidationBankDemo() { withAsterisk /> - {t.sections.validatedPrefilledObjects} + + {t.sections.validatedPrefilledObjects} + - + - {t.sections.richTextValidations} + + {t.sections.richTextValidations} + - +
- {t.common.submittedData} + + {t.common.submittedData} + {JSON.stringify(data, null, 2)} diff --git a/apps/web/src/apps/modules/example/full-page/domain/validators/full-page.validator.ts b/apps/web/src/apps/modules/example/full-page/domain/validators/full-page.validator.ts index a32d8e6..3d52662 100644 --- a/apps/web/src/apps/modules/example/full-page/domain/validators/full-page.validator.ts +++ b/apps/web/src/apps/modules/example/full-page/domain/validators/full-page.validator.ts @@ -12,22 +12,9 @@ import { compose, required, rangeLength } from '@repo/ui/validators'; export const createFullPageSchema = (t: any) => { return z.object({ // Code: Required, length between 3 and 10 characters. - code: compose(z.string(), required(t.fields.code), rangeLength(3, 10, t.fields.code)), + code: compose(z.string(), required(t('fields.code'))), // Name: Required, length between 3 and 50 characters. - name: compose(z.string(), required(t.fields.name), rangeLength(3, 50, t.fields.name)), - - // Status: Required selection (typically from a dropdown/select). - status: compose(z.string(), required(t.fields.status)), - - // Description: Optional text field. - description: z.string().optional(), + name: compose(z.string(), required(t('fields.name')), rangeLength(3, 50, t('fields.name'))), }); }; - -/** - * Data Transfer Object (DTO) for the Full Page form. - * This type is automatically inferred from the Zod schema factory. - * Use this type as a generic for form initialization, e.g., `useForm()`. - */ -export type FullPageFormDTO = z.infer>; diff --git a/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx b/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx index a1415d8..add31b8 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx +++ b/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx @@ -37,9 +37,9 @@ export default function FullPageModule() { } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> } /> } /> diff --git a/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json b/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json index 766c70d..c460661 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json +++ b/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json @@ -2,10 +2,12 @@ "title": "Full Page", "detail_page_title": "Detail Full Page", "create_page_title": "New Full Page", + "edit_page_title": "Edit Full Page", "duplicate_page_title": "Duplicate Full Page", "description": "An example module of a <1>full page layout for detailed forms.", "detail_page_description": "View and manage detailed information for this record.", "create_page_description": "Fill out the form below to add a new record to the system.", + "edit_page_description": "Update and modify the information of the selected record.", "duplicate_page_description": "Copy and modify information from an existing record to quickly create a new one.", "fields": { "code": "Code", diff --git a/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json b/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json index 2d2b36f..8e59d95 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json +++ b/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json @@ -2,10 +2,12 @@ "title": "Halaman Penuh", "detail_page_title": "Detail Halaman Penuh", "create_page_title": "Buat Halaman Penuh Baru", + "edit_page_title": "Ubah Halaman Penuh", "duplicate_page_title": "Duplikat Halaman Penuh", "description": "Contoh penerapan <1>tata letak halaman penuh untuk formulir detail.", "detail_page_description": "Lihat dan kelola informasi terperinci terkait data ini.", "create_page_description": "Lengkapi formulir di bawah ini untuk menambahkan data baru ke dalam sistem.", + "edit_page_description": "Perbarui dan ubah informasi pada data yang dipilih.", "duplicate_page_description": "Salin dan sesuaikan informasi dari data yang sudah ada untuk mempercepat pembuatan data baru.", "fields": { "code": "Kode", diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.form.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.form.tsx index a6f3f60..bc69850 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.form.tsx +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.form.tsx @@ -1,146 +1,69 @@ -import { - Paper, - Box, - Text, - Divider, - TextInput, - Select, - Textarea, - Stack, - Breadcrumbs, - Anchor, - Flex, - Title, - Button, - Group, -} from '@repo/ui/components'; -import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations'; -import { ChevronRight, Database } from 'lucide-react'; +import { Paper, Box, Stack } from '@repo/ui/components'; +import { FieldTextInput } from '@repo/ui/form'; +import { useEnterpriseModuleTranslationContext, EnterpriseFormPageProvider, FormPageType } from '@repo/ui/foundations'; +import { useMemo } from 'react'; +import { fullPageModuleConfig } from '../../domain/constants'; +import { createFullPageSchema } from '../../domain/validators/full-page.validator'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; -export default function FullPagePageForm({ formPageType }: { formPageType: 'edit' | 'create' | 'duplicate' }) { +export default function FullPagePageForm({ formPageType }: { formPageType: FormPageType }) { const { t } = useEnterpriseModuleTranslationContext(); + const title = useMemo(() => { + if (formPageType === 'CREATE') { + return { title: t('create_page_title'), description: t('create_page_description') }; + } else if (formPageType === 'EDIT') { + return { title: t('edit_page_title'), description: t('edit_page_description') }; + } else if (formPageType === 'DUPLICATE') { + return { title: t('duplicate_page_title'), description: t('duplicate_page_description') }; + } + return { title: '', description: '' }; + }, [formPageType, t]); + + const validator = useMemo(() => { + return createFullPageSchema(t); + }, [t]); + + const formControl = useForm({ resolver: zodResolver(validator) }); + return ( - - {/* --- INLINED PAGE HEADER --- */} - - } - > - - Database Clusters - - - {formPageType === 'create' ? 'Create Cluster' : 'Edit Cluster'} - - - - - - - - {formPageType === 'create' ? 'Create New Cluster' : 'Edit Cluster Configuration'} - - - Configure and provision a new highly available database cluster. - - - - - - - - - - - - - {/* --- END INLINED PAGE HEADER --- */} - + - - {formPageType === 'create' ? 'Create Entity' : 'Edit Entity'} - - - Fill in the required information to configure your entity properly. Fields marked with * are required. - - - - - - -