diff --git a/apps/web/src/apps/modules/example/full-page/presentation/components/filter-content.tsx b/apps/web/src/apps/modules/example/full-page/presentation/components/filter-content.tsx new file mode 100644 index 0000000..ba4d61f --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/presentation/components/filter-content.tsx @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; +import { SimpleGrid } from '@repo/ui/components'; +import { FieldTextInput } from '@repo/ui/form'; +import { UseFormReturn, useWatch } from 'react-hook-form'; + +export const FilterFormContent = ({ form, t }: { form: UseFormReturn; t: any }) => { + const codeValue = useWatch({ + control: form.control, + name: 'code', + }); + + useEffect(() => { + if (!codeValue) { + form.setValue('name', ''); + form.clearErrors('name'); + } + }, [codeValue, form]); + + return ( + + + + + + ); +}; diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx index f142aa1..94f4467 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx @@ -4,10 +4,27 @@ import { EnterpriseDataTable, } from '@repo/ui/foundations'; import { ColDef } from '@repo/ui/components'; +import { z } from 'zod'; import { Trans } from '@repo/core-i18n'; import { Text } from '@repo/ui/components'; import { LayoutDashboard } from 'lucide-react'; import { useMemo } from 'react'; +import { FilterFormContent } from '../components/filter-content'; + +const filterSchema = z + .object({ + code: z.string().optional(), + name: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.code && !data.name) { + ctx.addIssue({ + path: ['name'], + code: z.ZodIssueCode.custom, + message: 'Name is required when code is provided', + }); + } + }); export default function FullPagePageIndex() { const { t } = useEnterpriseModuleTranslationContext(); @@ -19,6 +36,16 @@ export default function FullPagePageIndex() { ]; }, [t]); + const filterConfig = useMemo(() => { + return { + schema: filterSchema, + renderBody: (form: any) => { + if (!form) return null; + return ; + }, + }; + }, [t]); + return ( - + ); } diff --git a/packages/core-i18n/src/languages/en/common.json b/packages/core-i18n/src/languages/en/common.json index 413cd6d..2f7fa3f 100644 --- a/packages/core-i18n/src/languages/en/common.json +++ b/packages/core-i18n/src/languages/en/common.json @@ -65,7 +65,9 @@ "detail": "Detail", "view": "View", "close": "Close", - "progress": "Progress" + "progress": "Progress", + "reset": "Reset", + "undo": "Undo" }, "confirmDialog": { "delete": { diff --git a/packages/core-i18n/src/languages/id/common.json b/packages/core-i18n/src/languages/id/common.json index 609f5fa..c22cb9a 100644 --- a/packages/core-i18n/src/languages/id/common.json +++ b/packages/core-i18n/src/languages/id/common.json @@ -65,7 +65,9 @@ "detail": "Detail", "view": "Lihat", "close": "Tutup", - "progress": "Progres" + "progress": "Progres", + "reset": "Atur Ulang", + "undo": "Kembalikan" }, "confirmDialog": { "delete": { diff --git a/packages/ui/src/foundations/enterprise-module/components/bulk-action-confirmation/index.tsx b/packages/ui/src/foundations/enterprise-module/components/bulk-action-confirmation/index.tsx index f787130..218ea71 100644 --- a/packages/ui/src/foundations/enterprise-module/components/bulk-action-confirmation/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/bulk-action-confirmation/index.tsx @@ -1,8 +1,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { Box, Button, Group, List, Modal, Paper, Progress, Stack, Text, ThemeIcon } from '@mantine/core'; +import { Box, Button, Group, Modal, Progress, Stack, Text } from '@mantine/core'; import { useForm, FormProvider } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Check, X, Info } from 'lucide-react'; import type { BaseEntity } from '@repo/core-api/data-services'; import type { @@ -197,8 +196,8 @@ export function BulkActionConfirmationModal( const [phase, setPhase] = useState('idle'); const [progress, setProgress] = useState(0); - const [currentBatch, setCurrentBatch] = useState(0); - const [totalBatches, setTotalBatches] = useState(0); + const [, setCurrentBatch] = useState(0); + const [, setTotalBatches] = useState(0); const [aggregatedResult, setAggregatedResult] = useState(null); // Ref to track cancellation requests during processing diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx index 0bce4d0..1550ea0 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx @@ -1,24 +1,155 @@ -import { Drawer, Text } from '@mantine/core'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Drawer, DrawerProps, Group, Stack, Text, ScrollArea } from '@mantine/core'; +import { useForm, FormProvider, UseFormReturn } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import type { ZodType } from 'zod'; +import { useEnterpriseModuleTranslationContext } from '../../../hooks/use-module.context'; + +/** + * Configuration for the filter drawer content and behavior. + * @template TMeta - The schema type for the react-hook-form + */ +export interface TableFilterConfig = Record> { + /** Renders the body of the filter drawer */ + renderBody?: (form?: UseFormReturn) => React.ReactNode; + /** Zod schema for form validation */ + schema?: ZodType; + /** Default values for the form fields */ + defaultValues?: TMeta; + /** Additional props to pass to the underlying Mantine Drawer */ + drawerProps?: Omit; +} export interface TableFilterDrawerProps { opened: boolean; onClose: () => void; title: string; + config?: TableFilterConfig; + currentFilterData: any; + onFilter: (data: any) => void; } -export function TableFilterDrawer({ opened, onClose, title }: TableFilterDrawerProps) { +const DrawerFormBody = React.memo(function DrawerFormBody< + TMeta extends Record = Record, +>({ + renderBody, + form, +}: { + renderBody: NonNullable['renderBody']>; + form?: ReturnType>; +}) { + return <>{renderBody(form)}; +}) as >(props: { + renderBody: NonNullable['renderBody']>; + form?: ReturnType>; +}) => React.ReactElement; + +export function TableFilterDrawer({ + opened, + onClose, + title, + config, + onFilter, + currentFilterData, +}: TableFilterDrawerProps) { + const { t } = useEnterpriseModuleTranslationContext(); + + // We have a form if a renderBody is provided + const hasForm = Boolean(config?.renderBody); + + const form = useForm>({ + mode: 'onSubmit', + resolver: config?.schema ? zodResolver(config.schema as any) : undefined, + defaultValues: config?.defaultValues ?? {}, + }); + + const [previousValues, setPreviousValues] = useState | null>(null); + const [hasReset, setHasReset] = useState(false); + + useEffect(() => { + if (opened && hasForm) { + form.reset({ + ...config?.defaultValues, + ...(currentFilterData || {}), + }); + setHasReset(false); + setPreviousValues(null); + } + }, [opened]); // Only reset when opened + + const handleApply = useCallback(async () => { + if (hasForm) { + const isValid = await form.trigger(); + if (!isValid) return; + const values = form.getValues(); + onFilter(values); + } else { + onFilter({}); + } + onClose(); + }, [hasForm, form, onFilter, onClose]); + + const handleReset = useCallback(() => { + if (hasForm) { + setPreviousValues(form.getValues()); + form.reset(config?.defaultValues ?? {}); + setHasReset(true); + } + }, [hasForm, form, config]); + + const handleUndoReset = useCallback(() => { + if (previousValues) { + form.reset(previousValues); + setHasReset(false); + setPreviousValues(null); + } + }, [form, previousValues]); + return ( {title}} + title={ + + {title} + + } position="right" + keepMounted={false} size="md" - padding="md" + {...config?.drawerProps} > - - Filter configuration will go here. - + + + {config?.renderBody ? ( + hasForm ? ( + + + + ) : ( + + ) + ) : ( + + Filter configuration will go here. + + )} + + + + {hasReset && ( + + )} + + + + ); } diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx index 2ac170a..62a047e 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx @@ -39,13 +39,15 @@ import { RowActionMenu } from './components/row-actions'; import { BulkActionMenu } from './components/bulk-actions'; import { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../action-confirmation-modal'; import { BulkActionConfirmationModal } from '../bulk-action-confirmation'; -import { TableFilterDrawer } from './components/table-filter-drawer'; +import { TableFilterDrawer, TableFilterConfig } from './components/table-filter-drawer'; import { TableSettingDrawer } from './components/table-setting-drawer'; import { EntityId } from '../../../../../../core-api/src/data-services/types'; export * from 'ag-grid-community'; export * from 'ag-grid-react'; +export type { TableFilterConfig }; + // --------------------------------------------------------------------------- // Types & Interfaces // --------------------------------------------------------------------------- @@ -119,6 +121,11 @@ export interface EnterpriseDataTableProps extends Omit void; onBulkClickRollback?: (data: E[]) => void; onBulkClickHold?: (data: E[]) => void; + + /** + * Configuration for the Filter Drawer + */ + filterConfig?: TableFilterConfig; } // --------------------------------------------------------------------------- @@ -158,6 +165,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT cancelModalConfig, rollbackModalConfig, holdModalConfig, + filterConfig, // Bulk action props customBulkActions, @@ -178,7 +186,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT // --------------------------------------------------------------------------- const { t } = useEnterpriseModuleTranslationContext(); const { dataServices } = useEnterpriseModuleDataServiceContext(); - const { selectedRows, setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext(); + const { selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData } = useEnterpriseModuleSelectionContext(); const navigation = useEnterpriseModuleNavigationContext(); const { config } = useEnterpriseModuleConfigContext(); @@ -789,6 +797,11 @@ export function EnterpriseDataTable(props: EnterpriseDataT opened={openedFilter} onClose={closeFilter} title={t('common:filterTitle', { module: moduleTitle })} + config={filterConfig} + currentFilterData={filterData} + onFilter={(data) => { + console.log('Filter applied:', data); + }} />