refactor(webui): use central reasoned buttons
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
import { Download, Edit3, Link2, Plus, RefreshCw, RotateCcw, Save, Search, Trash2, Upload, UserPlus, X } from "lucide-react";
|
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 {
|
import {
|
||||||
ApiError,
|
ApiError,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
DisabledActionTooltip,
|
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
ExplorerTree,
|
ExplorerTree,
|
||||||
fetchAuthGroups,
|
fetchAuthGroups,
|
||||||
@@ -604,19 +603,6 @@ function disabledReason(...conditions: Array<[boolean, string]>): string {
|
|||||||
return conditions.find(([applies]) => applies)?.[1] ?? "";
|
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) {
|
export default function AddressBookPage({ settings, auth, onAuthChange }: Props) {
|
||||||
const [books, setBooks] = useState<AddressBook[]>([]);
|
const [books, setBooks] = useState<AddressBook[]>([]);
|
||||||
const [addressLists, setAddressLists] = useState<AddressList[]>([]);
|
const [addressLists, setAddressLists] = useState<AddressList[]>([]);
|
||||||
@@ -1775,27 +1761,27 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
function renderSelectedBookActions() {
|
function renderSelectedBookActions() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReasonedButton type="button" title="Refresh address books" aria-label="Refresh address books" onClick={() => void refreshAll()} disabledReason={refreshReason}><RefreshCw size={15} /></ReasonedButton>
|
<Button type="button" title="Refresh address books" aria-label="Refresh address books" onClick={() => void refreshAll()} disabledReason={refreshReason}><RefreshCw size={15} /></Button>
|
||||||
<ReasonedButton type="button" title="Add address book" aria-label="Add address book" variant="primary" onClick={openCreateBookDialog} disabledReason={createBookReason}><Plus size={15} /></ReasonedButton>
|
<Button type="button" title="Add address book" aria-label="Add address book" variant="primary" onClick={openCreateBookDialog} disabledReason={createBookReason}><Plus size={15} /></Button>
|
||||||
<ReasonedButton type="button" title="Add address list" aria-label="Add address list" onClick={openCreateListDialog} disabledReason={createListReason}><Plus size={15} /></ReasonedButton>
|
<Button type="button" title="Add address list" aria-label="Add address list" onClick={openCreateListDialog} disabledReason={createListReason}><Plus size={15} /></Button>
|
||||||
<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>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
<ReasonedButton type="button" title="Connect CardDAV" aria-label="Connect CardDAV" onClick={openCardDavDialog} disabledReason={connectCardDavReason}><Link2 size={15} /></ReasonedButton>
|
<Button type="button" title="Connect CardDAV" aria-label="Connect CardDAV" onClick={openCardDavDialog} disabledReason={connectCardDavReason}><Link2 size={15} /></Button>
|
||||||
<ReasonedButton type="button" title="Inspect sync source" aria-label="Inspect sync source" onClick={() => selectedSyncSource && void openSyncInspector(selectedSyncSource)} disabledReason={inspectSyncReason}><Search size={15} /></ReasonedButton>
|
<Button type="button" title="Inspect sync source" aria-label="Inspect sync source" onClick={() => selectedSyncSource && void openSyncInspector(selectedSyncSource)} disabledReason={inspectSyncReason}><Search size={15} /></Button>
|
||||||
<ReasonedButton type="button" title="Preview sync" aria-label="Preview sync" onClick={() => void previewSelectedSync()} disabledReason={previewSyncReason}>Preview</ReasonedButton>
|
<Button type="button" title="Preview sync" aria-label="Preview sync" onClick={() => void previewSelectedSync()} disabledReason={previewSyncReason}>Preview</Button>
|
||||||
<ReasonedButton type="button" title="Run sync" aria-label="Run sync" onClick={() => void runSelectedSync()} disabledReason={runSyncReason}>Sync</ReasonedButton>
|
<Button type="button" title="Run sync" aria-label="Run sync" onClick={() => void runSelectedSync()} disabledReason={runSyncReason}>Sync</Button>
|
||||||
{selectedList ?
|
{selectedList ?
|
||||||
selectedList.deleted_at ?
|
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>
|
<Button type="button" title="Edit address list" aria-label="Edit address list" disabledReason={editListReason(selectedList)} onClick={() => openEditListDialog(selectedList)}><Edit3 size={15} /></Button>
|
||||||
<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" 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 ?
|
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>
|
<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>
|
||||||
<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" 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>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
{selectedContact.deleted_at ?
|
{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>
|
<Button type="button" title="Export contact vCard" aria-label="Export contact vCard" disabledReason={exportContactReason()} onClick={() => void exportOneContact(selectedContact)}><Download size={15} /> vCard</Button>
|
||||||
<ReasonedButton type="button" title="Edit contact" aria-label="Edit contact" disabledReason={editContactReason()} onClick={() => openEditContactDialog(selectedContact)}><Edit3 size={15} /> Edit</ReasonedButton>
|
<Button type="button" title="Edit contact" aria-label="Edit contact" disabledReason={editContactReason()} onClick={() => openEditContactDialog(selectedContact)}><Edit3 size={15} /> Edit</Button>
|
||||||
<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" variant="danger" title="Delete contact" aria-label="Delete contact" disabledReason={deleteContactReason()} onClick={() => setConfirmState({ kind: "contact", contact: selectedContact })}><Trash2 size={15} /> Delete</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -1862,13 +1848,13 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
{selectedContactListEntries.map((entry) => (
|
{selectedContactListEntries.map((entry) => (
|
||||||
<div className="address-membership-row" key={entry.id}>
|
<div className="address-membership-row" key={entry.id}>
|
||||||
<p className="muted">{entryTargetLabel(entry)}</p>
|
<p className="muted">{entryTargetLabel(entry)}</p>
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => void removeAddressListEntry(entry)}
|
onClick={() => void removeAddressListEntry(entry)}
|
||||||
disabledReason={removeContactFromListReason()}>
|
disabledReason={removeContactFromListReason()}>
|
||||||
<X size={15} /> Remove
|
<X size={15} /> Remove
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1985,9 +1971,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
{selectedList &&
|
{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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="address-contact-toolbar">
|
<div className="address-contact-toolbar">
|
||||||
@@ -2017,8 +2003,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
footerClassName="button-row compact-actions"
|
footerClassName="button-row compact-actions"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<ReasonedButton type="button" onClick={() => setBookDialog(null)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
|
<Button type="button" onClick={() => setBookDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
|
||||||
<ReasonedButton type="submit" form="address-book-form" variant="primary" disabledReason={bookSaveReason}><Save size={16} /> Save</ReasonedButton>
|
<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)}>
|
<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"
|
footerClassName="button-row compact-actions"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<ReasonedButton type="button" onClick={() => setListDialog(null)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
|
<Button type="button" onClick={() => setListDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
|
||||||
<ReasonedButton type="submit" form="address-list-form" variant="primary" disabledReason={listSaveReason}><Save size={16} /> Save</ReasonedButton>
|
<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)}>
|
<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"
|
footerClassName="button-row compact-actions"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<ReasonedButton type="button" onClick={() => setContactDialog(null)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
|
<Button type="button" onClick={() => setContactDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
|
||||||
<ReasonedButton type="submit" form="address-contact-form" variant="primary" disabledReason={contactSaveReason}><Save size={16} /> Save</ReasonedButton>
|
<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)}>
|
<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">
|
<section className="address-form-section">
|
||||||
<div className="address-form-section-heading">
|
<div className="address-form-section-heading">
|
||||||
<strong>Email addresses</strong>
|
<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>
|
||||||
<div className="address-form-list">
|
<div className="address-form-list">
|
||||||
{contactForm.emails.map((email) => (
|
{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>
|
<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 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 })} />
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2114,7 +2100,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<section className="address-form-section">
|
<section className="address-form-section">
|
||||||
<div className="address-form-section-heading">
|
<div className="address-form-section-heading">
|
||||||
<strong>Phone numbers</strong>
|
<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>
|
||||||
<div className="address-form-list">
|
<div className="address-form-list">
|
||||||
{contactForm.phones.map((phone) => (
|
{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>
|
<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.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 })} />
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2131,7 +2117,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<section className="address-form-section">
|
<section className="address-form-section">
|
||||||
<div className="address-form-section-heading">
|
<div className="address-form-section-heading">
|
||||||
<strong>Postal addresses</strong>
|
<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>
|
||||||
<div className="address-form-list">
|
<div className="address-form-list">
|
||||||
{contactForm.postal_addresses.map((address) => (
|
{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.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.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 })} />
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2160,7 +2146,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
closeDisabled={saving}
|
closeDisabled={saving}
|
||||||
className="address-member-dialog"
|
className="address-member-dialog"
|
||||||
footerClassName="button-row compact-actions"
|
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-member-picker">
|
||||||
<div className="address-contact-toolbar address-member-search">
|
<div className="address-contact-toolbar address-member-search">
|
||||||
<input value={memberQuery} onChange={(event) => setMemberQuery(event.target.value)} placeholder="Search contacts to add" autoFocus />
|
<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}>
|
disabled={saving || targetOptions.length === 0}>
|
||||||
{targetOptions.map((option) => <option key={option.key} value={option.key}>{option.label}</option>)}
|
{targetOptions.map((option) => <option key={option.key} value={option.key}>{option.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectedList && void addContactToAddressList(selectedList, contact, selectedTarget)}
|
onClick={() => selectedList && void addContactToAddressList(selectedList, contact, selectedTarget)}
|
||||||
disabledReason={addContactToListReason(selectedList, contact, selectedTarget?.key ?? "")}>
|
disabledReason={addContactToListReason(selectedList, contact, selectedTarget?.key ?? "")}>
|
||||||
<Plus size={15} /> Add
|
<Plus size={15} /> Add
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -2207,8 +2193,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
footerClassName="button-row compact-actions"
|
footerClassName="button-row compact-actions"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<ReasonedButton type="button" onClick={() => setImportOpen(false)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
|
<Button type="button" onClick={() => setImportOpen(false)} disabledReason={dialogCancelReason}>Cancel</Button>
|
||||||
<ReasonedButton type="submit" form="address-vcard-import-form" variant="primary" disabledReason={vcardImportReason}><Upload size={16} /> Import</ReasonedButton>
|
<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)}>
|
<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"
|
footerClassName="button-row compact-actions"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<ReasonedButton type="button" onClick={() => setCardDavOpen(false)} disabledReason={dialogCancelReason}>Cancel</ReasonedButton>
|
<Button type="button" onClick={() => setCardDavOpen(false)} disabledReason={dialogCancelReason}>Cancel</Button>
|
||||||
<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>
|
<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>
|
||||||
<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="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)}>
|
<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"
|
footerClassName="button-row compact-actions"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<ReasonedButton type="button" onClick={() => setSyncInspector(null)} disabledReason={dialogCancelReason}>Close</ReasonedButton>
|
<Button type="button" onClick={() => setSyncInspector(null)} disabledReason={dialogCancelReason}>Close</Button>
|
||||||
<ReasonedButton type="button" onClick={() => syncInspector && void previewSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [saving, savingReason])}>Preview</ReasonedButton>
|
<Button type="button" onClick={() => syncInspector && void previewSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [saving, savingReason])}>Preview</Button>
|
||||||
<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" 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 &&
|
{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>
|
<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>}
|
{syncInspector.source.last_error && <DismissibleAlert tone="danger" resetKey={syncInspector.source.last_error}>{syncInspector.source.last_error}</DismissibleAlert>}
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void toggleSyncSourceEnabled(syncInspector.source)}
|
onClick={() => void toggleSyncSourceEnabled(syncInspector.source)}
|
||||||
disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
|
disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
|
||||||
{syncInspector.source.enabled ? "Disable" : "Enable"}
|
{syncInspector.source.enabled ? "Disable" : "Enable"}
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => setConfirmState({ kind: "sync-source", source: syncInspector.source })}
|
onClick={() => setConfirmState({ kind: "sync-source", source: syncInspector.source })}
|
||||||
disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
|
disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
|
||||||
Disconnect
|
Disconnect
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2361,7 +2347,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
{syncConflicts.map((conflict) => (
|
{syncConflicts.map((conflict) => (
|
||||||
<div className="address-sync-record-row" key={conflict.id}>
|
<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>
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2405,20 +2391,20 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
className="address-sync-dialog"
|
className="address-sync-dialog"
|
||||||
footerClassName="button-row compact-actions"
|
footerClassName="button-row compact-actions"
|
||||||
footer={conflictDialog && <>
|
footer={conflictDialog && <>
|
||||||
<ReasonedButton type="button" onClick={() => { setConflictDialog(null); setConflictMergeChoices({}); }} disabledReason={dialogCancelReason}>Close</ReasonedButton>
|
<Button type="button" onClick={() => { setConflictDialog(null); setConflictMergeChoices({}); }} disabledReason={dialogCancelReason}>Close</Button>
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void resolveConflictWith(conflictDialog, "ignored", "ignored")}
|
onClick={() => void resolveConflictWith(conflictDialog, "ignored", "ignored")}
|
||||||
disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
|
disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
|
||||||
Ignore
|
Ignore
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void resolveConflictWith(conflictDialog, "keep_local")}
|
onClick={() => void resolveConflictWith(conflictDialog, "keep_local")}
|
||||||
disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
|
disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
|
||||||
Keep local
|
Keep local
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void applyMergedConflict(conflictDialog)}
|
onClick={() => void applyMergedConflict(conflictDialog)}
|
||||||
disabledReason={disabledReason(
|
disabledReason={disabledReason(
|
||||||
@@ -2427,8 +2413,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
[saving, savingReason]
|
[saving, savingReason]
|
||||||
)}>
|
)}>
|
||||||
Apply merged
|
Apply merged
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
<ReasonedButton
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void resolveConflictWith(conflictDialog, "use_remote")}
|
onClick={() => void resolveConflictWith(conflictDialog, "use_remote")}
|
||||||
@@ -2438,7 +2424,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
[saving, savingReason]
|
[saving, savingReason]
|
||||||
)}>
|
)}>
|
||||||
Use remote
|
Use remote
|
||||||
</ReasonedButton>
|
</Button>
|
||||||
</>}>
|
</>}>
|
||||||
{conflictDialog &&
|
{conflictDialog &&
|
||||||
<div className="address-conflict-review">
|
<div className="address-conflict-review">
|
||||||
|
|||||||
Reference in New Issue
Block a user