refactor(settings): put the remaining two settings tables on the shared pieces

#770 extracted SettingsTable for the categories and labels pages; accounts and
automation-rules still carried their own copy of the same 30-line table markup,
so they move onto it too.

All four also repeated the identical useReactTable setup — four pieces of state
and the same six options — which is now useSettingsTable(data, columns).

SettingsTable's container becomes overflow-x-auto, which is what the accounts
table (the widest of the four) already used. On the other three it only matters
when the table outgrows its container, where scrolling beats clipping.
This commit is contained in:
Víctor Falcón 2026-08-11 15:07:59 +02:00
parent a912fb2f45
commit 3c4de4dfbd
6 changed files with 87 additions and 261 deletions

View File

@ -30,7 +30,8 @@ export function SettingsTable<TData>({
const rows = table.getRowModel().rows;
return (
<div className="overflow-hidden rounded-md border">
// Scrolls rather than clips: the accounts table is wider than a phone.
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (

View File

@ -0,0 +1,44 @@
import {
type ColumnDef,
type ColumnFiltersState,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
type VisibilityState,
} from '@tanstack/react-table';
import { useState } from 'react';
/**
* A sortable, filterable table over a list the server already sent whole the
* shape every settings page needs. Sorting, filtering and column visibility are
* client-side, so no page has to wire the same four pieces of state again.
*/
export function useSettingsTable<TData>(
data: TData[],
columns: ColumnDef<TData>[],
initialSorting: SortingState = [],
) {
const [sorting, setSorting] = useState<SortingState>(initialSorting);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
{},
);
return useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
});
}

View File

@ -1,18 +1,6 @@
import { __ } from '@/utils/i18n';
import { Head, router } from '@inertiajs/react';
import {
Cell,
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { Cell, ColumnDef, flexRender, Row } from '@tanstack/react-table';
import { ArrowUpDown, Link2, MoreHorizontal } from 'lucide-react';
import { useState } from 'react';
@ -23,6 +11,7 @@ import { DeleteAccountDialog } from '@/components/accounts/delete-account-dialog
import { EditAccountDialog } from '@/components/accounts/edit-account-dialog';
import { BankLogo } from '@/components/bank-logo';
import HeadingSmall from '@/components/heading-small';
import { SettingsTable } from '@/components/shared/settings-table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@ -40,14 +29,8 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { TableCell, TableRow } from '@/components/ui/table';
import { useSettingsTable } from '@/hooks/use-settings-table';
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import { type BreadcrumbItem } from '@/types';
@ -177,12 +160,6 @@ interface AccountsPageProps {
}
export default function Accounts({ accounts }: AccountsPageProps) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
{},
);
const breadcrumbs: BreadcrumbItem[] = [
{
title: __('Bank accounts'),
@ -291,21 +268,7 @@ export default function Accounts({ accounts }: AccountsPageProps) {
},
];
const table = useReactTable({
data: accounts,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
});
const table = useSettingsTable(accounts, columns);
return (
<AppLayout breadcrumbs={breadcrumbs}>
@ -340,61 +303,17 @@ export default function Accounts({ accounts }: AccountsPageProps) {
/>
</div>
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
{table
.getHeaderGroups()
.map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(
(header) => {
return (
<TableHead
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext(),
)}
</TableHead>
);
},
)}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table
.getRowModel()
.rows.map((row) => (
<AccountRow
key={row.id}
row={row}
onSuccess={
handleAccountCreated
}
/>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{__('No accounts found.')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<SettingsTable
table={table}
emptyMessage={__('No accounts found.')}
renderRow={(row) => (
<AccountRow
key={row.id}
row={row}
onSuccess={handleAccountCreated}
/>
)}
/>
</div>
</div>
</SettingsLayout>

View File

@ -1,17 +1,5 @@
import { Head, usePage } from '@inertiajs/react';
import {
Cell,
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { Cell, ColumnDef, flexRender, Row } from '@tanstack/react-table';
import { MoreHorizontal } from 'lucide-react';
import { useMemo, useState } from 'react';
@ -24,6 +12,7 @@ import { DeleteAutomationRuleDialog } from '@/components/automation-rules/delete
import { EditAutomationRuleDialog } from '@/components/automation-rules/edit-automation-rule-dialog';
import { PostSaveApplyRulePrompt } from '@/components/automation-rules/post-save-apply-rule-prompt';
import HeadingSmall from '@/components/heading-small';
import { SettingsTable } from '@/components/shared/settings-table';
import { Button } from '@/components/ui/button';
import {
ContextMenu,
@ -40,14 +29,8 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { TableCell, TableRow } from '@/components/ui/table';
import { useSettingsTable } from '@/hooks/use-settings-table';
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import { type BreadcrumbItem } from '@/types';
@ -223,11 +206,6 @@ export default function AutomationRules() {
})),
[rawRules],
);
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
{},
);
const columns: ColumnDef<AutomationRule>[] = [
{
@ -257,21 +235,7 @@ export default function AutomationRules() {
},
];
const table = useReactTable({
data: rules,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
});
const table = useSettingsTable(rules, columns);
return (
<AppLayout breadcrumbs={breadcrumbs}>
@ -308,62 +272,18 @@ export default function AutomationRules() {
/>
</div>
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table
.getHeaderGroups()
.map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(
(header) => {
return (
<TableHead
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext(),
)}
</TableHead>
);
},
)}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table
.getRowModel()
.rows.map((row) => (
<AutomationRuleRow
key={row.id}
row={row}
categories={categories}
labels={labels}
/>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{__(
'No automation rules found.',
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<SettingsTable
table={table}
emptyMessage={__('No automation rules found.')}
renderRow={(row) => (
<AutomationRuleRow
key={row.id}
row={row}
categories={categories}
labels={labels}
/>
)}
/>
<div className="flex items-center justify-end">
<div className="text-sm text-muted-foreground">

View File

@ -1,16 +1,6 @@
import { __ } from '@/utils/i18n';
import { Head, usePage } from '@inertiajs/react';
import {
ColumnDef,
ColumnFiltersState,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { ColumnDef, Row } from '@tanstack/react-table';
import * as Icons from 'lucide-react';
import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
import { useMemo, useState } from 'react';
@ -34,6 +24,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useSettingsTable } from '@/hooks/use-settings-table';
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import {
@ -148,12 +139,6 @@ export default function Categories() {
</Button>
);
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
{},
);
const columns: ColumnDef<Category>[] = [
{
accessorKey: 'name',
@ -318,21 +303,7 @@ export default function Categories() {
},
];
const table = useReactTable({
data: orderedCategories,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
});
const table = useSettingsTable(orderedCategories, columns);
return (
<AppLayout breadcrumbs={breadcrumbs}>

View File

@ -1,18 +1,7 @@
import { __ } from '@/utils/i18n';
import { Head, usePage } from '@inertiajs/react';
import {
ColumnDef,
ColumnFiltersState,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { ColumnDef, Row } from '@tanstack/react-table';
import { ArrowUpDown, Tag } from 'lucide-react';
import { useState } from 'react';
import { index as labelsIndex } from '@/actions/App/Http/Controllers/Settings/LabelController';
import HeadingSmall from '@/components/heading-small';
@ -28,6 +17,7 @@ import { SettingsTable } from '@/components/shared/settings-table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useSettingsTable } from '@/hooks/use-settings-table';
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import { type BreadcrumbItem } from '@/types';
@ -67,13 +57,6 @@ function LabelRow({ row }: { row: Row<Label> }) {
export default function Labels() {
const { labels } = usePage<{ labels: Label[] }>().props;
const [sorting, setSorting] = useState<SortingState>([
{ id: 'name', desc: false },
]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
{},
);
const columns: ColumnDef<Label>[] = [
{
@ -128,21 +111,9 @@ export default function Labels() {
},
];
const table = useReactTable({
data: labels,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
});
const table = useSettingsTable(labels, columns, [
{ id: 'name', desc: false },
]);
return (
<AppLayout breadcrumbs={breadcrumbs}>