Paginate address book contacts
This commit is contained in:
@@ -168,8 +168,12 @@ type AddressListEntryListResponse = {
|
||||
entries: AddressListEntry[];
|
||||
};
|
||||
|
||||
type ContactListResponse = {
|
||||
export type ContactListResponse = {
|
||||
contacts: Contact[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
type AddressBookWriteTargetsResponse = {
|
||||
@@ -588,11 +592,15 @@ export function resolveAddressSyncConflict(
|
||||
});
|
||||
}
|
||||
|
||||
export async function listContacts(settings: ApiSettings, options: {addressBookId?: string | null;query?: string | null;limit?: number;includeDeleted?: boolean;} = {}): Promise<Contact[]> {
|
||||
const response = await apiFetch<ContactListResponse>(
|
||||
export function listContactsPage(settings: ApiSettings, options: {addressBookId?: string | null;addressListId?: string | null;query?: string | null;limit?: number;offset?: number;includeDeleted?: boolean;} = {}): Promise<ContactListResponse> {
|
||||
return apiFetch<ContactListResponse>(
|
||||
settings,
|
||||
`/api/v1/addresses/contacts${queryString({ address_book_id: options.addressBookId, query: options.query, limit: options.limit, include_deleted: options.includeDeleted ? "true" : null })}`
|
||||
`/api/v1/addresses/contacts${queryString({ address_book_id: options.addressBookId, address_list_id: options.addressListId, query: options.query, limit: options.limit, offset: options.offset, include_deleted: options.includeDeleted ? "true" : null })}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function listContacts(settings: ApiSettings, options: {addressBookId?: string | null;addressListId?: string | null;query?: string | null;limit?: number;offset?: number;includeDeleted?: boolean;} = {}): Promise<Contact[]> {
|
||||
const response = await listContactsPage(settings, options);
|
||||
return response.contacts;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ApiError,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
DataGridPaginationBar,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
ExplorerTree,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
listAddressSyncSources,
|
||||
listAddressSyncTombstones,
|
||||
listContacts,
|
||||
listContactsPage,
|
||||
previewAddressSyncSource,
|
||||
restoreAddressBook,
|
||||
restoreAddressList,
|
||||
@@ -621,6 +623,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
const [addressListEntries, setAddressListEntries] = useState<AddressListEntry[]>([]);
|
||||
const [syncSources, setSyncSources] = useState<AddressSyncSource[]>([]);
|
||||
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||
const [contactTotal, setContactTotal] = useState(0);
|
||||
const [contactPage, setContactPage] = useState(1);
|
||||
const [contactPageSize, setContactPageSize] = useState(50);
|
||||
const [selectedBookId, setSelectedBookId] = useState("");
|
||||
const [selectedListId, setSelectedListId] = useState("");
|
||||
const [selectedContactId, setSelectedContactId] = useState("");
|
||||
@@ -718,10 +723,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
const addressTreeNodes = useMemo(() => buildAddressTree(books, addressLists), [addressLists, books]);
|
||||
const selectedListContactIds = useMemo(() => listEntryContactIds(addressListEntries), [addressListEntries]);
|
||||
const selectedListEntryKeys = useMemo(() => new Set(addressListEntries.map(addressListEntryKey)), [addressListEntries]);
|
||||
const visibleContacts = useMemo(
|
||||
() => selectedList ? contacts.filter((contact) => selectedListContactIds.has(contact.id)) : contacts,
|
||||
[contacts, selectedList, selectedListContactIds]
|
||||
);
|
||||
const visibleContacts = contacts;
|
||||
const memberCandidateContacts = useMemo(() => {
|
||||
const normalizedQuery = memberQuery.trim().toLowerCase();
|
||||
return memberCandidates
|
||||
@@ -985,10 +987,25 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
const refreshContacts = useCallback(async (bookId: string, search: string) => {
|
||||
if (!bookId) {
|
||||
setContacts([]);
|
||||
setContactTotal(0);
|
||||
return;
|
||||
}
|
||||
setContacts(await listContacts(settings, { addressBookId: bookId, query: search, limit: 500, includeDeleted: showArchived }));
|
||||
}, [settings, showArchived]);
|
||||
const response = await listContactsPage(settings, {
|
||||
addressBookId: bookId,
|
||||
addressListId: selectedListId || undefined,
|
||||
query: search,
|
||||
limit: contactPageSize,
|
||||
offset: (contactPage - 1) * contactPageSize,
|
||||
includeDeleted: showArchived
|
||||
});
|
||||
const pageCount = Math.max(1, Math.ceil(response.total / contactPageSize));
|
||||
if (contactPage > pageCount) {
|
||||
setContactPage(pageCount);
|
||||
return;
|
||||
}
|
||||
setContacts(response.contacts);
|
||||
setContactTotal(response.total);
|
||||
}, [contactPage, contactPageSize, selectedListId, settings, showArchived]);
|
||||
|
||||
const refreshListEntries = useCallback(async (listId: string) => {
|
||||
if (!listId) {
|
||||
@@ -1163,12 +1180,14 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
|
||||
function openTreeNode(node: AddressTreeNode) {
|
||||
if (node.kind === "book" && node.book) {
|
||||
setContactPage(1);
|
||||
setSelectedBookId(node.book.id);
|
||||
setSelectedListId("");
|
||||
setSelectedContactId("");
|
||||
return;
|
||||
}
|
||||
if (node.kind === "list" && node.list) {
|
||||
setContactPage(1);
|
||||
setSelectedBookId(node.list.address_book_id);
|
||||
setSelectedListId(node.list.id);
|
||||
setSelectedContactId("");
|
||||
@@ -1984,7 +2003,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
label="Show archived"
|
||||
checked={showArchived}
|
||||
disabled={Boolean(toggleArchiveReason)}
|
||||
onChange={() => setShowArchived((current) => !current)}
|
||||
onChange={() => {
|
||||
setContactPage(1);
|
||||
setShowArchived((current) => !current);
|
||||
}}
|
||||
help="Include archived address books, lists, and contacts."
|
||||
/>
|
||||
</div>
|
||||
@@ -2018,7 +2040,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
<div>
|
||||
<h2>{selectedList ? selectedList.name : selectedBook ? selectedBook.name : "Contacts"}</h2>
|
||||
<p>
|
||||
{visibleContacts.length} contact{visibleContacts.length === 1 ? "" : "s"}
|
||||
{contactTotal} contact{contactTotal === 1 ? "" : "s"}
|
||||
{selectedList ? " in selected list" : selectedBook ? " in selected book" : ""}
|
||||
</p>
|
||||
</div>
|
||||
@@ -2030,7 +2052,13 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
</div>
|
||||
</header>
|
||||
<div className="address-contact-toolbar">
|
||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search contacts" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => {
|
||||
setContactPage(1);
|
||||
setQuery(event.target.value);
|
||||
}}
|
||||
placeholder="Search contacts" />
|
||||
{selectedBook?.read_only && <StatusBadge status="read-only" />}
|
||||
{selectedBook?.deleted_at && <StatusBadge status="archived" />}
|
||||
</div>
|
||||
@@ -2042,6 +2070,19 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
</SelectionList>
|
||||
}
|
||||
</div>
|
||||
<DataGridPaginationBar
|
||||
page={contactPage}
|
||||
pageSize={contactPageSize}
|
||||
totalRows={contactTotal}
|
||||
pageSizeOptions={[25, 50, 100, 200]}
|
||||
disabled={loading || saving || !selectedBook}
|
||||
className="address-contact-pagination"
|
||||
ariaLabel="Contact pagination"
|
||||
onPageChange={setContactPage}
|
||||
onPageSizeChange={(pageSize) => {
|
||||
setContactPageSize(pageSize);
|
||||
setContactPage(1);
|
||||
}} />
|
||||
</section>
|
||||
|
||||
<section className="address-detail-panel" aria-label="Contact detail">
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.address-contact-pagination {
|
||||
border-top: var(--border-line);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.address-contact-selection-list {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user