Make table page size a configurable setting, not a hardcoded 20
Pagination just landed hardcoded to 20 rows everywhere; add a way to change that instead of leaving it fixed for every table in the app. pageSize joins dateFormat/timeFormat on the existing display settings object (server-side default 20, 5-500 range enforced by the PUT schema) rather than becoming its own settings section, since it's the same kind of thing -- an admin-configured, globally-applied display preference read by every signed-in role via the already-public GET /api/settings/display endpoint, same as the date/time format already works. Renamed the "Date & Time" settings tab/page/route to "Display" (still just one component, now covering both date/time format and table pagination) since its scope no longer matches the old name -- kept Settings.tsx's usual pattern of one page per concern rather than adding a second, oddly-scoped tab just for one number field. New web/src/utils/pageSize.ts mirrors utils/date.ts's existing module-level "set once at startup, read anywhere without prop- drilling" pattern; usePagination()'s pageSize parameter now defaults to getPageSize() instead of a literal 20, evaluated fresh on every call so it picks up a saved change without touching any of the ten pages already using the hook. Verified server-side against a temp SQLite DB: pageSize defaults to 20, a partial update sets it without disturbing dateFormat/timeFormat and vice versa, and it persists across a fresh settings read. Also checked the default-parameter mechanics directly (re-evaluates the global value on every call rather than capturing it once, and an explicit override still wins). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7bf9b03839
commit
b40234a557
10 files changed
+180
-123
No files matched your search
@@ -98,17 +98,18 @@ All modules from the original plan are built:
|
|||||||
and has an "Export CSV" button next to it that exports whatever's
|
and has an "Export CSV" button next to it that exports whatever's
|
||||||
currently sorted/filtered. Tables that can realistically grow large
|
currently sorted/filtered. Tables that can realistically grow large
|
||||||
(DNS zones/records, IP Addresses, Secrets, Servers, Audit Log, and each
|
(DNS zones/records, IP Addresses, Secrets, Servers, Audit Log, and each
|
||||||
integration's device/container/guest/repo/template list) are paginated
|
integration's device/container/guest/repo/template list) are paginated,
|
||||||
20 rows at a time — CSV export still covers every sorted/filtered row,
|
20 rows per page by default — adjustable under Settings → Display — and
|
||||||
not just the current page.
|
CSV export still covers every sorted/filtered row, not just the current
|
||||||
|
page.
|
||||||
- **Settings** (admin-only) — notification channels (Gotify, ntfy, SMTP,
|
- **Settings** (admin-only) — notification channels (Gotify, ntfy, SMTP,
|
||||||
generic webhook) with per-channel test buttons, per-event toggles (DNS
|
generic webhook) with per-channel test buttons, per-event toggles (DNS
|
||||||
record added/updated/deleted, daily secret-expiry reminder and daily
|
record added/updated/deleted, daily secret-expiry reminder and daily
|
||||||
Tailscale key-expiry reminder — both sharing one configurable
|
Tailscale key-expiry reminder — both sharing one configurable
|
||||||
time/timezone), badge-color customization for both DNS
|
time/timezone), badge-color customization for both DNS
|
||||||
providers and integration types, and a date/time display format
|
providers and integration types, and a **Display** tab (date order,
|
||||||
(date order, 12/24-hour clock) applied consistently to every table in
|
12/24-hour clock, and rows-per-page for every paginated table) applied
|
||||||
the app.
|
consistently across the app.
|
||||||
|
|
||||||
All six integrations follow the same config-in-UI + encrypted-credentials
|
All six integrations follow the same config-in-UI + encrypted-credentials
|
||||||
pattern, added (and edited — e.g. to rotate an expired API token without
|
pattern, added (and edited — e.g. to rotate an expired API token without
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ const updateSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
providerColors: z.record(z.string()).optional(),
|
providerColors: z.record(z.string()).optional(),
|
||||||
integrationColors: z.record(z.string()).optional(),
|
integrationColors: z.record(z.string()).optional(),
|
||||||
display: z.object({ dateFormat: z.enum(["ymd", "dmy", "mdy"]), timeFormat: z.enum(["24h", "12h"]) }).partial().optional(),
|
display: z
|
||||||
|
.object({ dateFormat: z.enum(["ymd", "dmy", "mdy"]), timeFormat: z.enum(["24h", "12h"]), pageSize: z.number().int().min(5).max(500) })
|
||||||
|
.partial()
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRouter.put("/", requireRole("admin"), asyncHandler(async (req, res) => {
|
settingsRouter.put("/", requireRole("admin"), asyncHandler(async (req, res) => {
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export type TimeFormat = "24h" | "12h";
|
|||||||
export interface DisplaySettings {
|
export interface DisplaySettings {
|
||||||
dateFormat: DateFormat;
|
dateFormat: DateFormat;
|
||||||
timeFormat: TimeFormat;
|
timeFormat: TimeFormat;
|
||||||
|
/** Rows per page for every paginated table in the app. */
|
||||||
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
@@ -82,7 +84,7 @@ const DEFAULTS: AppSettings = {
|
|||||||
},
|
},
|
||||||
providerColors: {},
|
providerColors: {},
|
||||||
integrationColors: {},
|
integrationColors: {},
|
||||||
display: { dateFormat: "ymd", timeFormat: "24h" },
|
display: { dateFormat: "ymd", timeFormat: "24h", pageSize: 20 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const KEYS = Object.keys(DEFAULTS) as (keyof AppSettings)[];
|
const KEYS = Object.keys(DEFAULTS) as (keyof AppSettings)[];
|
||||||
|
|||||||
+7
-3
@@ -21,10 +21,11 @@ import Synology from "./pages/Synology";
|
|||||||
import Settings from "./pages/Settings";
|
import Settings from "./pages/Settings";
|
||||||
import NotificationSettings from "./pages/settings/NotificationSettings";
|
import NotificationSettings from "./pages/settings/NotificationSettings";
|
||||||
import BadgeSettings from "./pages/settings/BadgeSettings";
|
import BadgeSettings from "./pages/settings/BadgeSettings";
|
||||||
import DateTimeSettings from "./pages/settings/DateTimeSettings";
|
import DisplaySettings from "./pages/settings/DisplaySettings";
|
||||||
import CacheSettings from "./pages/settings/CacheSettings";
|
import CacheSettings from "./pages/settings/CacheSettings";
|
||||||
import AppShell from "./layout/AppShell";
|
import AppShell from "./layout/AppShell";
|
||||||
import { setDateTimeSettings } from "./utils/date";
|
import { setDateTimeSettings } from "./utils/date";
|
||||||
|
import { setPageSize } from "./utils/pageSize";
|
||||||
|
|
||||||
const roleRank: Record<UserRole, number> = { viewer: 0, operator: 1, admin: 2 };
|
const roleRank: Record<UserRole, number> = { viewer: 0, operator: 1, admin: 2 };
|
||||||
|
|
||||||
@@ -58,7 +59,10 @@ export default function App() {
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
api.settings.display().then((res) => setDateTimeSettings(res.display)).catch(() => {});
|
api.settings.display().then((res) => {
|
||||||
|
setDateTimeSettings(res.display);
|
||||||
|
setPageSize(res.display.pageSize);
|
||||||
|
}).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
@@ -120,7 +124,7 @@ export default function App() {
|
|||||||
<Route index element={<Navigate to="notifications" replace />} />
|
<Route index element={<Navigate to="notifications" replace />} />
|
||||||
<Route path="notifications" element={<NotificationSettings />} />
|
<Route path="notifications" element={<NotificationSettings />} />
|
||||||
<Route path="badges" element={<BadgeSettings />} />
|
<Route path="badges" element={<BadgeSettings />} />
|
||||||
<Route path="date-time" element={<DateTimeSettings />} />
|
<Route path="display" element={<DisplaySettings />} />
|
||||||
<Route path="cache" element={<CacheSettings />} />
|
<Route path="cache" element={<CacheSettings />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export type TimeFormat = "24h" | "12h";
|
|||||||
export interface DisplaySettings {
|
export interface DisplaySettings {
|
||||||
dateFormat: DateFormat;
|
dateFormat: DateFormat;
|
||||||
timeFormat: TimeFormat;
|
timeFormat: TimeFormat;
|
||||||
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { getPageSize } from "../utils/pageSize";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client-side pagination over an already-fetched array. Purely a display
|
* Client-side pagination over an already-fetched array. Purely a display
|
||||||
@@ -10,8 +11,11 @@ import { useMemo, useState } from "react";
|
|||||||
* integration, a filter that shrinks the result set) can never strand the
|
* integration, a filter that shrinks the result set) can never strand the
|
||||||
* view on a page that no longer exists — no caller-side "reset page on
|
* view on a page that no longer exists — no caller-side "reset page on
|
||||||
* filter change" wiring required.
|
* filter change" wiring required.
|
||||||
|
*
|
||||||
|
* Defaults to the app-wide "rows per page" setting (Settings → Display) —
|
||||||
|
* pass an explicit pageSize only to override it for one specific table.
|
||||||
*/
|
*/
|
||||||
export function usePagination<T>(items: T[] | null | undefined, pageSize = 20) {
|
export function usePagination<T>(items: T[] | null | undefined, pageSize = getPageSize()) {
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
|
||||||
const totalCount = items?.length ?? 0;
|
const totalCount = items?.length ?? 0;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { NavLink, Outlet } from "react-router-dom";
|
|||||||
const SUB_NAV = [
|
const SUB_NAV = [
|
||||||
{ to: "/settings/notifications", label: "Notifications" },
|
{ to: "/settings/notifications", label: "Notifications" },
|
||||||
{ to: "/settings/badges", label: "Badges" },
|
{ to: "/settings/badges", label: "Badges" },
|
||||||
{ to: "/settings/date-time", label: "Date & Time" },
|
{ to: "/settings/display", label: "Display" },
|
||||||
{ to: "/settings/cache", label: "Cache" },
|
{ to: "/settings/cache", label: "Cache" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { api, type AppSettings, type DateFormat, type TimeFormat } from "../../api/client";
|
|
||||||
import { setDateTimeSettings } from "../../utils/date";
|
|
||||||
|
|
||||||
const DATE_FORMAT_OPTIONS: { value: DateFormat; label: string; example: string }[] = [
|
|
||||||
{ value: "ymd", label: "Year-Month-Day", example: "2026-09-15" },
|
|
||||||
{ value: "dmy", label: "Day/Month/Year", example: "15/09/2026" },
|
|
||||||
{ value: "mdy", label: "Month/Day/Year", example: "09/15/2026" },
|
|
||||||
];
|
|
||||||
|
|
||||||
function previewTime(timeFormat: TimeFormat): string {
|
|
||||||
return timeFormat === "12h" ? "2:30:00 PM" : "14:30:00";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DateTimeSettings() {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
|
||||||
const [dateFormat, setDateFormatState] = useState<DateFormat>("ymd");
|
|
||||||
const [timeFormat, setTimeFormatState] = useState<TimeFormat>("24h");
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [saved, setSaved] = useState(false);
|
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
api.settings
|
|
||||||
.get()
|
|
||||||
.then((res: { settings: AppSettings }) => {
|
|
||||||
setDateFormatState(res.settings.display.dateFormat);
|
|
||||||
setTimeFormatState(res.settings.display.timeFormat);
|
|
||||||
})
|
|
||||||
.catch((err) => setLoadError(err instanceof Error ? err.message : String(err)))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
setSaving(true);
|
|
||||||
setSaveError(null);
|
|
||||||
setSaved(false);
|
|
||||||
try {
|
|
||||||
const res = await api.settings.update({ display: { dateFormat, timeFormat } });
|
|
||||||
setDateTimeSettings(res.settings.display);
|
|
||||||
setSaved(true);
|
|
||||||
setTimeout(() => setSaved(false), 3000);
|
|
||||||
} catch (err) {
|
|
||||||
setSaveError(err instanceof Error ? err.message : String(err));
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) return <div className="text-secondary">Loading…</div>;
|
|
||||||
|
|
||||||
const dateExample = DATE_FORMAT_OPTIONS.find((o) => o.value === dateFormat)?.example ?? "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
|
||||||
<h3 className="mb-0">Date & Time</h3>
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
{saveError && <span className="text-danger small">{saveError}</span>}
|
|
||||||
{saved && <span className="text-success small">✓ Settings saved</span>}
|
|
||||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
|
||||||
{saving ? "Saving…" : "Save Settings"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loadError && <div className="alert alert-danger">{loadError}</div>}
|
|
||||||
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="card-title">Display format</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<p className="text-secondary">
|
|
||||||
Controls how dates and times are shown throughout the app — tables previously mixed formats depending on
|
|
||||||
the page; this makes them consistent everywhere.
|
|
||||||
</p>
|
|
||||||
<div className="row g-3">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<label className="form-label">Date format</label>
|
|
||||||
<select className="form-select" value={dateFormat} onChange={(e) => setDateFormatState(e.target.value as DateFormat)}>
|
|
||||||
{DATE_FORMAT_OPTIONS.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label} ({opt.example})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6">
|
|
||||||
<label className="form-label">Clock</label>
|
|
||||||
<select className="form-select" value={timeFormat} onChange={(e) => setTimeFormatState(e.target.value as TimeFormat)}>
|
|
||||||
<option value="24h">24-hour (14:30:00)</option>
|
|
||||||
<option value="12h">12-hour (2:30:00 PM)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3">
|
|
||||||
<div className="text-secondary small">Preview</div>
|
|
||||||
<div>
|
|
||||||
<code>
|
|
||||||
{dateExample} {previewTime(timeFormat)}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { api, type AppSettings, type DateFormat, type TimeFormat } from "../../api/client";
|
||||||
|
import { setDateTimeSettings } from "../../utils/date";
|
||||||
|
import { setPageSize } from "../../utils/pageSize";
|
||||||
|
|
||||||
|
const DATE_FORMAT_OPTIONS: { value: DateFormat; label: string; example: string }[] = [
|
||||||
|
{ value: "ymd", label: "Year-Month-Day", example: "2026-09-15" },
|
||||||
|
{ value: "dmy", label: "Day/Month/Year", example: "15/09/2026" },
|
||||||
|
{ value: "mdy", label: "Month/Day/Year", example: "09/15/2026" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function previewTime(timeFormat: TimeFormat): string {
|
||||||
|
return timeFormat === "12h" ? "2:30:00 PM" : "14:30:00";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DisplaySettings() {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [dateFormat, setDateFormatState] = useState<DateFormat>("ymd");
|
||||||
|
const [timeFormat, setTimeFormatState] = useState<TimeFormat>("24h");
|
||||||
|
const [pageSize, setPageSizeState] = useState(20);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.settings
|
||||||
|
.get()
|
||||||
|
.then((res: { settings: AppSettings }) => {
|
||||||
|
setDateFormatState(res.settings.display.dateFormat);
|
||||||
|
setTimeFormatState(res.settings.display.timeFormat);
|
||||||
|
setPageSizeState(res.settings.display.pageSize);
|
||||||
|
})
|
||||||
|
.catch((err) => setLoadError(err instanceof Error ? err.message : String(err)))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true);
|
||||||
|
setSaveError(null);
|
||||||
|
setSaved(false);
|
||||||
|
try {
|
||||||
|
const res = await api.settings.update({ display: { dateFormat, timeFormat, pageSize } });
|
||||||
|
setDateTimeSettings(res.settings.display);
|
||||||
|
setPageSize(res.settings.display.pageSize);
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
setSaveError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <div className="text-secondary">Loading…</div>;
|
||||||
|
|
||||||
|
const dateExample = DATE_FORMAT_OPTIONS.find((o) => o.value === dateFormat)?.example ?? "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h3 className="mb-0">Display</h3>
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
{saveError && <span className="text-danger small">{saveError}</span>}
|
||||||
|
{saved && <span className="text-success small">✓ Settings saved</span>}
|
||||||
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? "Saving…" : "Save Settings"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loadError && <div className="alert alert-danger">{loadError}</div>}
|
||||||
|
|
||||||
|
<div className="row row-cards">
|
||||||
|
<div className="col-md-7">
|
||||||
|
<div className="card h-100">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">Date & time format</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p className="text-secondary">
|
||||||
|
Controls how dates and times are shown throughout the app — tables previously mixed formats
|
||||||
|
depending on the page; this makes them consistent everywhere.
|
||||||
|
</p>
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">Date format</label>
|
||||||
|
<select className="form-select" value={dateFormat} onChange={(e) => setDateFormatState(e.target.value as DateFormat)}>
|
||||||
|
{DATE_FORMAT_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label} ({opt.example})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">Clock</label>
|
||||||
|
<select className="form-select" value={timeFormat} onChange={(e) => setTimeFormatState(e.target.value as TimeFormat)}>
|
||||||
|
<option value="24h">24-hour (14:30:00)</option>
|
||||||
|
<option value="12h">12-hour (2:30:00 PM)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="text-secondary small">Preview</div>
|
||||||
|
<div>
|
||||||
|
<code>
|
||||||
|
{dateExample} {previewTime(timeFormat)}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-5">
|
||||||
|
<div className="card h-100">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">Table pagination</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p className="text-secondary">
|
||||||
|
How many rows a paginated table (Secrets, IP Addresses, DNS records, Servers, Audit Log, and each
|
||||||
|
integration's device/guest/repo/template list) shows per page.
|
||||||
|
</p>
|
||||||
|
<label className="form-label">Rows per page</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
min={5}
|
||||||
|
max={500}
|
||||||
|
value={pageSize}
|
||||||
|
onChange={(e) => setPageSizeState(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
<div className="form-hint">Between 5 and 500.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
let current = 20;
|
||||||
|
|
||||||
|
/** Called once at app startup (and whenever the setting is changed) so usePagination() picks it up without prop-drilling — same pattern as utils/date.ts's date/time settings. */
|
||||||
|
export function setPageSize(size: number): void {
|
||||||
|
current = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageSize(): number {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user