Skip to content

Commit fca4470

Browse files
committed
feat: Apply filters via button
1 parent 8db4aae commit fca4470

File tree

6 files changed

+137
-50
lines changed

6 files changed

+137
-50
lines changed

src/features/instance/databases/DatabaseTableView.tsx

Lines changed: 87 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { ConfirmDeletionModal } from '@/components/ConfirmDeletionModal';
22
import { Button } from '@/components/ui/button';
3+
import {
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuItem,
7+
DropdownMenuTrigger,
8+
} from '@/components/ui/dropdownMenu';
39
import { useInstanceClientIdParams } from '@/config/useInstanceClient';
410
import { ColumnFiltersSchema } from '@/features/instance/databases/components/ColumnFilters';
511
import { PickColumnsDropdown } from '@/features/instance/databases/components/PickColumnsDropdown';
@@ -20,19 +26,27 @@ import {
2026
} from '@/features/instance/operations/queries/getSearchByConditions';
2127
import { getSearchByIdOptions } from '@/features/instance/operations/queries/getSearchById';
2228
import { getSearchByValueOptions } from '@/features/instance/operations/queries/getSearchByValue';
23-
import { useDebounce } from '@/hooks/useDebounce';
2429
import { useEffectedState } from '@/hooks/useEffectedState';
2530
import { useInstanceBrowseManagePermission, useInstanceSchemaTablePermission } from '@/hooks/usePermissions';
2631
import { useRefreshClick } from '@/hooks/useRefreshClick';
2732
import { useSessionStorage } from '@/hooks/useSessionStorage';
28-
import { useToggleCallback } from '@/hooks/useToggleCallback';
33+
import { useToggler } from '@/hooks/useToggler';
2934
import { keyBy } from '@/lib/keyBy';
3035
import { queryClient } from '@/react-query/queryClient';
3136
import { zodResolver } from '@hookform/resolvers/zod';
3237
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
3338
import { useNavigate, useParams } from '@tanstack/react-router';
3439
import { Row, VisibilityState } from '@tanstack/react-table';
35-
import { ImportIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash } from 'lucide-react';
40+
import {
41+
Ellipsis,
42+
FunnelIcon,
43+
FunnelPlusIcon,
44+
FunnelXIcon,
45+
ImportIcon,
46+
PlusIcon,
47+
RefreshCwIcon,
48+
Trash,
49+
} from 'lucide-react';
3650
import { useCallback, useEffect, useMemo, useState } from 'react';
3751
import { useForm } from 'react-hook-form';
3852
import { toast } from 'sonner';
@@ -61,29 +75,40 @@ export function DatabaseTableView() {
6175
tableName,
6276
}),
6377
);
64-
const attributesMap = useMemo(() => keyBy(describeTableData.attributes, 'attribute'), [describeTableData])
78+
const attributesMap = useMemo(() => keyBy(describeTableData.attributes, 'attribute'), [describeTableData]);
6579
const [selectedIds, setSelectedIds] = useEffectedState<null | unknown[]>(null, allParams);
6680
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
6781

82+
const { toggled: filtersToggled, toggleOn: showFilters, toggleOff: hideFilters } = useToggler(false);
6883
const columnFiltersForm = useForm({
6984
resolver: zodResolver(ColumnFiltersSchema),
7085
});
86+
const { reset: resetFiltersForm } = columnFiltersForm;
7187
const columnFiltersValues = columnFiltersForm.watch();
72-
const debouncedColumnFiltersValues = useDebounce(columnFiltersValues, 500, JSON.stringify);
73-
const searchConditions: SearchCondition[] | null = useMemo(() => {
88+
89+
const [appliedSearchConditions, setAppliedSearchConditions] = useState<SearchCondition[] | null>(null);
90+
91+
const applyFilters = useCallback(() => {
7492
const conditions: SearchCondition[] = [];
75-
for (const key in debouncedColumnFiltersValues) {
76-
if (debouncedColumnFiltersValues[key]?.length) {
93+
for (const key in columnFiltersValues) {
94+
if (columnFiltersValues[key]?.length) {
7795
try {
78-
conditions.push(...translateColumnFilterToSearchConditions(key, debouncedColumnFiltersValues[key], attributesMap[key]));
79-
}
80-
catch (err) {
96+
conditions.push(...translateColumnFilterToSearchConditions(key, columnFiltersValues[key], attributesMap[key]));
97+
} catch (err) {
8198
toast.error(String(err));
8299
}
83100
}
84101
}
85-
return conditions.length ? conditions : null;
86-
}, [attributesMap, debouncedColumnFiltersValues]);
102+
setAppliedSearchConditions(conditions.length ? conditions : null);
103+
resetFiltersForm({ ...columnFiltersValues });
104+
}, [attributesMap, resetFiltersForm, columnFiltersValues]);
105+
const clearFilters = useCallback(() => {
106+
// Note sure why we need to resetFiltersForm twice here...
107+
resetFiltersForm({}, { keepValues: false, keepDirtyValues: false, keepDefaultValues: false });
108+
resetFiltersForm();
109+
setAppliedSearchConditions(null);
110+
hideFilters();
111+
}, [hideFilters, resetFiltersForm]);
87112

88113
const { dataTableColumns, hashAttribute } = formatBrowseDataTableHeader(describeTableData);
89114
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -101,14 +126,16 @@ export function DatabaseTableView() {
101126
const [pageSize, setPageSize] = useState(20);
102127
const [totalPages, setTotalPages] = useState(Math.ceil(describeTableData.record_count / pageSize));
103128

129+
const useFilteredList = filtersToggled && !!appliedSearchConditions;
130+
104131
// Full list
105132
const {
106133
data: fullTableData,
107134
refetch: refetchSearchByValueOptions,
108135
isFetching: tableDataFetching,
109136
} = useQuery(
110137
getSearchByValueOptions({
111-
enabled: !searchConditions,
138+
enabled: !useFilteredList,
112139
...instanceParams,
113140
databaseName,
114141
tableName,
@@ -121,17 +148,17 @@ export function DatabaseTableView() {
121148
// Filtered list
122149
const { data: filteredTableData } = useQuery(
123150
getSearchByConditionsOptions({
124-
enabled: !!searchConditions,
151+
enabled: useFilteredList,
125152
...instanceParams,
126153
databaseName,
127154
tableName,
128-
conditions: searchConditions,
155+
conditions: appliedSearchConditions,
129156
sort,
130157
pageSize,
131158
pageIndex,
132159
}),
133160
);
134-
const tableData = searchConditions ? filteredTableData : fullTableData;
161+
const tableData = useFilteredList ? filteredTableData : fullTableData;
135162
// One by id
136163
const { data: searchByIdData } = useQuery(
137164
getSearchByIdOptions({
@@ -280,7 +307,6 @@ export function DatabaseTableView() {
280307
`ColumnDisplayed/${databaseName}}/${tableName}` as 'ColumnDisplayed/{database}/{table}',
281308
{} satisfies VisibilityState,
282309
);
283-
const [showSearch, toggleShowSearch] = useToggleCallback(false);
284310

285311
return (
286312
<>
@@ -312,34 +338,63 @@ export function DatabaseTableView() {
312338
</span>
313339
</Button>
314340
)}
315-
<Button variant="defaultOutline" onClick={onRefreshClick} disabled={tableDataFetching}>
316-
<RefreshCwIcon />
317-
</Button>
318341
</div>
319342

320343
<div className="flex space-x-2">
321-
<Button variant="ghost" onClick={toggleShowSearch}>
322-
<SearchIcon className="inline-block " />
323-
{showSearch ? 'Clear Search' : 'Search'}
344+
{filtersToggled && appliedSearchConditions && (
345+
<Button variant="ghost" onClick={clearFilters} accessKey="f">
346+
<FunnelXIcon className="inline-block " />
347+
<span>Clear <u>F</u>ilters</span>
348+
</Button>
349+
)}
350+
{filtersToggled && columnFiltersForm.formState.isDirty && (
351+
<Button variant="default" onClick={applyFilters}>
352+
<FunnelPlusIcon className="inline-block " />
353+
Apply Filters
354+
</Button>
355+
)}
356+
{filtersToggled && !appliedSearchConditions && (
357+
<Button variant="ghost" onClick={hideFilters} accessKey="f">
358+
<FunnelXIcon className="inline-block " />
359+
<span>Hide <u>F</u>ilters</span>
360+
</Button>
361+
)}
362+
363+
{!filtersToggled && (
364+
<Button variant="ghost" onClick={showFilters} accessKey="f">
365+
<FunnelIcon className="inline-block " />
366+
<span>Show <u>F</u>ilters</span>
367+
</Button>
368+
)}
369+
370+
<Button variant="defaultOutline" onClick={onRefreshClick} disabled={tableDataFetching}>
371+
<RefreshCwIcon />
324372
</Button>
373+
325374
<PickColumnsDropdown
326375
columns={dataTableColumns}
327376
columnVisibility={columnVisibility}
328377
setColumnVisibility={setColumnVisibility}
329378
/>
330-
{canManageBrowseInstance && (
331-
<Button variant="destructiveOutline" onClick={openDeleteModal}>
332-
<Trash className="inline-block " />
333-
Drop Table
334-
</Button>
335-
)}
379+
380+
{canManageBrowseInstance && (<DropdownMenu>
381+
<DropdownMenuTrigger>
382+
<Ellipsis aria-label="Table options" />
383+
</DropdownMenuTrigger>
384+
<DropdownMenuContent side="bottom" align="end">
385+
<DropdownMenuItem className="focus:bg-red/70 focus:text-white" onClick={openDeleteModal}>
386+
<Trash className="inline-block " />
387+
Drop Table
388+
</DropdownMenuItem>
389+
</DropdownMenuContent>
390+
</DropdownMenu>)}
336391
</div>
337392
</div>
338393

339394
<TableView<Record<string, unknown>, unknown>
340395
data={tableData?.data || []}
341396
isFetching={tableDataFetching}
342-
showSearch={showSearch}
397+
filtersToggled={filtersToggled}
343398
columns={dataTableColumns}
344399
columnVisibility={columnVisibility}
345400
onRowClick={onRowClick}
@@ -349,6 +404,7 @@ export function DatabaseTableView() {
349404
pageIndex={pageIndex}
350405
pageSize={pageSize}
351406
columnFiltersForm={columnFiltersForm}
407+
applyFilters={applyFilters}
352408
setPageIndex={setPageIndex}
353409
setPageSize={setPageSize}
354410
/>

src/features/instance/databases/components/ColumnFilters.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@ import { FormMessage } from '@/components/ui/form/FormMessage';
66
import { Input } from '@/components/ui/input';
77
import { TableCell, TableHeader, TableRow } from '@/components/ui/table';
88
import { HeaderGroup } from '@tanstack/react-table';
9+
import { KeyboardEvent, useCallback } from 'react';
910
import { UseFormReturn } from 'react-hook-form';
1011
import { z } from 'zod';
1112

1213
export const ColumnFiltersSchema = z.record(z.string(), z.string());
1314

1415
export function ColumnFilters<TData>({
16+
applyFilters,
1517
columnFiltersForm,
1618
headerGroups,
1719
}: {
20+
applyFilters: () => void,
1821
columnFiltersForm: UseFormReturn<z.infer<typeof ColumnFiltersSchema>>,
1922
headerGroups: HeaderGroup<TData>[]
2023
}) {
24+
25+
const handleSubmit = useCallback((e: KeyboardEvent) => {
26+
if (e.key === 'Enter') {
27+
e.preventDefault();
28+
applyFilters();
29+
return false;
30+
}
31+
}, [applyFilters]);
2132
return (<TableHeader>
2233
<Form {...columnFiltersForm}>
2334
{headerGroups.map((headerGroup) => (
@@ -36,6 +47,7 @@ export function ColumnFilters<TData>({
3647
autoCapitalize="none"
3748
autoComplete="off"
3849
className="rounded-none"
50+
onKeyDown={handleSubmit}
3951
value={field.value ?? ''}
4052
/>
4153
</FormControl>

src/features/instance/databases/components/PickColumnsDropdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
DropdownMenuTrigger,
77
} from '@/components/ui/dropdownMenu';
88
import { formatBrowseDataTableHeader } from '@/features/instance/databases/functions/formatBrowseDataTableHeader';
9-
import { useToggleCallback } from '@/hooks/useToggleCallback';
9+
import { useToggler } from '@/hooks/useToggler';
1010
import { excludeFalsy } from '@/lib/arrays/excludeFalsy';
1111
import { VisibilityState } from '@tanstack/react-table';
1212
import { Columns3CogIcon } from 'lucide-react';
@@ -55,7 +55,7 @@ function ColumnPicker({
5555
columnVisibility: VisibilityState,
5656
setColumnVisibility: (columnVisibility: VisibilityState) => void,
5757
}) {
58-
const [isChecked, onCheckedChanged] = useToggleCallback(columnVisibility[columnHeader] ?? true);
58+
const { toggled: isChecked, toggle: onCheckedChanged } = useToggler(columnVisibility[columnHeader] ?? true);
5959
useEffect(() => {
6060
if (columnVisibility[columnHeader] !== isChecked) {
6161
setColumnVisibility({

src/features/instance/databases/components/TableView.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { UseFormReturn } from 'react-hook-form';
2222
import { z } from 'zod';
2323

2424
interface BrowseDataTableProps<TData, TValue> {
25+
applyFilters: () => void,
2526
columnFiltersForm: UseFormReturn<z.infer<typeof ColumnFiltersSchema>>,
2627
columns: ColumnDef<TData, TValue>[],
2728
columnVisibility: VisibilityState,
@@ -33,12 +34,13 @@ interface BrowseDataTableProps<TData, TValue> {
3334
pageSize: number,
3435
setPageIndex: Dispatch<SetStateAction<number>>,
3536
setPageSize: Dispatch<SetStateAction<number>>,
36-
showSearch: boolean,
37+
filtersToggled: boolean,
3738
totalPages: number,
3839
totalRecords: number,
3940
}
4041

4142
export function TableView<TData, TValue>({
43+
applyFilters,
4244
columns,
4345
columnVisibility,
4446
columnFiltersForm,
@@ -50,7 +52,7 @@ export function TableView<TData, TValue>({
5052
pageSize,
5153
setPageIndex,
5254
setPageSize,
53-
showSearch,
55+
filtersToggled,
5456
totalPages,
5557
totalRecords,
5658
}: BrowseDataTableProps<TData, TValue>) {
@@ -87,8 +89,8 @@ export function TableView<TData, TValue>({
8789
<TableHeadSortable key={header.id} header={header} onColumnClick={onColumnClick} />)}
8890
</TableRow>))}
8991
</TableHeader>
90-
{showSearch && (
91-
<ColumnFilters columnFiltersForm={columnFiltersForm} headerGroups={table.getHeaderGroups()} />
92+
{filtersToggled && (
93+
<ColumnFilters applyFilters={applyFilters} columnFiltersForm={columnFiltersForm} headerGroups={table.getHeaderGroups()} />
9294
)}
9395
<TableBody className="bg-black border border-grey-700">
9496
{table.getRowModel().rows?.length ? (table.getRowModel().rows.map((row) => (

src/hooks/useToggleCallback.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/hooks/useToggler.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { MouseEvent, useCallback, useMemo, useState } from 'react';
2+
3+
export function useToggler(defaultValue?: boolean) {
4+
const [toggled, setToggled] = useState<boolean>(defaultValue || false);
5+
const toggle = useCallback((e: MouseEvent) => {
6+
e.preventDefault();
7+
setToggled((checked: boolean) => {
8+
return !checked;
9+
});
10+
return false;
11+
}, []);
12+
const toggleOn = useCallback((e?: MouseEvent) => {
13+
e?.preventDefault();
14+
setToggled(true);
15+
return false;
16+
}, []);
17+
const toggleOff = useCallback((e?: MouseEvent) => {
18+
e?.preventDefault();
19+
setToggled(false);
20+
return false;
21+
}, []);
22+
return useMemo(() => {
23+
return {
24+
toggled,
25+
toggle,
26+
toggleOn,
27+
toggleOff,
28+
};
29+
}, [toggle, toggleOff, toggleOn, toggled]);
30+
}

0 commit comments

Comments
 (0)