Adopt shared WebUI layout primitives
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Download, Edit3, GitMerge, History, Link2, Network, Plus, RefreshCw, RotateCcw, Save, Search, ShieldCheck, Trash2, Upload, UserPlus, X } from "lucide-react";
|
import { Download, Edit3, GitMerge, History, Link2, Network, Plus, RefreshCw, RotateCcw, Save, Search, ShieldCheck, Trash2, Upload, UserPlus, X } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState, type DragEvent as ReactDragEvent, type FormEvent } from "react";
|
import { useCallback, useEffect, useMemo, useState, type DragEvent as ReactDragEvent, type FormEvent } from "react";
|
||||||
import {
|
import { DialogSection, DialogForm, FormGrid, ActionToolbar,
|
||||||
ApiError,
|
ApiError,
|
||||||
ActionBlockerHint,
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
fetchAuthGroups,
|
fetchAuthGroups,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
FormField,
|
FormField,
|
||||||
|
FormSection,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
PasswordField,
|
PasswordField,
|
||||||
@@ -3059,7 +3060,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="button" variant="primary" onClick={openCreateContactDialog} disabledReason={createContactReason}><Plus size={16} /> Contact</Button>
|
<Button type="button" variant="primary" onClick={openCreateContactDialog} disabledReason={createContactReason}><Plus size={16} /> Contact</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="address-contact-toolbar">
|
<ActionToolbar className="address-contact-toolbar">
|
||||||
<input
|
<input
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -3069,7 +3070,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
placeholder="Search contacts" />
|
placeholder="Search contacts" />
|
||||||
{selectedBook?.read_only && <StatusBadge status="read-only" />}
|
{selectedBook?.read_only && <StatusBadge status="read-only" />}
|
||||||
{selectedBook?.deleted_at && <StatusBadge status="archived" />}
|
{selectedBook?.deleted_at && <StatusBadge status="archived" />}
|
||||||
</div>
|
</ActionToolbar>
|
||||||
<div className="address-contact-list">
|
<div className="address-contact-list">
|
||||||
{visibleContacts.length === 0 ?
|
{visibleContacts.length === 0 ?
|
||||||
<div className="address-empty-note">{selectedBook ? "No contacts found." : "Select an address book."}</div> :
|
<div className="address-empty-note">{selectedBook ? "No contacts found." : "Select an address book."}</div> :
|
||||||
@@ -3111,8 +3112,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="submit" form="address-book-form" variant="primary" disabledReason={bookSaveReason}><Save size={16} /> Save</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)}>
|
<DialogForm id="address-book-form" className="address-dialog-form" onSubmit={(event) => void submitBook(event)}>
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Scope" documentation={ADDRESS_FIELDS_DOCUMENTATION}>
|
<FormField label="Scope" documentation={ADDRESS_FIELDS_DOCUMENTATION}>
|
||||||
<select value={bookForm.scope_type} disabled={bookDialog?.mode === "edit"} onChange={(event) => setBookForm((current) => ({ ...current, scope_type: event.target.value as AddressBookScope }))}>
|
<select value={bookForm.scope_type} disabled={bookDialog?.mode === "edit"} onChange={(event) => setBookForm((current) => ({ ...current, scope_type: event.target.value as AddressBookScope }))}>
|
||||||
<option value="user">Personal</option>
|
<option value="user">Personal</option>
|
||||||
@@ -3129,10 +3130,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
}
|
}
|
||||||
</div>
|
</FormGrid>
|
||||||
<FormField label="Name" documentation={ADDRESS_FIELDS_DOCUMENTATION}><input value={bookForm.name} onChange={(event) => setBookForm((current) => ({ ...current, name: event.target.value }))} autoFocus /></FormField>
|
<FormField label="Name" documentation={ADDRESS_FIELDS_DOCUMENTATION}><input value={bookForm.name} onChange={(event) => setBookForm((current) => ({ ...current, name: event.target.value }))} autoFocus /></FormField>
|
||||||
<FormField label="Description"><textarea value={bookForm.description} onChange={(event) => setBookForm((current) => ({ ...current, description: event.target.value }))} rows={3} /></FormField>
|
<FormField label="Description"><textarea value={bookForm.description} onChange={(event) => setBookForm((current) => ({ ...current, description: event.target.value }))} rows={3} /></FormField>
|
||||||
</form>
|
</DialogForm>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3147,7 +3148,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="submit" form="address-list-form" variant="primary" disabledReason={listSaveReason}><Save size={16} /> Save</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)}>
|
<DialogForm id="address-list-form" className="address-dialog-form" onSubmit={(event) => void submitList(event)}>
|
||||||
<p className="muted">Lists group contacts inside the selected address book.</p>
|
<p className="muted">Lists group contacts inside the selected address book.</p>
|
||||||
<FormField label="Address book" documentation={ADDRESS_FIELDS_DOCUMENTATION}>
|
<FormField label="Address book" documentation={ADDRESS_FIELDS_DOCUMENTATION}>
|
||||||
<input value={selectedBook?.name ?? ""} disabled readOnly />
|
<input value={selectedBook?.name ?? ""} disabled readOnly />
|
||||||
@@ -3158,7 +3159,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<FormField label="Description">
|
<FormField label="Description">
|
||||||
<textarea value={listForm.description} onChange={(event) => setListForm((current) => ({ ...current, description: event.target.value }))} rows={3} />
|
<textarea value={listForm.description} onChange={(event) => setListForm((current) => ({ ...current, description: event.target.value }))} rows={3} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</form>
|
</DialogForm>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3174,22 +3175,21 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="submit" form="address-contact-form" variant="primary" disabledReason={contactSaveReason}><Save size={16} /> Save</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)}>
|
<DialogForm id="address-contact-form" className="address-dialog-form" onSubmit={(event) => void submitContact(event)}>
|
||||||
<div className="form-grid three">
|
<FormGrid columns={3} collapseAt="standard" className="">
|
||||||
<FormField label="Display name" documentation={ADDRESS_FIELDS_DOCUMENTATION}><input value={contactForm.display_name} onChange={(event) => setContactForm((current) => ({ ...current, display_name: event.target.value }))} autoFocus /></FormField>
|
<FormField label="Display name" documentation={ADDRESS_FIELDS_DOCUMENTATION}><input value={contactForm.display_name} onChange={(event) => setContactForm((current) => ({ ...current, display_name: event.target.value }))} autoFocus /></FormField>
|
||||||
<FormField label="Given name"><input value={contactForm.given_name} onChange={(event) => setContactForm((current) => ({ ...current, given_name: event.target.value }))} /></FormField>
|
<FormField label="Given name"><input value={contactForm.given_name} onChange={(event) => setContactForm((current) => ({ ...current, given_name: event.target.value }))} /></FormField>
|
||||||
<FormField label="Family name"><input value={contactForm.family_name} onChange={(event) => setContactForm((current) => ({ ...current, family_name: event.target.value }))} /></FormField>
|
<FormField label="Family name"><input value={contactForm.family_name} onChange={(event) => setContactForm((current) => ({ ...current, family_name: event.target.value }))} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Organization" documentation={ADDRESS_FIELDS_DOCUMENTATION}><input value={contactForm.organization} onChange={(event) => setContactForm((current) => ({ ...current, organization: event.target.value }))} /></FormField>
|
<FormField label="Organization" documentation={ADDRESS_FIELDS_DOCUMENTATION}><input value={contactForm.organization} onChange={(event) => setContactForm((current) => ({ ...current, organization: event.target.value }))} /></FormField>
|
||||||
<FormField label="Role title"><input value={contactForm.role_title} onChange={(event) => setContactForm((current) => ({ ...current, role_title: event.target.value }))} /></FormField>
|
<FormField label="Role title"><input value={contactForm.role_title} onChange={(event) => setContactForm((current) => ({ ...current, role_title: event.target.value }))} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<section className="address-form-section">
|
<FormSection
|
||||||
<div className="address-form-section-heading">
|
className="address-form-section"
|
||||||
<strong>Email addresses</strong>
|
title="Email addresses"
|
||||||
<Button type="button" onClick={addEmailRow} disabledReason={addContactRowReason}><Plus size={15} /> Email</Button>
|
actions={<Button type="button" onClick={addEmailRow} disabledReason={addContactRowReason}><Plus size={15} /> Email</Button>}
|
||||||
</div>
|
contentClassName="address-form-list">
|
||||||
<div className="address-form-list">
|
|
||||||
{contactForm.emails.map((email) => (
|
{contactForm.emails.map((email) => (
|
||||||
<div className="address-form-row address-form-row-email" key={email.rowId}>
|
<div className="address-form-row address-form-row-email" key={email.rowId}>
|
||||||
<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>
|
||||||
@@ -3198,15 +3198,13 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="button" variant="danger" title="Remove email" aria-label="Remove email" disabledReason={removeEmailRowReason} onClick={() => removeEmailRow(email.rowId)}><Trash2 size={15} /></Button>
|
<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>
|
</FormSection>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="address-form-section">
|
<FormSection
|
||||||
<div className="address-form-section-heading">
|
className="address-form-section"
|
||||||
<strong>Phone numbers</strong>
|
title="Phone numbers"
|
||||||
<Button type="button" onClick={addPhoneRow} disabledReason={addContactRowReason}><Plus size={15} /> Phone</Button>
|
actions={<Button type="button" onClick={addPhoneRow} disabledReason={addContactRowReason}><Plus size={15} /> Phone</Button>}
|
||||||
</div>
|
contentClassName="address-form-list">
|
||||||
<div className="address-form-list">
|
|
||||||
{contactForm.phones.map((phone) => (
|
{contactForm.phones.map((phone) => (
|
||||||
<div className="address-form-row address-form-row-phone" key={phone.rowId}>
|
<div className="address-form-row address-form-row-phone" key={phone.rowId}>
|
||||||
<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>
|
||||||
@@ -3215,15 +3213,13 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="button" variant="danger" title="Remove phone" aria-label="Remove phone" disabledReason={removePhoneRowReason} onClick={() => removePhoneRow(phone.rowId)}><Trash2 size={15} /></Button>
|
<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>
|
</FormSection>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="address-form-section">
|
<FormSection
|
||||||
<div className="address-form-section-heading">
|
className="address-form-section"
|
||||||
<strong>Postal addresses</strong>
|
title="Postal addresses"
|
||||||
<Button type="button" onClick={addPostalAddressRow} disabledReason={addContactRowReason}><Plus size={15} /> Address</Button>
|
actions={<Button type="button" onClick={addPostalAddressRow} disabledReason={addContactRowReason}><Plus size={15} /> Address</Button>}
|
||||||
</div>
|
contentClassName="address-form-list">
|
||||||
<div className="address-form-list">
|
|
||||||
{contactForm.postal_addresses.map((address) => (
|
{contactForm.postal_addresses.map((address) => (
|
||||||
<div className="address-form-row address-form-row-postal" key={address.rowId}>
|
<div className="address-form-row address-form-row-postal" key={address.rowId}>
|
||||||
<label className="address-primary-choice"><input type="radio" name="address-primary-postal" checked={address.is_primary} onChange={() => setPrimaryPostalAddressRow(address.rowId)} /> Primary</label>
|
<label className="address-primary-choice"><input type="radio" name="address-primary-postal" checked={address.is_primary} onChange={() => setPrimaryPostalAddressRow(address.rowId)} /> Primary</label>
|
||||||
@@ -3236,11 +3232,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="button" variant="danger" title="Remove postal address" aria-label="Remove postal address" disabledReason={removePostalAddressRowReason} onClick={() => removePostalAddressRow(address.rowId)}><Trash2 size={15} /></Button>
|
<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>
|
</FormSection>
|
||||||
</section>
|
|
||||||
<FormField label="Tags"><input value={contactForm.tags} placeholder="Comma-separated tags" onChange={(event) => setContactForm((current) => ({ ...current, tags: event.target.value }))} /></FormField>
|
<FormField label="Tags"><input value={contactForm.tags} placeholder="Comma-separated tags" onChange={(event) => setContactForm((current) => ({ ...current, tags: event.target.value }))} /></FormField>
|
||||||
<FormField label="Note"><textarea rows={3} value={contactForm.note} onChange={(event) => setContactForm((current) => ({ ...current, note: event.target.value }))} /></FormField>
|
<FormField label="Note"><textarea rows={3} value={contactForm.note} onChange={(event) => setContactForm((current) => ({ ...current, note: event.target.value }))} /></FormField>
|
||||||
</form>
|
</DialogForm>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3299,7 +3294,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{canWriteGovernance &&
|
{canWriteGovernance &&
|
||||||
<form className="address-dialog-form address-form-section" onSubmit={(event) => void submitChannelRule(event)}>
|
<DialogForm className="address-dialog-form address-form-section" onSubmit={(event) => void submitChannelRule(event)}>
|
||||||
<div className="address-form-section-heading">
|
<div className="address-form-section-heading">
|
||||||
<div>
|
<div>
|
||||||
<strong>Record a fact</strong>
|
<strong>Record a fact</strong>
|
||||||
@@ -3307,7 +3302,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="primary" disabledReason={channelRuleSaveReason}><Plus size={15} /> Add</Button>
|
<Button type="submit" variant="primary" disabledReason={channelRuleSaveReason}><Plus size={15} /> Add</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Channel" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}>
|
<FormField label="Channel" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={channelRuleForm.channel}
|
value={channelRuleForm.channel}
|
||||||
@@ -3331,8 +3326,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
{governancePointOptions.map((item) => <option value={item.id} key={item.id}>{item.label}</option>)}
|
{governancePointOptions.map((item) => <option value={item.id} key={item.id}>{item.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Decision" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}>
|
<FormField label="Decision" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}>
|
||||||
<select value={channelRuleForm.decision} onChange={(event) => setChannelRuleForm((current) => ({ ...current, decision: event.target.value as AddressChannelDecision }))}>
|
<select value={channelRuleForm.decision} onChange={(event) => setChannelRuleForm((current) => ({ ...current, decision: event.target.value as AddressChannelDecision }))}>
|
||||||
<option value="allowed">Allowed</option>
|
<option value="allowed">Allowed</option>
|
||||||
@@ -3348,19 +3343,19 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<FormField label="Communication purpose" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}>
|
<FormField label="Communication purpose" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}>
|
||||||
<input value={channelRuleForm.purpose} placeholder="All purposes" onChange={(event) => setChannelRuleForm((current) => ({ ...current, purpose: event.target.value }))} />
|
<input value={channelRuleForm.purpose} placeholder="All purposes" onChange={(event) => setChannelRuleForm((current) => ({ ...current, purpose: event.target.value }))} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<div className="form-grid three">
|
<FormGrid columns={3} collapseAt="standard" className="">
|
||||||
<FormField label="Legal basis"><input value={channelRuleForm.legal_basis} onChange={(event) => setChannelRuleForm((current) => ({ ...current, legal_basis: event.target.value }))} /></FormField>
|
<FormField label="Legal basis"><input value={channelRuleForm.legal_basis} onChange={(event) => setChannelRuleForm((current) => ({ ...current, legal_basis: event.target.value }))} /></FormField>
|
||||||
<FormField label="Evidence reference"><input value={channelRuleForm.evidence_ref} onChange={(event) => setChannelRuleForm((current) => ({ ...current, evidence_ref: event.target.value }))} /></FormField>
|
<FormField label="Evidence reference"><input value={channelRuleForm.evidence_ref} onChange={(event) => setChannelRuleForm((current) => ({ ...current, evidence_ref: event.target.value }))} /></FormField>
|
||||||
<FormField label="Locale"><input value={channelRuleForm.locale} placeholder="de-DE" onChange={(event) => setChannelRuleForm((current) => ({ ...current, locale: event.target.value }))} /></FormField>
|
<FormField label="Locale"><input value={channelRuleForm.locale} placeholder="de-DE" onChange={(event) => setChannelRuleForm((current) => ({ ...current, locale: event.target.value }))} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<div className="form-grid three">
|
<FormGrid columns={3} collapseAt="standard" className="">
|
||||||
<FormField label="Effective from"><input type="datetime-local" value={channelRuleForm.effective_from} onChange={(event) => setChannelRuleForm((current) => ({ ...current, effective_from: event.target.value }))} /></FormField>
|
<FormField label="Effective from"><input type="datetime-local" value={channelRuleForm.effective_from} onChange={(event) => setChannelRuleForm((current) => ({ ...current, effective_from: event.target.value }))} /></FormField>
|
||||||
<FormField label="Effective until"><input type="datetime-local" value={channelRuleForm.effective_until} onChange={(event) => setChannelRuleForm((current) => ({ ...current, effective_until: event.target.value }))} /></FormField>
|
<FormField label="Effective until"><input type="datetime-local" value={channelRuleForm.effective_until} onChange={(event) => setChannelRuleForm((current) => ({ ...current, effective_until: event.target.value }))} /></FormField>
|
||||||
<FormField label="Preference rank"><input type="number" min="0" max="10000" value={channelRuleForm.preference_rank} onChange={(event) => setChannelRuleForm((current) => ({ ...current, preference_rank: event.target.value }))} /></FormField>
|
<FormField label="Preference rank"><input type="number" min="0" max="10000" value={channelRuleForm.preference_rank} onChange={(event) => setChannelRuleForm((current) => ({ ...current, preference_rank: event.target.value }))} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<FormField label="Reason" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}><textarea rows={2} value={channelRuleForm.reason} onChange={(event) => setChannelRuleForm((current) => ({ ...current, reason: event.target.value }))} /></FormField>
|
<FormField label="Reason" documentation={ADDRESS_GOVERNANCE_DOCUMENTATION}><textarea rows={2} value={channelRuleForm.reason} onChange={(event) => setChannelRuleForm((current) => ({ ...current, reason: event.target.value }))} /></FormField>
|
||||||
</form>
|
</DialogForm>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -3531,9 +3526,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="submit" form="address-quality-point-form" variant="primary" disabledReason={disabledReason([!qualityPointTarget, "Select a contact point."], [!canWriteGovernance, "You need permission to manage address quality."], [saving, savingReason])}><Save size={15} /> Record</Button>
|
<Button type="submit" form="address-quality-point-form" variant="primary" disabledReason={disabledReason([!qualityPointTarget, "Select a contact point."], [!canWriteGovernance, "You need permission to manage address quality."], [saving, savingReason])}><Save size={15} /> Record</Button>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<form id="address-quality-point-form" className="address-dialog-form" onSubmit={(event) => void submitQualityDecision(event)}>
|
<DialogForm id="address-quality-point-form" className="address-dialog-form" onSubmit={(event) => void submitQualityDecision(event)}>
|
||||||
<FormField label="Contact point"><input value={qualityPointTarget?.label ?? ""} readOnly disabled /></FormField>
|
<FormField label="Contact point"><input value={qualityPointTarget?.label ?? ""} readOnly disabled /></FormField>
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Quality state">
|
<FormField label="Quality state">
|
||||||
<select value={qualityForm.state} onChange={(event) => setQualityForm((current) => ({ ...current, state: event.target.value as ContactPointQualityState }))}>
|
<select value={qualityForm.state} onChange={(event) => setQualityForm((current) => ({ ...current, state: event.target.value as ContactPointQualityState }))}>
|
||||||
<option value="valid">Valid</option>
|
<option value="valid">Valid</option>
|
||||||
@@ -3544,10 +3539,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Reason code"><input value={qualityForm.reason_code} placeholder={`addresses.quality.${qualityForm.state}`} onChange={(event) => setQualityForm((current) => ({ ...current, reason_code: event.target.value }))} /></FormField>
|
<FormField label="Reason code"><input value={qualityForm.reason_code} placeholder={`addresses.quality.${qualityForm.state}`} onChange={(event) => setQualityForm((current) => ({ ...current, reason_code: event.target.value }))} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<FormField label="Reason"><textarea rows={3} value={qualityForm.reason} onChange={(event) => setQualityForm((current) => ({ ...current, reason: event.target.value }))} /></FormField>
|
<FormField label="Reason"><textarea rows={3} value={qualityForm.reason} onChange={(event) => setQualityForm((current) => ({ ...current, reason: event.target.value }))} /></FormField>
|
||||||
<FormField label="Evidence reference"><input value={qualityForm.evidence_ref} placeholder="mail:delivery:..." onChange={(event) => setQualityForm((current) => ({ ...current, evidence_ref: event.target.value }))} /></FormField>
|
<FormField label="Evidence reference"><input value={qualityForm.evidence_ref} placeholder="mail:delivery:..." onChange={(event) => setQualityForm((current) => ({ ...current, evidence_ref: event.target.value }))} /></FormField>
|
||||||
</form>
|
</DialogForm>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3562,7 +3557,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="submit" form="address-merge-form" variant="primary" disabledReason={disabledReason([!mergeDialog || mergeDialog.reason.trim().length < 3, "Record why these contacts are being merged."], [saving, savingReason])}><GitMerge size={15} /> Merge</Button>
|
<Button type="submit" form="address-merge-form" variant="primary" disabledReason={disabledReason([!mergeDialog || mergeDialog.reason.trim().length < 3, "Record why these contacts are being merged."], [saving, savingReason])}><GitMerge size={15} /> Merge</Button>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
{mergeDialog && <form id="address-merge-form" className="address-dialog-form" onSubmit={(event) => void submitContactMerge(event)}>
|
{mergeDialog && <DialogForm id="address-merge-form" className="address-dialog-form" onSubmit={(event) => void submitContactMerge(event)}>
|
||||||
<DismissibleAlert tone="warning" dismissible={false}>The other contact is archived and redirected to the survivor. Address-list memberships and matching contact points are repaired transactionally.</DismissibleAlert>
|
<DismissibleAlert tone="warning" dismissible={false}>The other contact is archived and redirected to the survivor. Address-list memberships and matching contact points are repaired transactionally.</DismissibleAlert>
|
||||||
<FormField label="Surviving contact">
|
<FormField label="Surviving contact">
|
||||||
<select value={mergeDialog.winnerId} onChange={(event) => changeMergeWinner(event.target.value)}>
|
<select value={mergeDialog.winnerId} onChange={(event) => changeMergeWinner(event.target.value)}>
|
||||||
@@ -3570,7 +3565,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<option value={mergeDialog.suggestion.right.id}>{mergeDialog.suggestion.right.display_name} · {primaryEmail(mergeDialog.suggestion.right) || "no email"}</option>
|
<option value={mergeDialog.suggestion.right.id}>{mergeDialog.suggestion.right.display_name} · {primaryEmail(mergeDialog.suggestion.right) || "no email"}</option>
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="form-grid two address-merge-field-sources">
|
<FormGrid columns={2} collapseAt="standard" className="address-merge-field-sources">
|
||||||
{MERGE_SCALAR_FIELDS.map((field) => (
|
{MERGE_SCALAR_FIELDS.map((field) => (
|
||||||
<FormField label={`${field.label} source`} key={field.id}>
|
<FormField label={`${field.label} source`} key={field.id}>
|
||||||
<select
|
<select
|
||||||
@@ -3584,7 +3579,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
))}
|
))}
|
||||||
</div>
|
</FormGrid>
|
||||||
<FormField label="Contact points">
|
<FormField label="Contact points">
|
||||||
<SegmentedControl<"union" | "winner_only">
|
<SegmentedControl<"union" | "winner_only">
|
||||||
role="group"
|
role="group"
|
||||||
@@ -3596,7 +3591,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Reason"><textarea rows={3} value={mergeDialog.reason} onChange={(event) => setMergeDialog((current) => current ? { ...current, reason: event.target.value } : null)} /></FormField>
|
<FormField label="Reason"><textarea rows={3} value={mergeDialog.reason} onChange={(event) => setMergeDialog((current) => current ? { ...current, reason: event.target.value } : null)} /></FormField>
|
||||||
</form>}
|
</DialogForm>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3611,10 +3606,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="submit" form="address-merge-recovery-form" variant="primary" disabledReason={disabledReason([!mergeRecoveryDialog || mergeRecoveryDialog.reason.trim().length < 3, "Record why the merge is being recovered."], [saving, savingReason])}><RotateCcw size={15} /> {mergeRecoveryDialog?.action === "split" ? "Split" : "Undo"}</Button>
|
<Button type="submit" form="address-merge-recovery-form" variant="primary" disabledReason={disabledReason([!mergeRecoveryDialog || mergeRecoveryDialog.reason.trim().length < 3, "Record why the merge is being recovered."], [saving, savingReason])}><RotateCcw size={15} /> {mergeRecoveryDialog?.action === "split" ? "Split" : "Undo"}</Button>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
{mergeRecoveryDialog && <form id="address-merge-recovery-form" className="address-dialog-form" onSubmit={(event) => void submitMergeRecovery(event)}>
|
{mergeRecoveryDialog && <DialogForm id="address-merge-recovery-form" className="address-dialog-form" onSubmit={(event) => void submitMergeRecovery(event)}>
|
||||||
<p className="muted">Recovery restores the recorded pre-merge values and list memberships. It is rejected if either contact changed after the merge.</p>
|
<p className="muted">Recovery restores the recorded pre-merge values and list memberships. It is rejected if either contact changed after the merge.</p>
|
||||||
<FormField label="Reason"><textarea rows={3} value={mergeRecoveryDialog.reason} onChange={(event) => setMergeRecoveryDialog((current) => current ? { ...current, reason: event.target.value } : null)} autoFocus /></FormField>
|
<FormField label="Reason"><textarea rows={3} value={mergeRecoveryDialog.reason} onChange={(event) => setMergeRecoveryDialog((current) => current ? { ...current, reason: event.target.value } : null)} autoFocus /></FormField>
|
||||||
</form>}
|
</DialogForm>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3626,9 +3621,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
footerClassName="button-row compact-actions"
|
footerClassName="button-row compact-actions"
|
||||||
footer={<Button type="button" onClick={() => setMemberDialogOpen(false)} disabledReason={dialogCancelReason}>Close</Button>}>
|
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">
|
<ActionToolbar 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 />
|
||||||
</div>
|
</ActionToolbar>
|
||||||
<div className="address-member-candidate-list" role="list">
|
<div className="address-member-candidate-list" role="list">
|
||||||
{memberCandidateContacts.length === 0 ?
|
{memberCandidateContacts.length === 0 ?
|
||||||
<div className="address-empty-note">No contacts available to add.</div> :
|
<div className="address-empty-note">No contacts available to add.</div> :
|
||||||
@@ -3683,7 +3678,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="button" variant="primary" onClick={() => void applyTabularImport()} disabledReason={tabularApplyReason}><Upload size={16} /> Apply import</Button>}
|
<Button type="button" variant="primary" onClick={() => void applyTabularImport()} disabledReason={tabularApplyReason}><Upload size={16} /> Apply import</Button>}
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<div className="address-dialog-form">
|
<DialogSection className="address-dialog-form">
|
||||||
<SegmentedControl<ImportMode>
|
<SegmentedControl<ImportMode>
|
||||||
role="group"
|
role="group"
|
||||||
size="equal"
|
size="equal"
|
||||||
@@ -3693,7 +3688,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
onChange={(mode) => { setImportMode(mode); setImportRun(null); }}
|
onChange={(mode) => { setImportMode(mode); setImportRun(null); }}
|
||||||
/>
|
/>
|
||||||
{importMode === "vcard" &&
|
{importMode === "vcard" &&
|
||||||
<form id="address-vcard-import-form" className="address-dialog-form" onSubmit={(event) => void submitVcardImport(event)}>
|
<DialogForm id="address-vcard-import-form" className="address-dialog-form" onSubmit={(event) => void submitVcardImport(event)}>
|
||||||
<p className="muted">Paste one or more vCard entries into the selected address book.</p>
|
<p className="muted">Paste one or more vCard entries into the selected address book.</p>
|
||||||
<FormField label="vCard content">
|
<FormField label="vCard content">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -3704,10 +3699,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
placeholder={"BEGIN:VCARD\nVERSION:4.0\nFN:Ada Lovelace\nEMAIL;TYPE=work:ada@example.local\nEND:VCARD"}
|
placeholder={"BEGIN:VCARD\nVERSION:4.0\nFN:Ada Lovelace\nEMAIL;TYPE=work:ada@example.local\nEND:VCARD"}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</form>}
|
</DialogForm>}
|
||||||
{importMode === "tabular" &&
|
{importMode === "tabular" &&
|
||||||
<div className="address-import-workspace">
|
<div className="address-import-workspace">
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Mapping profile">
|
<FormField label="Mapping profile">
|
||||||
<select
|
<select
|
||||||
value={selectedImportProfileId}
|
value={selectedImportProfileId}
|
||||||
@@ -3728,10 +3723,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
{creatingImportProfile ? "Cancel editor" : "New mapping"}
|
{creatingImportProfile ? "Cancel editor" : "New mapping"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormGrid>
|
||||||
{creatingImportProfile ?
|
{creatingImportProfile ?
|
||||||
<div className="address-import-profile-editor">
|
<div className="address-import-profile-editor">
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Profile name"><input value={importProfileForm.name} onChange={(event) => setImportProfileForm((current) => ({ ...current, name: event.target.value }))} /></FormField>
|
<FormField label="Profile name"><input value={importProfileForm.name} onChange={(event) => setImportProfileForm((current) => ({ ...current, name: event.target.value }))} /></FormField>
|
||||||
<FormField label="Format">
|
<FormField label="Format">
|
||||||
<select value={importProfileForm.source_format} disabled={Boolean(editingImportProfileId)} onChange={(event) => setImportProfileForm((current) => ({ ...current, source_format: event.target.value as "csv" | "xlsx" }))}>
|
<select value={importProfileForm.source_format} disabled={Boolean(editingImportProfileId)} onChange={(event) => setImportProfileForm((current) => ({ ...current, source_format: event.target.value as "csv" | "xlsx" }))}>
|
||||||
@@ -3771,7 +3766,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<FormField label="Locale"><input value={importProfileForm.locale} onChange={(event) => setImportProfileForm((current) => ({ ...current, locale: event.target.value }))} placeholder="de-DE" /></FormField>
|
<FormField label="Locale"><input value={importProfileForm.locale} onChange={(event) => setImportProfileForm((current) => ({ ...current, locale: event.target.value }))} placeholder="de-DE" /></FormField>
|
||||||
<FormField label="Default tags"><input value={importProfileForm.default_tags} onChange={(event) => setImportProfileForm((current) => ({ ...current, default_tags: event.target.value }))} placeholder="monthly, imported" /></FormField>
|
<FormField label="Default tags"><input value={importProfileForm.default_tags} onChange={(event) => setImportProfileForm((current) => ({ ...current, default_tags: event.target.value }))} placeholder="monthly, imported" /></FormField>
|
||||||
<FormField label="Maximum rows"><input type="number" min="1" max="10000" value={importProfileForm.max_rows} onChange={(event) => setImportProfileForm((current) => ({ ...current, max_rows: event.target.value }))} /></FormField>
|
<FormField label="Maximum rows"><input type="number" min="1" max="10000" value={importProfileForm.max_rows} onChange={(event) => setImportProfileForm((current) => ({ ...current, max_rows: event.target.value }))} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<div className="address-import-mapping-grid">
|
<div className="address-import-mapping-grid">
|
||||||
{IMPORT_MAPPING_FIELDS.map(([target, label]) =>
|
{IMPORT_MAPPING_FIELDS.map(([target, label]) =>
|
||||||
<FormField label={label} key={target}>
|
<FormField label={label} key={target}>
|
||||||
@@ -3813,7 +3808,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</div>}
|
</div>}
|
||||||
</>}
|
</>}
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</DialogSection>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3830,14 +3825,14 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<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>
|
<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)}>
|
<DialogForm id="address-carddav-form" className="address-dialog-form" onSubmit={(event) => void submitCardDavSource(event)}>
|
||||||
<FormField label="Address book">
|
<FormField label="Address book">
|
||||||
<input value={selectedBook?.name ?? ""} disabled readOnly />
|
<input value={selectedBook?.name ?? ""} disabled readOnly />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="CardDAV URL">
|
<FormField label="CardDAV URL">
|
||||||
<input value={cardDavForm.collection_url} onChange={(event) => setCardDavForm((current) => ({ ...current, collection_url: event.target.value }))} autoFocus />
|
<input value={cardDavForm.collection_url} onChange={(event) => setCardDavForm((current) => ({ ...current, collection_url: event.target.value }))} autoFocus />
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="form-grid three">
|
<FormGrid columns={3} collapseAt="standard" className="">
|
||||||
<FormField label="Authentication">
|
<FormField label="Authentication">
|
||||||
<select value={cardDavForm.auth_type} onChange={(event) => setCardDavForm((current) => ({ ...current, auth_type: event.target.value as CardDavFormState["auth_type"] }))}>
|
<select value={cardDavForm.auth_type} onChange={(event) => setCardDavForm((current) => ({ ...current, auth_type: event.target.value as CardDavFormState["auth_type"] }))}>
|
||||||
<option value="none">None</option>
|
<option value="none">None</option>
|
||||||
@@ -3855,7 +3850,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<FormField label="Display name">
|
<FormField label="Display name">
|
||||||
<input value={cardDavForm.display_name} onChange={(event) => setCardDavForm((current) => ({ ...current, display_name: event.target.value }))} placeholder={selectedBook?.name ?? "CardDAV"} />
|
<input value={cardDavForm.display_name} onChange={(event) => setCardDavForm((current) => ({ ...current, display_name: event.target.value }))} placeholder={selectedBook?.name ?? "CardDAV"} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
{cardDavForm.auth_type !== "none" &&
|
{cardDavForm.auth_type !== "none" &&
|
||||||
<FormField label="Reusable credential">
|
<FormField label="Reusable credential">
|
||||||
<select
|
<select
|
||||||
@@ -3883,7 +3878,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
}
|
}
|
||||||
{cardDavCredentialsError && <DismissibleAlert tone="danger" resetKey={cardDavCredentialsError}>{cardDavCredentialsError}</DismissibleAlert>}
|
{cardDavCredentialsError && <DismissibleAlert tone="danger" resetKey={cardDavCredentialsError}>{cardDavCredentialsError}</DismissibleAlert>}
|
||||||
{cardDavForm.auth_type === "basic" &&
|
{cardDavForm.auth_type === "basic" &&
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Username">
|
<FormField label="Username">
|
||||||
<input value={cardDavForm.username} onChange={(event) => setCardDavForm((current) => ({ ...current, username: event.target.value }))} disabled={Boolean(cardDavForm.credential_envelope_id)} />
|
<input value={cardDavForm.username} onChange={(event) => setCardDavForm((current) => ({ ...current, username: event.target.value }))} disabled={Boolean(cardDavForm.credential_envelope_id)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -3892,7 +3887,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<PasswordField value={cardDavForm.password} onValueChange={(value) => setCardDavForm((current) => ({ ...current, password: value }))} autoComplete="new-password" />
|
<PasswordField value={cardDavForm.password} onValueChange={(value) => setCardDavForm((current) => ({ ...current, password: value }))} autoComplete="new-password" />
|
||||||
</FormField>
|
</FormField>
|
||||||
}
|
}
|
||||||
</div>
|
</FormGrid>
|
||||||
}
|
}
|
||||||
{cardDavForm.auth_type === "bearer" && !cardDavForm.credential_envelope_id &&
|
{cardDavForm.auth_type === "bearer" && !cardDavForm.credential_envelope_id &&
|
||||||
<FormField label="Bearer token">
|
<FormField label="Bearer token">
|
||||||
@@ -3913,7 +3908,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
))}
|
))}
|
||||||
</SelectionList>
|
</SelectionList>
|
||||||
}
|
}
|
||||||
</form>
|
</DialogForm>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -3930,8 +3925,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<Button type="submit" form="address-ldap-form" variant="primary" disabledReason={disabledReason([saving, savingReason], [!ldapForm.url.trim(), "Enter an LDAP URL."], [!ldapForm.base_dn.trim(), "Select or enter a base DN."], [!ldapForm.attribute_map.source_key?.trim(), "Map a stable source-key attribute."])}><Save size={16} /> Connect</Button>
|
<Button type="submit" form="address-ldap-form" variant="primary" disabledReason={disabledReason([saving, savingReason], [!ldapForm.url.trim(), "Enter an LDAP URL."], [!ldapForm.base_dn.trim(), "Select or enter a base DN."], [!ldapForm.attribute_map.source_key?.trim(), "Map a stable source-key attribute."])}><Save size={16} /> Connect</Button>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<form id="address-ldap-form" className="address-dialog-form" onSubmit={(event) => void submitLdapSource(event)}>
|
<DialogForm id="address-ldap-form" className="address-dialog-form" onSubmit={(event) => void submitLdapSource(event)}>
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Directory URL">
|
<FormField label="Directory URL">
|
||||||
<input value={ldapForm.url} onChange={(event) => setLdapForm((current) => ({ ...current, url: event.target.value }))} placeholder="ldaps://directory.example.org" autoFocus />
|
<input value={ldapForm.url} onChange={(event) => setLdapForm((current) => ({ ...current, url: event.target.value }))} placeholder="ldaps://directory.example.org" autoFocus />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -3954,7 +3949,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<FormField label="Bind DN / username">
|
<FormField label="Bind DN / username">
|
||||||
<input value={ldapForm.bind_dn} onChange={(event) => setLdapForm((current) => ({ ...current, bind_dn: event.target.value }))} />
|
<input value={ldapForm.bind_dn} onChange={(event) => setLdapForm((current) => ({ ...current, bind_dn: event.target.value }))} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label="Require StartTLS for ldap://"
|
label="Require StartTLS for ldap://"
|
||||||
checked={ldapForm.start_tls}
|
checked={ldapForm.start_tls}
|
||||||
@@ -3962,7 +3957,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
help="LDAPS always uses TLS. Plain ldap:// endpoints are accepted only when StartTLS is enabled."
|
help="LDAPS always uses TLS. Plain ldap:// endpoints are accepted only when StartTLS is enabled."
|
||||||
/>
|
/>
|
||||||
{cardDavCredentialsError && <DismissibleAlert tone="danger" resetKey={cardDavCredentialsError}>{cardDavCredentialsError}</DismissibleAlert>}
|
{cardDavCredentialsError && <DismissibleAlert tone="danger" resetKey={cardDavCredentialsError}>{cardDavCredentialsError}</DismissibleAlert>}
|
||||||
<div className="form-grid two">
|
<FormGrid columns={2} collapseAt="standard" className="">
|
||||||
<FormField label="Base DN">
|
<FormField label="Base DN">
|
||||||
<input list="address-ldap-base-dns" value={ldapForm.base_dn} onChange={(event) => setLdapForm((current) => ({ ...current, base_dn: event.target.value }))} placeholder="ou=people,dc=example,dc=org" />
|
<input list="address-ldap-base-dns" value={ldapForm.base_dn} onChange={(event) => setLdapForm((current) => ({ ...current, base_dn: event.target.value }))} placeholder="ou=people,dc=example,dc=org" />
|
||||||
<datalist id="address-ldap-base-dns">{ldapBaseDns.map((baseDn) => <option value={baseDn} key={baseDn} />)}</datalist>
|
<datalist id="address-ldap-base-dns">{ldapBaseDns.map((baseDn) => <option value={baseDn} key={baseDn} />)}</datalist>
|
||||||
@@ -3972,7 +3967,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Page size"><input type="number" min="1" max="1000" value={ldapForm.page_size} onChange={(event) => setLdapForm((current) => ({ ...current, page_size: event.target.value }))} /></FormField>
|
<FormField label="Page size"><input type="number" min="1" max="1000" value={ldapForm.page_size} onChange={(event) => setLdapForm((current) => ({ ...current, page_size: event.target.value }))} /></FormField>
|
||||||
<FormField label="Maximum entries"><input type="number" min="1" max="10000" value={ldapForm.max_entries} onChange={(event) => setLdapForm((current) => ({ ...current, max_entries: event.target.value }))} /></FormField>
|
<FormField label="Maximum entries"><input type="number" min="1" max="10000" value={ldapForm.max_entries} onChange={(event) => setLdapForm((current) => ({ ...current, max_entries: event.target.value }))} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<div className="address-import-mapping-grid">
|
<div className="address-import-mapping-grid">
|
||||||
{LDAP_MAPPING_FIELDS.map(([target, label]) =>
|
{LDAP_MAPPING_FIELDS.map(([target, label]) =>
|
||||||
<FormField label={label} key={target}>
|
<FormField label={label} key={target}>
|
||||||
@@ -3980,7 +3975,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</FormField>)}
|
</FormField>)}
|
||||||
</div>
|
</div>
|
||||||
<p className="muted">The directory remains authoritative and read-only. Preview the first refresh before applying it.</p>
|
<p className="muted">The directory remains authoritative and read-only. Preview the first refresh before applying it.</p>
|
||||||
</form>
|
</DialogForm>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
Reference in New Issue
Block a user