feat: add governed LDIF contact imports
This commit is contained in:
@@ -494,6 +494,7 @@ export type AddressImportConfiguration = {
|
||||
duplicate_source_key_policy: "reject" | "first" | "last";
|
||||
existing_contact_policy: "update" | "ignore" | "reject";
|
||||
blank_value_policy: "ignore" | "clear" | "reject";
|
||||
ldif_change_record_policy: "reject" | "ignore" | "treat_add_as_entry";
|
||||
locale?: string | null;
|
||||
default_tags: string[];
|
||||
max_rows: number;
|
||||
@@ -508,7 +509,7 @@ export type AddressImportProfile = {
|
||||
scope_id?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
source_format: "csv" | "xlsx";
|
||||
source_format: "csv" | "xlsx" | "ldif";
|
||||
configuration: AddressImportConfiguration;
|
||||
is_current: boolean;
|
||||
created_at: string;
|
||||
@@ -1068,7 +1069,7 @@ export function createAddressImportProfile(
|
||||
scope_id?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
source_format: "csv" | "xlsx";
|
||||
source_format: "csv" | "xlsx" | "ldif";
|
||||
configuration: AddressImportConfiguration;
|
||||
}
|
||||
): Promise<AddressImportProfile> {
|
||||
|
||||
@@ -237,7 +237,7 @@ type ImportMode = "vcard" | "tabular";
|
||||
|
||||
type ImportProfileFormState = {
|
||||
name: string;
|
||||
source_format: "csv" | "xlsx";
|
||||
source_format: "csv" | "xlsx" | "ldif";
|
||||
delimiter: "," | ";" | "\t" | "|";
|
||||
encoding: "utf-8" | "utf-8-sig" | "cp1252" | "latin-1";
|
||||
header_row: string;
|
||||
@@ -245,6 +245,7 @@ type ImportProfileFormState = {
|
||||
duplicate_source_key_policy: "reject" | "first" | "last";
|
||||
existing_contact_policy: "update" | "ignore" | "reject";
|
||||
blank_value_policy: "ignore" | "clear" | "reject";
|
||||
ldif_change_record_policy: "reject" | "ignore" | "treat_add_as_entry";
|
||||
locale: string;
|
||||
default_tags: string;
|
||||
max_rows: string;
|
||||
@@ -398,6 +399,39 @@ const EMPTY_LDAP_FORM: LdapFormState = {
|
||||
attribute_map: DEFAULT_LDAP_ATTRIBUTE_MAP
|
||||
};
|
||||
|
||||
const DEFAULT_TABULAR_FIELD_MAPPINGS: Record<string, string> = {
|
||||
source_key: "id",
|
||||
display_name: "display_name",
|
||||
given_name: "given_name",
|
||||
family_name: "family_name",
|
||||
organization: "organization",
|
||||
role_title: "role_title",
|
||||
email: "email",
|
||||
phone: "phone",
|
||||
street: "street",
|
||||
postal_code: "postal_code",
|
||||
locality: "locality",
|
||||
region: "region",
|
||||
country: "country",
|
||||
tags: "tags"
|
||||
};
|
||||
|
||||
const DEFAULT_LDIF_FIELD_MAPPINGS: Record<string, string> = {
|
||||
source_key: "dn",
|
||||
display_name: "cn",
|
||||
given_name: "givenname",
|
||||
family_name: "sn",
|
||||
organization: "o",
|
||||
role_title: "title",
|
||||
email: "mail",
|
||||
phone: "telephonenumber",
|
||||
street: "street",
|
||||
postal_code: "postalcode",
|
||||
locality: "l",
|
||||
region: "st",
|
||||
country: "c"
|
||||
};
|
||||
|
||||
const EMPTY_IMPORT_PROFILE_FORM: ImportProfileFormState = {
|
||||
name: "",
|
||||
source_format: "csv",
|
||||
@@ -408,25 +442,11 @@ const EMPTY_IMPORT_PROFILE_FORM: ImportProfileFormState = {
|
||||
duplicate_source_key_policy: "reject",
|
||||
existing_contact_policy: "update",
|
||||
blank_value_policy: "ignore",
|
||||
ldif_change_record_policy: "reject",
|
||||
locale: "",
|
||||
default_tags: "",
|
||||
max_rows: "10000",
|
||||
field_mappings: {
|
||||
source_key: "id",
|
||||
display_name: "display_name",
|
||||
given_name: "given_name",
|
||||
family_name: "family_name",
|
||||
organization: "organization",
|
||||
role_title: "role_title",
|
||||
email: "email",
|
||||
phone: "phone",
|
||||
street: "street",
|
||||
postal_code: "postal_code",
|
||||
locality: "locality",
|
||||
region: "region",
|
||||
country: "country",
|
||||
tags: "tags"
|
||||
}
|
||||
field_mappings: DEFAULT_TABULAR_FIELD_MAPPINGS
|
||||
};
|
||||
|
||||
const IMPORT_MAPPING_FIELDS = [
|
||||
@@ -2384,6 +2404,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
duplicate_source_key_policy: importProfileForm.duplicate_source_key_policy,
|
||||
existing_contact_policy: importProfileForm.existing_contact_policy,
|
||||
blank_value_policy: importProfileForm.blank_value_policy,
|
||||
ldif_change_record_policy: importProfileForm.ldif_change_record_policy,
|
||||
locale: importProfileForm.locale.trim() || null,
|
||||
default_tags: importProfileForm.default_tags.split(",").map((tag) => tag.trim()).filter(Boolean),
|
||||
max_rows: Number(importProfileForm.max_rows) || 10000
|
||||
@@ -2438,6 +2459,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
duplicate_source_key_policy: config.duplicate_source_key_policy,
|
||||
existing_contact_policy: config.existing_contact_policy,
|
||||
blank_value_policy: config.blank_value_policy,
|
||||
ldif_change_record_policy: config.ldif_change_record_policy ?? "reject",
|
||||
locale: config.locale ?? "",
|
||||
default_tags: config.default_tags.join(", "),
|
||||
max_rows: String(config.max_rows),
|
||||
@@ -3798,7 +3820,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
role="group"
|
||||
size="equal"
|
||||
ariaLabel="Contact import format"
|
||||
options={[{ id: "vcard", label: "vCard" }, { id: "tabular", label: "CSV / XLSX" }]}
|
||||
options={[{ id: "vcard", label: "vCard" }, { id: "tabular", label: "Mapped file" }]}
|
||||
value={importMode}
|
||||
onChange={(mode) => { setImportMode(mode); clearRetainedImportRun(); }}
|
||||
/>
|
||||
@@ -3861,9 +3883,18 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
<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="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 ImportProfileFormState["source_format"],
|
||||
field_mappings: event.target.value === "ldif"
|
||||
? DEFAULT_LDIF_FIELD_MAPPINGS
|
||||
: current.source_format === "ldif"
|
||||
? DEFAULT_TABULAR_FIELD_MAPPINGS
|
||||
: current.field_mappings
|
||||
}))}>
|
||||
<option value="csv">CSV</option>
|
||||
<option value="xlsx">XLSX</option>
|
||||
<option value="ldif">LDIF</option>
|
||||
</select>
|
||||
</FormField>
|
||||
{importProfileForm.source_format === "csv" && <>
|
||||
@@ -3879,7 +3910,15 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
</FormField>
|
||||
</>}
|
||||
{importProfileForm.source_format === "xlsx" && <FormField label="Sheet name"><input value={importProfileForm.sheet_name} onChange={(event) => setImportProfileForm((current) => ({ ...current, sheet_name: event.target.value }))} placeholder="First sheet" /></FormField>}
|
||||
<FormField label="Header row"><input type="number" min="1" max="100" value={importProfileForm.header_row} onChange={(event) => setImportProfileForm((current) => ({ ...current, header_row: event.target.value }))} /></FormField>
|
||||
{importProfileForm.source_format !== "ldif" && <FormField label="Header row"><input type="number" min="1" max="100" value={importProfileForm.header_row} onChange={(event) => setImportProfileForm((current) => ({ ...current, header_row: event.target.value }))} /></FormField>}
|
||||
{importProfileForm.source_format === "ldif" &&
|
||||
<FormField label="LDIF change records">
|
||||
<select value={importProfileForm.ldif_change_record_policy} onChange={(event) => setImportProfileForm((current) => ({ ...current, ldif_change_record_policy: event.target.value as ImportProfileFormState["ldif_change_record_policy"] }))}>
|
||||
<option value="reject">Reject with diagnostics</option>
|
||||
<option value="ignore">Ignore all change records</option>
|
||||
<option value="treat_add_as_entry">Import add records; reject modify/delete</option>
|
||||
</select>
|
||||
</FormField>}
|
||||
<FormField label="Duplicate source keys">
|
||||
<select value={importProfileForm.duplicate_source_key_policy} onChange={(event) => setImportProfileForm((current) => ({ ...current, duplicate_source_key_policy: event.target.value as ImportProfileFormState["duplicate_source_key_policy"] }))}>
|
||||
<option value="reject">Reject duplicates</option><option value="first">Use first row</option><option value="last">Use last row</option>
|
||||
@@ -3905,7 +3944,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
<input
|
||||
value={importProfileForm.field_mappings[target] ?? ""}
|
||||
onChange={(event) => setImportProfileForm((current) => ({ ...current, field_mappings: { ...current.field_mappings, [target]: event.target.value } }))}
|
||||
placeholder="Source column"
|
||||
placeholder={importProfileForm.source_format === "ldif" ? "LDIF attribute" : "Source column"}
|
||||
/>
|
||||
</FormField>)}
|
||||
</div>
|
||||
@@ -3914,7 +3953,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
<FormField label="Import file">
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,.xlsx,text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
accept=".csv,.xlsx,.ldif,.ldi,text/csv,application/ldif,text/ldif,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
onChange={(event) => { setImportFile(event.target.files?.[0] ?? null); clearRetainedImportRun(); }}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
Reference in New Issue
Block a user