refactor(webui): use central reasoned buttons

This commit is contained in:
2026-07-21 12:38:45 +02:00
parent 5d560d4c58
commit 70ee3c0148

View File

@@ -1,11 +1,10 @@
import { Download, Edit3, Link2, Plus, RefreshCw, RotateCcw, Save, Search, Trash2, Upload, UserPlus, X } from "lucide-react";
import { useCallback, useEffect, useMemo, useState, type ButtonHTMLAttributes, type DragEvent as ReactDragEvent, type FormEvent } from "react";
import { useCallback, useEffect, useMemo, useState, type DragEvent as ReactDragEvent, type FormEvent } from "react";
import {
ApiError,
Button,
ConfirmDialog,
Dialog,
DisabledActionTooltip,
DismissibleAlert,
ExplorerTree,
fetchAuthGroups,
@@ -604,19 +603,6 @@ function disabledReason(...conditions: Array<[boolean, string]>): string {
return conditions.find(([applies]) => applies)?.[1] ?? "";
}
type ReasonedButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary" | "ghost" | "danger";
disabledReason: string;
};
function ReasonedButton({ disabledReason, disabled, children, ...props }: ReasonedButtonProps) {
return (
<DisabledActionTooltip reason={disabledReason}>
<Button {...props} disabled={disabled || Boolean(disabledReason)}>{children}</Button>
</DisabledActionTooltip>
);
}
export default function AddressBookPage({ settings, auth, onAuthChange }: Props) {
const [books, setBooks] = useState<AddressBook[]>([]);
const [addressLists, setAddressLists] = useState<AddressList[]>([]);
@@ -1775,27 +1761,27 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
function renderSelectedBookActions() {
return (
<>
<ReasonedButton type="button" title="Refresh address books" aria-label="Refresh address books" onClick={() => void refreshAll()} disabledReason={refreshReason}><RefreshCw size={15} /></ReasonedButton>
<ReasonedButton type="button" title="Add address book" aria-label="Add address book" variant="primary" onClick={openCreateBookDialog} disabledReason={createBookReason}><Plus size={15} /></ReasonedButton>
<ReasonedButton type="button" title="Add address list" aria-label="Add address list" onClick={openCreateListDialog} disabledReason={createListReason}><Plus size={15} /></ReasonedButton>
<ReasonedButton type="button" title="Import vCard into selected address book" aria-label="Import vCard into selected address book" onClick={() => setImportOpen(true)} disabledReason={importBookReason}><Upload size={15} /></ReasonedButton>
<ReasonedButton type="button" title="Export selected address book as vCard" aria-label="Export selected address book as vCard" onClick={() => void exportSelectedBook()} disabledReason={exportBookReason}><Download size={15} /></ReasonedButton>
<ReasonedButton type="button" title="Connect CardDAV" aria-label="Connect CardDAV" onClick={openCardDavDialog} disabledReason={connectCardDavReason}><Link2 size={15} /></ReasonedButton>
<ReasonedButton type="button" title="Inspect sync source" aria-label="Inspect sync source" onClick={() => selectedSyncSource && void openSyncInspector(selectedSyncSource)} disabledReason={inspectSyncReason}><Search size={15} /></ReasonedButton>
<ReasonedButton type="button" title="Preview sync" aria-label="Preview sync" onClick={() => void previewSelectedSync()} disabledReason={previewSyncReason}>Preview</ReasonedButton>
<ReasonedButton type="button" title="Run sync" aria-label="Run sync" onClick={() => void runSelectedSync()} disabledReason={runSyncReason}>Sync</ReasonedButton>
<Button type="button" title="Refresh address books" aria-label="Refresh address books" onClick={() => void refreshAll()} disabledReason={refreshReason}><RefreshCw size={15} /></Button>
<Button type="button" title="Add address book" aria-label="Add address book" variant="primary" onClick={openCreateBookDialog} disabledReason={createBookReason}><Plus size={15} /></Button>
<Button type="button" title="Add address list" aria-label="Add address list" onClick={openCreateListDialog} disabledReason={createListReason}><Plus size={15} /></Button>
<Button type="button" title="Import vCard into selected address book" aria-label="Import vCard into selected address book" onClick={() => setImportOpen(true)} disabledReason={importBookReason}><Upload size={15} /></Button>
<Button type="button" title="Export selected address book as vCard" aria-label="Export selected address book as vCard" onClick={() => void exportSelectedBook()} disabledReason={exportBookReason}><Download size={15} /></Button>
<Button type="button" title="Connect CardDAV" aria-label="Connect CardDAV" onClick={openCardDavDialog} disabledReason={connectCardDavReason}><Link2 size={15} /></Button>
<Button type="button" title="Inspect sync source" aria-label="Inspect sync source" onClick={() => selectedSyncSource && void openSyncInspector(selectedSyncSource)} disabledReason={inspectSyncReason}><Search size={15} /></Button>
<Button type="button" title="Preview sync" aria-label="Preview sync" onClick={() => void previewSelectedSync()} disabledReason={previewSyncReason}>Preview</Button>
<Button type="button" title="Run sync" aria-label="Run sync" onClick={() => void runSelectedSync()} disabledReason={runSyncReason}>Sync</Button>
{selectedList ?
selectedList.deleted_at ?
<ReasonedButton type="button" title="Restore address list" aria-label="Restore address list" disabledReason={restoreListReason(selectedList)} onClick={() => void restoreDeletedList(selectedList)}><RotateCcw size={15} /></ReasonedButton> :
<Button type="button" title="Restore address list" aria-label="Restore address list" disabledReason={restoreListReason(selectedList)} onClick={() => void restoreDeletedList(selectedList)}><RotateCcw size={15} /></Button> :
<>
<ReasonedButton type="button" title="Edit address list" aria-label="Edit address list" disabledReason={editListReason(selectedList)} onClick={() => openEditListDialog(selectedList)}><Edit3 size={15} /></ReasonedButton>
<ReasonedButton type="button" variant="danger" title="Delete address list" aria-label="Delete address list" disabledReason={deleteListReason(selectedList)} onClick={() => setConfirmState({ kind: "list", list: selectedList })}><Trash2 size={15} /></ReasonedButton>
<Button type="button" title="Edit address list" aria-label="Edit address list" disabledReason={editListReason(selectedList)} onClick={() => openEditListDialog(selectedList)}><Edit3 size={15} /></Button>
<Button type="button" variant="danger" title="Delete address list" aria-label="Delete address list" disabledReason={deleteListReason(selectedList)} onClick={() => setConfirmState({ kind: "list", list: selectedList })}><Trash2 size={15} /></Button>
</> :
selectedBook?.deleted_at ?
<ReasonedButton type="button" title="Restore address book" aria-label="Restore address book" disabledReason={restoreBookReason(selectedBook)} onClick={() => void restoreBook(selectedBook)}><RotateCcw size={15} /></ReasonedButton> :
<Button type="button" title="Restore address book" aria-label="Restore address book" disabledReason={restoreBookReason(selectedBook)} onClick={() => void restoreBook(selectedBook)}><RotateCcw size={15} /></Button> :
<>
<ReasonedButton type="button" title="Edit address book" aria-label="Edit address book" disabledReason={selectedBook ? editBookReason(selectedBook) : "Select an address book before editing."} onClick={() => selectedBook && openEditBookDialog(selectedBook)}><Edit3 size={15} /></ReasonedButton>
<ReasonedButton type="button" variant="danger" title="Delete address book" aria-label="Delete address book" disabledReason={selectedBook ? deleteBookReason(selectedBook) : "Select an address book before deleting."} onClick={() => selectedBook && setConfirmState({ kind: "book", book: selectedBook })}><Trash2 size={15} /></ReasonedButton>
<Button type="button" title="Edit address book" aria-label="Edit address book" disabledReason={selectedBook ? editBookReason(selectedBook) : "Select an address book before editing."} onClick={() => selectedBook && openEditBookDialog(selectedBook)}><Edit3 size={15} /></Button>
<Button type="button" variant="danger" title="Delete address book" aria-label="Delete address book" disabledReason={selectedBook ? deleteBookReason(selectedBook) : "Select an address book before deleting."} onClick={() => selectedBook && setConfirmState({ kind: "book", book: selectedBook })}><Trash2 size={15} /></Button>
</>
}
</>
@@ -1841,11 +1827,11 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
</div>
<div className="button-row compact-actions">
{selectedContact.deleted_at ?
<ReasonedButton type="button" title="Restore contact" aria-label="Restore contact" disabledReason={restoreContactReason()} onClick={() => void restoreDeletedContact(selectedContact)}><RotateCcw size={15} /> Restore</ReasonedButton> :
<Button type="button" title="Restore contact" aria-label="Restore contact" disabledReason={restoreContactReason()} onClick={() => void restoreDeletedContact(selectedContact)}><RotateCcw size={15} /> Restore</Button> :
<>
<ReasonedButton type="button" title="Export contact vCard" aria-label="Export contact vCard" disabledReason={exportContactReason()} onClick={() => void exportOneContact(selectedContact)}><Download size={15} /> vCard</ReasonedButton>
<ReasonedButton type="button" title="Edit contact" aria-label="Edit contact" disabledReason={editContactReason()} onClick={() => openEditContactDialog(selectedContact)}><Edit3 size={15} /> Edit</ReasonedButton>
<ReasonedButton type="button" variant="danger" title="Delete contact" aria-label="Delete contact" disabledReason={deleteContactReason()} onClick={() => setConfirmState({ kind: "contact", contact: selectedContact })}><Trash2 size={15} /> Delete</ReasonedButton>
<Button type="button" title="Export contact vCard" aria-label="Export contact vCard" disabledReason={exportContactReason()} onClick={() => void exportOneContact(selectedContact)}><Download size={15} /> vCard</Button>
<Button type="button" title="Edit contact" aria-label="Edit contact" disabledReason={editContactReason()} onClick={() => openEditContactDialog(selectedContact)}><Edit3 size={15} /> Edit</Button>
<Button type="button" variant="danger" title="Delete contact" aria-label="Delete contact" disabledReason={deleteContactReason()} onClick={() => setConfirmState({ kind: "contact", contact: selectedContact })}><Trash2 size={15} /> Delete</Button>
</>
}
</div>
@@ -1862,13 +1848,13 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
{selectedContactListEntries.map((entry) => (
<div className="address-membership-row" key={entry.id}>
<p className="muted">{entryTargetLabel(entry)}</p>
<ReasonedButton
<Button
type="button"
variant="danger"
onClick={() => void removeAddressListEntry(entry)}
disabledReason={removeContactFromListReason()}>
<X size={15} /> Remove
</ReasonedButton>
</Button>
</div>
))}
</div>
@@ -1985,9 +1971,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
</div>
<div className="button-row compact-actions">
{selectedList &&
<ReasonedButton type="button" onClick={() => void openAddMembersDialog()} disabledReason={addMembersReason}><UserPlus size={16} /> Add to list</ReasonedButton>
<Button type="button" onClick={() => void openAddMembersDialog()} disabledReason={addMembersReason}><UserPlus size={16} /> Add to list</Button>
}
<ReasonedButton type="button" variant="primary" onClick={openCreateContactDialog} disabledReason={createContactReason}><Plus size={16} /> Contact</ReasonedButton>
<Button type="button" variant="primary" onClick={openCreateContactDialog} disabledReason={createContactReason}><Plus size={16} /> Contact</Button>
</div>
</header>
<div className="address-contact-toolbar">
@@ -2017,8 +2003,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
footerClassName="button-row compact-actions"
footer={
<>
<ReasonedButton type="button" onClick={() => setBookDialog(null)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
<ReasonedButton type="submit" form="address-book-form" variant="primary" disabledReason={bookSaveReason}><Save size={16} /> Save</ReasonedButton>
<Button type="button" onClick={() => setBookDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
<Button type="submit" form="address-book-form" variant="primary" disabledReason={bookSaveReason}><Save size={16} /> Save</Button>
</>
}>
<form id="address-book-form" className="address-dialog-form" onSubmit={(event) => void submitBook(event)}>
@@ -2053,8 +2039,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
footerClassName="button-row compact-actions"
footer={
<>
<ReasonedButton type="button" onClick={() => setListDialog(null)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
<ReasonedButton type="submit" form="address-list-form" variant="primary" disabledReason={listSaveReason}><Save size={16} /> Save</ReasonedButton>
<Button type="button" onClick={() => setListDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
<Button type="submit" form="address-list-form" variant="primary" disabledReason={listSaveReason}><Save size={16} /> Save</Button>
</>
}>
<form id="address-list-form" className="address-dialog-form" onSubmit={(event) => void submitList(event)}>
@@ -2080,8 +2066,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
footerClassName="button-row compact-actions"
footer={
<>
<ReasonedButton type="button" onClick={() => setContactDialog(null)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
<ReasonedButton type="submit" form="address-contact-form" variant="primary" disabledReason={contactSaveReason}><Save size={16} /> Save</ReasonedButton>
<Button type="button" onClick={() => setContactDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
<Button type="submit" form="address-contact-form" variant="primary" disabledReason={contactSaveReason}><Save size={16} /> Save</Button>
</>
}>
<form id="address-contact-form" className="address-dialog-form" onSubmit={(event) => void submitContact(event)}>
@@ -2097,7 +2083,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
<section className="address-form-section">
<div className="address-form-section-heading">
<strong>Email addresses</strong>
<ReasonedButton type="button" onClick={addEmailRow} disabledReason={addContactRowReason}><Plus size={15} /> Email</ReasonedButton>
<Button type="button" onClick={addEmailRow} disabledReason={addContactRowReason}><Plus size={15} /> Email</Button>
</div>
<div className="address-form-list">
{contactForm.emails.map((email) => (
@@ -2105,7 +2091,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
<label className="address-primary-choice"><input type="radio" name="address-primary-email" checked={email.is_primary} onChange={() => setPrimaryEmailRow(email.rowId)} /> Primary</label>
<input value={email.label} placeholder="Label" onChange={(event) => updateEmailRow(email.rowId, { label: event.target.value })} />
<input type="email" value={email.email} placeholder="Email address" onChange={(event) => updateEmailRow(email.rowId, { email: event.target.value })} />
<ReasonedButton type="button" variant="danger" title="Remove email" aria-label="Remove email" disabledReason={removeEmailRowReason} onClick={() => removeEmailRow(email.rowId)}><Trash2 size={15} /></ReasonedButton>
<Button type="button" variant="danger" title="Remove email" aria-label="Remove email" disabledReason={removeEmailRowReason} onClick={() => removeEmailRow(email.rowId)}><Trash2 size={15} /></Button>
</div>
))}
</div>
@@ -2114,7 +2100,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
<section className="address-form-section">
<div className="address-form-section-heading">
<strong>Phone numbers</strong>
<ReasonedButton type="button" onClick={addPhoneRow} disabledReason={addContactRowReason}><Plus size={15} /> Phone</ReasonedButton>
<Button type="button" onClick={addPhoneRow} disabledReason={addContactRowReason}><Plus size={15} /> Phone</Button>
</div>
<div className="address-form-list">
{contactForm.phones.map((phone) => (
@@ -2122,7 +2108,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
<label className="address-primary-choice"><input type="radio" name="address-primary-phone" checked={phone.is_primary} onChange={() => setPrimaryPhoneRow(phone.rowId)} /> Primary</label>
<input value={phone.label} placeholder="Label" onChange={(event) => updatePhoneRow(phone.rowId, { label: event.target.value })} />
<input value={phone.phone} placeholder="Phone number" onChange={(event) => updatePhoneRow(phone.rowId, { phone: event.target.value })} />
<ReasonedButton type="button" variant="danger" title="Remove phone" aria-label="Remove phone" disabledReason={removePhoneRowReason} onClick={() => removePhoneRow(phone.rowId)}><Trash2 size={15} /></ReasonedButton>
<Button type="button" variant="danger" title="Remove phone" aria-label="Remove phone" disabledReason={removePhoneRowReason} onClick={() => removePhoneRow(phone.rowId)}><Trash2 size={15} /></Button>
</div>
))}
</div>
@@ -2131,7 +2117,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
<section className="address-form-section">
<div className="address-form-section-heading">
<strong>Postal addresses</strong>
<ReasonedButton type="button" onClick={addPostalAddressRow} disabledReason={addContactRowReason}><Plus size={15} /> Address</ReasonedButton>
<Button type="button" onClick={addPostalAddressRow} disabledReason={addContactRowReason}><Plus size={15} /> Address</Button>
</div>
<div className="address-form-list">
{contactForm.postal_addresses.map((address) => (
@@ -2143,7 +2129,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
<input value={address.locality} placeholder="Locality" onChange={(event) => updatePostalAddressRow(address.rowId, { locality: event.target.value })} />
<input value={address.region} placeholder="Region" onChange={(event) => updatePostalAddressRow(address.rowId, { region: event.target.value })} />
<input value={address.country} placeholder="Country" onChange={(event) => updatePostalAddressRow(address.rowId, { country: event.target.value })} />
<ReasonedButton type="button" variant="danger" title="Remove postal address" aria-label="Remove postal address" disabledReason={removePostalAddressRowReason} onClick={() => removePostalAddressRow(address.rowId)}><Trash2 size={15} /></ReasonedButton>
<Button type="button" variant="danger" title="Remove postal address" aria-label="Remove postal address" disabledReason={removePostalAddressRowReason} onClick={() => removePostalAddressRow(address.rowId)}><Trash2 size={15} /></Button>
</div>
))}
</div>
@@ -2160,7 +2146,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
closeDisabled={saving}
className="address-member-dialog"
footerClassName="button-row compact-actions"
footer={<ReasonedButton type="button" onClick={() => setMemberDialogOpen(false)} disabledReason={dialogCancelReason}>Close</ReasonedButton>}>
footer={<Button type="button" onClick={() => setMemberDialogOpen(false)} disabledReason={dialogCancelReason}>Close</Button>}>
<div className="address-member-picker">
<div className="address-contact-toolbar address-member-search">
<input value={memberQuery} onChange={(event) => setMemberQuery(event.target.value)} placeholder="Search contacts to add" autoFocus />
@@ -2184,12 +2170,12 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
disabled={saving || targetOptions.length === 0}>
{targetOptions.map((option) => <option key={option.key} value={option.key}>{option.label}</option>)}
</select>
<ReasonedButton
<Button
type="button"
onClick={() => selectedList && void addContactToAddressList(selectedList, contact, selectedTarget)}
disabledReason={addContactToListReason(selectedList, contact, selectedTarget?.key ?? "")}>
<Plus size={15} /> Add
</ReasonedButton>
</Button>
</div>
);
})
@@ -2207,8 +2193,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
footerClassName="button-row compact-actions"
footer={
<>
<ReasonedButton type="button" onClick={() => setImportOpen(false)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
<ReasonedButton type="submit" form="address-vcard-import-form" variant="primary" disabledReason={vcardImportReason}><Upload size={16} /> Import</ReasonedButton>
<Button type="button" onClick={() => setImportOpen(false)} disabledReason={dialogCancelReason}>Cancel</Button>
<Button type="submit" form="address-vcard-import-form" variant="primary" disabledReason={vcardImportReason}><Upload size={16} /> Import</Button>
</>
}>
<form id="address-vcard-import-form" className="address-dialog-form" onSubmit={(event) => void submitVcardImport(event)}>
@@ -2234,9 +2220,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
footerClassName="button-row compact-actions"
footer={
<>
<ReasonedButton type="button" onClick={() => setCardDavOpen(false)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
<ReasonedButton type="button" onClick={() => void discoverCardDavSources()} disabledReason={disabledReason([saving, savingReason], [!cardDavForm.collection_url.trim(), "Enter a CardDAV URL before discovery."])}><Search size={16} /> Discover</ReasonedButton>
<ReasonedButton type="submit" form="address-carddav-form" variant="primary" disabledReason={disabledReason([saving, savingReason], [!cardDavForm.collection_url.trim(), "Enter a CardDAV address-book URL before saving."])}><Save size={16} /> Connect</ReasonedButton>
<Button type="button" onClick={() => setCardDavOpen(false)} disabledReason={dialogCancelReason}>Cancel</Button>
<Button type="button" onClick={() => void discoverCardDavSources()} disabledReason={disabledReason([saving, savingReason], [!cardDavForm.collection_url.trim(), "Enter a CardDAV URL before discovery."])}><Search size={16} /> Discover</Button>
<Button type="submit" form="address-carddav-form" variant="primary" disabledReason={disabledReason([saving, savingReason], [!cardDavForm.collection_url.trim(), "Enter a CardDAV address-book URL before saving."])}><Save size={16} /> Connect</Button>
</>
}>
<form id="address-carddav-form" className="address-dialog-form" onSubmit={(event) => void submitCardDavSource(event)}>
@@ -2302,9 +2288,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
footerClassName="button-row compact-actions"
footer={
<>
<ReasonedButton type="button" onClick={() => setSyncInspector(null)} disabledReason={dialogCancelReason}>Close</ReasonedButton>
<ReasonedButton type="button" onClick={() => syncInspector && void previewSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [saving, savingReason])}>Preview</ReasonedButton>
<ReasonedButton type="button" variant="primary" onClick={() => syncInspector && void runSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [!canWriteSync, "You need permission to run address sync."], [saving, savingReason])}>Run sync</ReasonedButton>
<Button type="button" onClick={() => setSyncInspector(null)} disabledReason={dialogCancelReason}>Close</Button>
<Button type="button" onClick={() => syncInspector && void previewSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [saving, savingReason])}>Preview</Button>
<Button type="button" variant="primary" onClick={() => syncInspector && void runSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [!canWriteSync, "You need permission to run address sync."], [saving, savingReason])}>Run sync</Button>
</>
}>
{syncInspector &&
@@ -2322,19 +2308,19 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
<p className="muted">Last attempt: {formatDateTime(syncInspector.source.last_attempted_at)} · Last success: {formatDateTime(syncInspector.source.last_success_at)}</p>
{syncInspector.source.last_error && <DismissibleAlert tone="danger" resetKey={syncInspector.source.last_error}>{syncInspector.source.last_error}</DismissibleAlert>}
<div className="button-row compact-actions">
<ReasonedButton
<Button
type="button"
onClick={() => void toggleSyncSourceEnabled(syncInspector.source)}
disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
{syncInspector.source.enabled ? "Disable" : "Enable"}
</ReasonedButton>
<ReasonedButton
</Button>
<Button
type="button"
variant="danger"
onClick={() => setConfirmState({ kind: "sync-source", source: syncInspector.source })}
disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
Disconnect
</ReasonedButton>
</Button>
</div>
</div>
@@ -2361,7 +2347,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
{syncConflicts.map((conflict) => (
<div className="address-sync-record-row" key={conflict.id}>
<span><strong>{conflict.field_path}</strong><small>{conflict.resource_href || conflict.remote_uid || conflict.contact_id}</small></span>
<ReasonedButton type="button" onClick={() => openConflictReview(conflict)} disabledReason={disabledReason([saving, savingReason])}>Review</ReasonedButton>
<Button type="button" onClick={() => openConflictReview(conflict)} disabledReason={disabledReason([saving, savingReason])}>Review</Button>
</div>
))}
</div>
@@ -2405,20 +2391,20 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
className="address-sync-dialog"
footerClassName="button-row compact-actions"
footer={conflictDialog && <>
<ReasonedButton type="button" onClick={() => { setConflictDialog(null); setConflictMergeChoices({}); }} disabledReason={dialogCancelReason}>Close</ReasonedButton>
<ReasonedButton
<Button type="button" onClick={() => { setConflictDialog(null); setConflictMergeChoices({}); }} disabledReason={dialogCancelReason}>Close</Button>
<Button
type="button"
onClick={() => void resolveConflictWith(conflictDialog, "ignored", "ignored")}
disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
Ignore
</ReasonedButton>
<ReasonedButton
</Button>
<Button
type="button"
onClick={() => void resolveConflictWith(conflictDialog, "keep_local")}
disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
Keep local
</ReasonedButton>
<ReasonedButton
</Button>
<Button
type="button"
onClick={() => void applyMergedConflict(conflictDialog)}
disabledReason={disabledReason(
@@ -2427,8 +2413,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
[saving, savingReason]
)}>
Apply merged
</ReasonedButton>
<ReasonedButton
</Button>
<Button
type="button"
variant="primary"
onClick={() => void resolveConflictWith(conflictDialog, "use_remote")}
@@ -2438,7 +2424,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
[saving, savingReason]
)}>
Use remote
</ReasonedButton>
</Button>
</>}>
{conflictDialog &&
<div className="address-conflict-review">