Skip to content

Commit

Permalink
feat: bind dynamic filter
Browse files Browse the repository at this point in the history
  • Loading branch information
phucvinh57 committed Nov 1, 2024
1 parent a9d90dc commit 21d978a
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 25 deletions.
74 changes: 68 additions & 6 deletions app/datasets/[dataset]/filter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { Card, CardContent } from '@/components/ui/card';
import { DateRangePicker } from '@/components/ui/dateRangePicker';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useExtractDocuments } from '@/hooks/useExtractDocuments';
import { DatasetAPI } from '@/lib/api/dataset';
import { useFilterStore } from '@/states/filter';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { type ChangeEvent, useCallback } from 'react';
import type { DateRange } from 'react-day-picker';

export function DatasetFilter() {
const { dataset } = useParams<{ dataset: string }>();
Expand All @@ -15,17 +19,52 @@ export function DatasetFilter() {
queryFn: () => DatasetAPI.getSchema(dataset)
});

const { filter, setDate, setNumber, setString } = useFilterStore();

const { refetch: fetchDocuments } = useExtractDocuments(dataset);

const handleStringInput = useCallback(
(fieldName: string) => {
return (e: ChangeEvent<HTMLInputElement>) => {
setString(fieldName, e.target.value);
};
},
[setString]
);

const handleNumberInput = useCallback(
(fieldName: string, type: 'min' | 'max') => {
return (e: ChangeEvent<HTMLInputElement>) => {
setNumber(fieldName, type, e.target.valueAsNumber);
};
},
[setNumber]
);

const handleDateRangeChange = useCallback(
(fieldName: string) => {
return (range?: DateRange) => setDate(fieldName, range);
},
[setDate]
);

return (
<Card>
<CardContent className='flex flex-col gap-2 p-4'>
{fieldSchemas?.map((field) => {
if (!field.filterable) return null;
if (field.type === 'keyword' || field.type === 'text') {
return (
<div key={field.name} className='flex-1'>
<Label htmlFor={field.name} className='mb-2'>
{field.displayName}
</Label>
<Input placeholder={field.description} type='text' id={field.name} />
<Input
placeholder={field.description}
type='text'
id={field.name}
onChange={handleStringInput(field.name)}
/>
</div>
);
}
Expand All @@ -34,11 +73,21 @@ export function DatasetFilter() {
<div className='flex gap-2' key={field.name}>
<div className='flex-1'>
<Label htmlFor={`${field.name}-min`}>{field.displayName} tối thiểu</Label>
<Input type='number' id={field.name} placeholder={field.description} />
<Input
type='number'
id={field.name}
placeholder={field.description}
onChange={handleNumberInput(field.name, 'min')}
/>
</div>
<div className='flex-1'>
<Label htmlFor={`${field.name}-max`}>{field.displayName} tối đa</Label>
<Input type='number' id={field.name} placeholder={field.description} />
<Input
type='number'
id={field.name}
placeholder={field.description}
onChange={handleNumberInput(field.name, 'max')}
/>
</div>
</div>
);
Expand All @@ -47,14 +96,27 @@ export function DatasetFilter() {
return (
<div key={field.name} className='flex-1'>
<Label htmlFor={field.name}>{field.displayName}</Label>
<DateRangePicker id={field.name} placeholder={field.description} />
<DateRangePicker
id={field.name}
placeholder={field.description ?? ''}
date={filter[field.name] as DateRange}
// @ts-ignore
onSelect={handleDateRangeChange(field.name)}
/>
</div>
);
}
return null;
})}
<div className='flex mt-2'>
<Button className='flex-1'>Lọc</Button>
<Button
className='flex-1'
onClick={() => {
fetchDocuments();
}}
>
Lọc
</Button>
</div>
</CardContent>
</Card>
Expand Down
20 changes: 13 additions & 7 deletions app/datasets/[dataset]/table.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { Card, CardContent } from '@/components/ui/card';
import { useExtractDocuments } from '@/hooks/useExtractDocuments';
import { DatasetAPI, type FieldSchema } from '@/lib/api/dataset';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';

export function DatasetTable() {
const { dataset } = useParams<{ dataset: string }>();

const { data: records } = useQuery({
queryKey: [`/datasets/${dataset}/documents`],
queryFn: () => DatasetAPI.getDocuments(dataset)
});

const { data: fieldSchemas } = useQuery({
queryKey: [`/datasets/${dataset}/schema`],
queryFn: () => DatasetAPI.getSchema(dataset)
});

const { data: records, refetch: fetchDocuments } = useExtractDocuments(dataset);

// Fetch documents on initial render
useEffect(() => {
fetchDocuments();
}, [fetchDocuments]);

const fieldSchemaMap = useMemo(() => {
if (!fieldSchemas) return {};
return fieldSchemas.reduce(
Expand All @@ -41,10 +44,13 @@ export function DatasetTable() {
if (fieldSchemaMap[fieldName]?.type === 'date') {
valueStr = new Date(value).toLocaleString();
}
} else {
valueStr = JSON.stringify(value);
}
if (fieldName === '_id') return null;
return (
<div key={fieldName}>
<div className='font-semibold'>{fieldSchemaMap[fieldName]?.displayName || fieldName}</div>
<div className='font-semibold'>{fieldSchemaMap[fieldName]?.displayName ?? fieldName}</div>
<div>{valueStr}</div>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
},
"suspicious": {
"noConsole": "warn"
},
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error"
}
},
"ignore": ["dist", "coverage", "node_modules", ".next"]
Expand Down
Binary file modified bun.lockb
Binary file not shown.
11 changes: 5 additions & 6 deletions components/ui/dateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import { format } from 'date-fns';
import { Calendar as CalendarIcon } from 'lucide-react';
import * as React from 'react';
import type { DateRange } from 'react-day-picker';
import type { DateRange, SelectRangeEventHandler } from 'react-day-picker';

import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
Expand All @@ -12,11 +11,11 @@ import { cn } from '@/lib/utils';

type DateRangePickerProps = React.HTMLAttributes<HTMLDivElement> & {
placeholder: string;
onSelect?: SelectRangeEventHandler;
date: DateRange | undefined;
};

export function DateRangePicker({ className, placeholder }: DateRangePickerProps) {
const [date, setDate] = React.useState<DateRange | undefined>();

export function DateRangePicker({ className, placeholder, onSelect, date }: DateRangePickerProps) {
return (
<div className={cn('grid gap-2', className)}>
<Popover>
Expand Down Expand Up @@ -48,7 +47,7 @@ export function DateRangePicker({ className, placeholder }: DateRangePickerProps
mode='range'
defaultMonth={date?.from}
selected={date}
onSelect={setDate}
onSelect={onSelect}
numberOfMonths={2}
/>
</PopoverContent>
Expand Down
13 changes: 13 additions & 0 deletions hooks/useExtractDocuments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DatasetAPI } from '@/lib/api/dataset';
import { useFilterStore } from '@/states/filter';
import { useQuery } from '@tanstack/react-query';

export function useExtractDocuments(dataset: string) {
const filter = useFilterStore((state) => state.filter);

return useQuery({
queryKey: [`/datasets/${dataset}/documents`],
queryFn: () => DatasetAPI.getDocuments(dataset, filter),
enabled: false
});
}
13 changes: 9 additions & 4 deletions lib/api/dataset.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DatasetFilter } from '@/states/filter';
import { api } from './http';

export type Dataset = {
Expand All @@ -7,10 +8,14 @@ export type Dataset = {

export type FieldSchema = {
type: 'keyword' | 'text' | 'long' | 'double' | 'date' | string;
description?: string;
displayName: string;
name: string;
displayName: string;
description?: string;
order: number;
filterable: boolean;
sortable: boolean;
};

async function list(): Promise<Dataset[]> {
const resp = await api.get<string[]>('/api/datasets');
return resp.data.map((name) => ({ name, description: '' }));
Expand All @@ -26,8 +31,8 @@ type Document = {
[key: string]: unknown;
};

async function getDocuments<T = Document>(dataset: string): Promise<T[]> {
const resp = await api.get(`/api/datasets/${dataset}/documents`);
async function getDocuments<T = Document>(dataset: string, filter: DatasetFilter): Promise<T[]> {
const resp = await api.post(`/api/datasets/${dataset}/documents`, filter);
return resp.data;
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zustand": "^5.0.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
Expand Down
40 changes: 40 additions & 0 deletions states/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { DateRange } from 'react-day-picker';
import { create } from 'zustand';

type NumberRange = { min: number; max: number };

export type DatasetFilter = Record<string, NumberRange | DateRange | string | boolean>;

type FilterState = {
filter: DatasetFilter;
setString(fieldName: string, value: string): void;
setNumber(fieldName: string, type: 'min' | 'max', value: number): void;
setDate(fieldName: string, range?: DateRange): void;
};

export const useFilterStore = create<FilterState>((set) => ({
filter: {},
setDate(fieldName, range) {
if (range) {
set((state) => ({ filter: { ...state.filter, [fieldName]: range } }));
}
},
setString(fieldName, value) {
set((state) => ({ filter: { ...state.filter, [fieldName]: value } }));
},
setNumber(fieldName, type, value) {
set((state) => {
const rangeFilter = (state.filter[fieldName] as NumberRange) ?? {};
const parseValue = Number.isNaN(value) ? undefined : value;
return {
filter: {
...state.filter,
[fieldName]: {
...rangeFilter,
[type]: parseValue
}
}
};
});
}
}));
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"isolatedModules": true,

/* Strictness */
"strict": false,
"strict": true,
"noUncheckedIndexedAccess": true,
"checkJs": true,

Expand Down

0 comments on commit 21d978a

Please sign in to comment.