feat: add configurable data table filtering and update API endpoints for the example module
This commit is contained in:
+37
@@ -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<any>; t: any }) => {
|
||||
const codeValue = useWatch({
|
||||
control: form.control,
|
||||
name: 'code',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!codeValue) {
|
||||
form.setValue('name', '');
|
||||
form.clearErrors('name');
|
||||
}
|
||||
}, [codeValue, form]);
|
||||
|
||||
return (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<FieldTextInput
|
||||
control={form.control}
|
||||
name="code"
|
||||
label={t('fields.code')}
|
||||
placeholder={`Enter ${t('fields.code')}`}
|
||||
/>
|
||||
|
||||
<FieldTextInput
|
||||
control={form.control}
|
||||
name="name"
|
||||
label={t('fields.name')}
|
||||
placeholder={`Enter ${t('fields.name')}`}
|
||||
disabled={!codeValue}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
);
|
||||
};
|
||||
+28
-1
@@ -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 <FilterFormContent form={form} t={t} />;
|
||||
},
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<EnterpriseIndexPageProvider
|
||||
pageHeaderProps={{
|
||||
@@ -34,7 +61,7 @@ export default function FullPagePageIndex() {
|
||||
],
|
||||
}}
|
||||
>
|
||||
<EnterpriseDataTable columnDefs={columnDefs} />
|
||||
<EnterpriseDataTable columnDefs={columnDefs} filterConfig={filterConfig} />
|
||||
</EnterpriseIndexPageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
"detail": "Detail",
|
||||
"view": "View",
|
||||
"close": "Close",
|
||||
"progress": "Progress"
|
||||
"progress": "Progress",
|
||||
"reset": "Reset",
|
||||
"undo": "Undo"
|
||||
},
|
||||
"confirmDialog": {
|
||||
"delete": {
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
"detail": "Detail",
|
||||
"view": "Lihat",
|
||||
"close": "Tutup",
|
||||
"progress": "Progres"
|
||||
"progress": "Progres",
|
||||
"reset": "Atur Ulang",
|
||||
"undo": "Kembalikan"
|
||||
},
|
||||
"confirmDialog": {
|
||||
"delete": {
|
||||
|
||||
+3
-4
@@ -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<E extends BaseEntity = BaseEntity>(
|
||||
|
||||
const [phase, setPhase] = useState<ProcessingPhase>('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<AggregatedResult | null>(null);
|
||||
|
||||
// Ref to track cancellation requests during processing
|
||||
|
||||
+138
-7
@@ -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<TMeta extends Record<string, unknown> = Record<string, unknown>> {
|
||||
/** Renders the body of the filter drawer */
|
||||
renderBody?: (form?: UseFormReturn<TMeta>) => React.ReactNode;
|
||||
/** Zod schema for form validation */
|
||||
schema?: ZodType<TMeta>;
|
||||
/** Default values for the form fields */
|
||||
defaultValues?: TMeta;
|
||||
/** Additional props to pass to the underlying Mantine Drawer */
|
||||
drawerProps?: Omit<DrawerProps, 'opened' | 'onClose'>;
|
||||
}
|
||||
|
||||
export interface TableFilterDrawerProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
config?: TableFilterConfig<any>;
|
||||
currentFilterData: any;
|
||||
onFilter: (data: any) => void;
|
||||
}
|
||||
|
||||
export function TableFilterDrawer({ opened, onClose, title }: TableFilterDrawerProps) {
|
||||
const DrawerFormBody = React.memo(function DrawerFormBody<
|
||||
TMeta extends Record<string, unknown> = Record<string, unknown>,
|
||||
>({
|
||||
renderBody,
|
||||
form,
|
||||
}: {
|
||||
renderBody: NonNullable<TableFilterConfig<TMeta>['renderBody']>;
|
||||
form?: ReturnType<typeof useForm<TMeta>>;
|
||||
}) {
|
||||
return <>{renderBody(form)}</>;
|
||||
}) as <TMeta extends Record<string, unknown>>(props: {
|
||||
renderBody: NonNullable<TableFilterConfig<TMeta>['renderBody']>;
|
||||
form?: ReturnType<typeof useForm<TMeta>>;
|
||||
}) => 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<Record<string, unknown>>({
|
||||
mode: 'onSubmit',
|
||||
resolver: config?.schema ? zodResolver(config.schema as any) : undefined,
|
||||
defaultValues: config?.defaultValues ?? {},
|
||||
});
|
||||
|
||||
const [previousValues, setPreviousValues] = useState<Record<string, unknown> | 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 (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<Text fw={600}>{title}</Text>}
|
||||
title={
|
||||
<Text fw={600} size="lg">
|
||||
{title}
|
||||
</Text>
|
||||
}
|
||||
position="right"
|
||||
keepMounted={false}
|
||||
size="md"
|
||||
padding="md"
|
||||
{...config?.drawerProps}
|
||||
>
|
||||
<Text c="dimmed" size="sm">
|
||||
Filter configuration will go here.
|
||||
</Text>
|
||||
<Stack h="calc(100dvh - 60px)" gap={0}>
|
||||
<ScrollArea flex={1} p="sm">
|
||||
{config?.renderBody ? (
|
||||
hasForm ? (
|
||||
<FormProvider {...form}>
|
||||
<DrawerFormBody renderBody={config.renderBody} form={form} />
|
||||
</FormProvider>
|
||||
) : (
|
||||
<DrawerFormBody renderBody={config.renderBody} />
|
||||
)
|
||||
) : (
|
||||
<Text c="dimmed" size="sm">
|
||||
Filter configuration will go here.
|
||||
</Text>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<Group justify="flex-end" p="sm" style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}>
|
||||
{hasReset && (
|
||||
<Button size="sm" variant="outline" onClick={handleUndoReset} mr="auto">
|
||||
{t('common:actions.undo')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="default" onClick={handleReset}>
|
||||
{t('common:actions.reset')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleApply}>
|
||||
{t('common:actions.filter')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<E extends BaseEntity> extends Omit<AgG
|
||||
onBulkClickCancel?: (data: E[]) => void;
|
||||
onBulkClickRollback?: (data: E[]) => void;
|
||||
onBulkClickHold?: (data: E[]) => void;
|
||||
|
||||
/**
|
||||
* Configuration for the Filter Drawer
|
||||
*/
|
||||
filterConfig?: TableFilterConfig<any>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -158,6 +165,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
cancelModalConfig,
|
||||
rollbackModalConfig,
|
||||
holdModalConfig,
|
||||
filterConfig,
|
||||
|
||||
// Bulk action props
|
||||
customBulkActions,
|
||||
@@ -178,7 +186,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
// ---------------------------------------------------------------------------
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
|
||||
const { selectedRows, setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
|
||||
const { selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData } = useEnterpriseModuleSelectionContext<E>();
|
||||
const navigation = useEnterpriseModuleNavigationContext();
|
||||
|
||||
const { config } = useEnterpriseModuleConfigContext();
|
||||
@@ -789,6 +797,11 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
opened={openedFilter}
|
||||
onClose={closeFilter}
|
||||
title={t('common:filterTitle', { module: moduleTitle })}
|
||||
config={filterConfig}
|
||||
currentFilterData={filterData}
|
||||
onFilter={(data) => {
|
||||
console.log('Filter applied:', data);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TableSettingDrawer
|
||||
|
||||
Reference in New Issue
Block a user