fix(web): drop Refresh button, move Add right-aligned on the title row
- PageHead is now a single column: eyebrow, then a row with the title
('Accounts <total>') on the left and the Add pill flush right via
flex justify-between. Subtitle ('Showing X of Y') sits below.
- Refresh button removed entirely. The internal refresh() function
stays — the create-account / create-user dialog still calls it on
success so the new row appears in its sorted position. The 30-second
cache + revalidateTag-on-mutation already keeps the table fresh
without a manual button.
- Removed the now-unused 'refreshing' state + the matching dependency
from the IntersectionObserver effect.
This commit is contained in:
parent
fa93a8c866
commit
9b222eec56
@ -68,7 +68,6 @@ export default function AccountsTable({
|
|||||||
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
||||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
@ -112,16 +111,15 @@ export default function AccountsTable({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force-evict the cache and re-fetch page 1. Used by the create-success
|
||||||
|
// path so a freshly added row appears in its sorted position. Keeping
|
||||||
|
// the function (instead of inlining at the call site) means a future
|
||||||
|
// 'pull to refresh' gesture has a single hook.
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
setRefreshing(true);
|
|
||||||
try {
|
|
||||||
const page = await refreshAccounts({ prefix: prefixPattern, dir: sortDir });
|
const page = await refreshAccounts({ prefix: prefixPattern, dir: sortDir });
|
||||||
setRows(page.rows);
|
setRows(page.rows);
|
||||||
setHasMore(page.hasMore);
|
setHasMore(page.hasMore);
|
||||||
setTotal(page.total);
|
setTotal(page.total);
|
||||||
} finally {
|
|
||||||
setRefreshing(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeSort(next: SortDir) {
|
async function changeSort(next: SortDir) {
|
||||||
@ -146,7 +144,7 @@ export default function AccountsTable({
|
|||||||
// 300px rootMargin so the next page starts loading before the user
|
// 300px rootMargin so the next page starts loading before the user
|
||||||
// hits the bottom — feels seamless when scrolling fast.
|
// hits the bottom — feels seamless when scrolling fast.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasMore || loadingMore || refreshing) return;
|
if (!hasMore || loadingMore) return;
|
||||||
const sentinel = sentinelRef.current;
|
const sentinel = sentinelRef.current;
|
||||||
if (!sentinel) return;
|
if (!sentinel) return;
|
||||||
|
|
||||||
@ -171,7 +169,7 @@ export default function AccountsTable({
|
|||||||
);
|
);
|
||||||
observer.observe(sentinel);
|
observer.observe(sentinel);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [hasMore, loadingMore, refreshing, rows.length, prefixPattern, sortDir]);
|
}, [hasMore, loadingMore, rows.length, prefixPattern, sortDir]);
|
||||||
|
|
||||||
async function confirmDelete() {
|
async function confirmDelete() {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
@ -196,8 +194,6 @@ export default function AccountsTable({
|
|||||||
<PageHead
|
<PageHead
|
||||||
total={total}
|
total={total}
|
||||||
loaded={0}
|
loaded={0}
|
||||||
onRefresh={refresh}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onAdd={() => setCreateOpen(true)}
|
onAdd={() => setCreateOpen(true)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-6 rounded-2xl bg-white px-8 py-16 text-center ring-1 ring-zinc-200/60">
|
<div className="mt-6 rounded-2xl bg-white px-8 py-16 text-center ring-1 ring-zinc-200/60">
|
||||||
@ -225,8 +221,6 @@ export default function AccountsTable({
|
|||||||
<PageHead
|
<PageHead
|
||||||
total={total}
|
total={total}
|
||||||
loaded={optimistic.length}
|
loaded={optimistic.length}
|
||||||
onRefresh={refresh}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onAdd={() => setCreateOpen(true)}
|
onAdd={() => setCreateOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -446,14 +440,10 @@ function CardRow({ label, children }: { label: string; children: React.ReactNode
|
|||||||
function PageHead({
|
function PageHead({
|
||||||
total,
|
total,
|
||||||
loaded,
|
loaded,
|
||||||
onRefresh,
|
|
||||||
refreshing,
|
|
||||||
onAdd,
|
onAdd,
|
||||||
}: {
|
}: {
|
||||||
total: number;
|
total: number;
|
||||||
loaded: number;
|
loaded: number;
|
||||||
onRefresh: () => void;
|
|
||||||
refreshing: boolean;
|
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
}) {
|
}) {
|
||||||
// total = COUNT(*) from the API; loaded = how many rows the user has
|
// total = COUNT(*) from the API; loaded = how many rows the user has
|
||||||
@ -461,44 +451,32 @@ function PageHead({
|
|||||||
// from the total, so a fully-scrolled list reads cleanly.
|
// from the total, so a fully-scrolled list reads cleanly.
|
||||||
const showLoaded = loaded > 0 && loaded < total;
|
const showLoaded = loaded > 0 && loaded < total;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">
|
<p className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">
|
||||||
Table
|
Table
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
|
<div className="mt-1 flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
|
||||||
Accounts
|
Accounts
|
||||||
<span className="ml-2 align-middle text-base font-medium text-zinc-400">
|
<span className="ml-2 align-middle text-base font-medium text-zinc-400">
|
||||||
{total}
|
{total}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAdd}
|
||||||
|
aria-label="Add account"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-colors hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">+</span>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{showLoaded && (
|
{showLoaded && (
|
||||||
<p className="mt-1 text-[11px] text-zinc-400">
|
<p className="mt-1 text-[11px] text-zinc-400">
|
||||||
Showing {loaded} of {total}
|
Showing {loaded} of {total}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRefresh}
|
|
||||||
disabled={refreshing}
|
|
||||||
className="inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-xs font-medium text-zinc-700 shadow-sm ring-1 ring-zinc-200 transition-colors hover:bg-zinc-50 hover:text-zinc-900 disabled:opacity-60"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true" className={refreshing ? "inline-block animate-spin" : ""}>
|
|
||||||
⟳
|
|
||||||
</span>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onAdd}
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-full bg-zinc-900 px-4 py-2 text-xs font-medium text-white shadow-sm transition-colors hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">+</span>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,6 @@ export default function UsersTable({
|
|||||||
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
||||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
@ -123,9 +122,9 @@ export default function UsersTable({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force-evict the cache and re-fetch page 1. Used by the create-success
|
||||||
|
// path so a freshly added row appears in its sorted position.
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
setRefreshing(true);
|
|
||||||
try {
|
|
||||||
const page = await refreshUsers({
|
const page = await refreshUsers({
|
||||||
prefix: prefixPattern,
|
prefix: prefixPattern,
|
||||||
sort: sortKey,
|
sort: sortKey,
|
||||||
@ -134,9 +133,6 @@ export default function UsersTable({
|
|||||||
setRows(page.rows);
|
setRows(page.rows);
|
||||||
setHasMore(page.hasMore);
|
setHasMore(page.hasMore);
|
||||||
setTotal(page.total);
|
setTotal(page.total);
|
||||||
} finally {
|
|
||||||
setRefreshing(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeSort(nextKey: SortKey) {
|
async function changeSort(nextKey: SortKey) {
|
||||||
@ -165,7 +161,7 @@ export default function UsersTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasMore || loadingMore || refreshing) return;
|
if (!hasMore || loadingMore) return;
|
||||||
const sentinel = sentinelRef.current;
|
const sentinel = sentinelRef.current;
|
||||||
if (!sentinel) return;
|
if (!sentinel) return;
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
@ -190,7 +186,7 @@ export default function UsersTable({
|
|||||||
);
|
);
|
||||||
observer.observe(sentinel);
|
observer.observe(sentinel);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [hasMore, loadingMore, refreshing, rows.length, prefixPattern, sortKey, sortDir]);
|
}, [hasMore, loadingMore, rows.length, prefixPattern, sortKey, sortDir]);
|
||||||
|
|
||||||
async function confirmDelete() {
|
async function confirmDelete() {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
@ -231,8 +227,6 @@ export default function UsersTable({
|
|||||||
<PageHead
|
<PageHead
|
||||||
total={total}
|
total={total}
|
||||||
loaded={0}
|
loaded={0}
|
||||||
onRefresh={refresh}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onAdd={() => setCreateOpen(true)}
|
onAdd={() => setCreateOpen(true)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-6 rounded-2xl bg-white px-8 py-16 text-center ring-1 ring-zinc-200/60">
|
<div className="mt-6 rounded-2xl bg-white px-8 py-16 text-center ring-1 ring-zinc-200/60">
|
||||||
@ -258,8 +252,6 @@ export default function UsersTable({
|
|||||||
<PageHead
|
<PageHead
|
||||||
total={total}
|
total={total}
|
||||||
loaded={optimistic.length}
|
loaded={optimistic.length}
|
||||||
onRefresh={refresh}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onAdd={() => setCreateOpen(true)}
|
onAdd={() => setCreateOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -468,54 +460,38 @@ function MobileRow({ label, children }: { label: string; children: React.ReactNo
|
|||||||
function PageHead({
|
function PageHead({
|
||||||
total,
|
total,
|
||||||
loaded,
|
loaded,
|
||||||
onRefresh,
|
|
||||||
refreshing,
|
|
||||||
onAdd,
|
onAdd,
|
||||||
}: {
|
}: {
|
||||||
total: number;
|
total: number;
|
||||||
loaded: number;
|
loaded: number;
|
||||||
onRefresh: () => void;
|
|
||||||
refreshing: boolean;
|
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
}) {
|
}) {
|
||||||
const showLoaded = loaded > 0 && loaded < total;
|
const showLoaded = loaded > 0 && loaded < total;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">Table</p>
|
<p className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">Table</p>
|
||||||
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
|
<div className="mt-1 flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
|
||||||
Users
|
Users
|
||||||
<span className="ml-2 align-middle text-base font-medium text-zinc-400">
|
<span className="ml-2 align-middle text-base font-medium text-zinc-400">
|
||||||
{total}
|
{total}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAdd}
|
||||||
|
aria-label="Add user"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-colors hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">+</span>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{showLoaded && (
|
{showLoaded && (
|
||||||
<p className="mt-1 text-[11px] text-zinc-400">
|
<p className="mt-1 text-[11px] text-zinc-400">
|
||||||
Showing {loaded} of {total}
|
Showing {loaded} of {total}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRefresh}
|
|
||||||
disabled={refreshing}
|
|
||||||
className="inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-xs font-medium text-zinc-700 shadow-sm ring-1 ring-zinc-200 transition-colors hover:bg-zinc-50 hover:text-zinc-900 disabled:opacity-60"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true" className={refreshing ? "inline-block animate-spin" : ""}>
|
|
||||||
⟳
|
|
||||||
</span>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onAdd}
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-full bg-zinc-900 px-4 py-2 text-xs font-medium text-white shadow-sm transition-colors hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">+</span>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user