Release govoplan-addresses v0.1.22: unify interface contracts and documentation
Module Package Release / publish-packages (push) Successful in 13s
Module Package Release / publish-packages (push) Successful in 13s
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/addresses-webui",
|
"name": "@govoplan/addresses-webui",
|
||||||
"version": "0.1.21",
|
"version": "0.1.22",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.45",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
"react-dom": ">=19.2.7 <20",
|
"react-dom": ">=19.2.7 <20",
|
||||||
|
|||||||
+2
-2
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-addresses"
|
name = "govoplan-addresses"
|
||||||
version = "0.1.21"
|
version = "0.1.22"
|
||||||
description = "GovOPlaN reusable address and recipient-source module."
|
description = "GovOPlaN reusable address and recipient-source module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defusedxml>=0.7.1",
|
"defusedxml>=0.7.1",
|
||||||
"govoplan-core>=0.1.18",
|
"govoplan-core>=0.1.45",
|
||||||
"ldap3>=2.9.1,<3",
|
"ldap3>=2.9.1,<3",
|
||||||
"openpyxl>=3.1.5,<4",
|
"openpyxl>=3.1.5,<4",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ LDAP_PROVIDER = ExternalProviderDeclaration(
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="addresses",
|
id="addresses",
|
||||||
name="Addresses",
|
name="Addresses",
|
||||||
version="0.1.21",
|
version="0.1.22",
|
||||||
required_capabilities=(
|
required_capabilities=(
|
||||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||||
@@ -716,7 +716,15 @@ manifest = ModuleManifest(
|
|||||||
body=(
|
body=(
|
||||||
"Campaigns may keep immutable campaign-local recipient snapshots, but durable address directories, "
|
"Campaigns may keep immutable campaign-local recipient snapshots, but durable address directories, "
|
||||||
"recipient-source definitions, consent metadata, provenance, deduplication, and import/export workflows "
|
"recipient-source definitions, consent metadata, provenance, deduplication, and import/export workflows "
|
||||||
"are owned by govoplan-addresses."
|
"are owned by govoplan-addresses. The Address Book workspace keeps Reload directly "
|
||||||
|
"before Add address book at the upper right. Import / export, Connections, and Address "
|
||||||
|
"quality open labelled, scoped tools; Manage applies to the selected book or list and "
|
||||||
|
"separates archive actions from editing. Export version is chosen in Import / export. "
|
||||||
|
"Contact creation remains beside the contact list. Folder icons alone expand or collapse "
|
||||||
|
"the tree; labels select a group, book, or list without changing expansion. A selected "
|
||||||
|
"group is navigation, not an aggregate contact book. Reload preserves collapsed "
|
||||||
|
"branches. All permission and read-only reasons, confirmations, import safeguards, "
|
||||||
|
"and contact-to-list drag and drop still apply."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
@@ -740,7 +748,15 @@ manifest = ModuleManifest(
|
|||||||
"body": (
|
"body": (
|
||||||
"Campaigns darf unveränderliche campaignlokale Empfänger-Snapshots halten. Dauerhafte Adressverzeichnisse, "
|
"Campaigns darf unveränderliche campaignlokale Empfänger-Snapshots halten. Dauerhafte Adressverzeichnisse, "
|
||||||
"Empfängerquellendefinitionen, Einwilligungsmetadaten, Herkunft, Dublettenbereinigung sowie Import- und Exportabläufe "
|
"Empfängerquellendefinitionen, Einwilligungsmetadaten, Herkunft, Dublettenbereinigung sowie Import- und Exportabläufe "
|
||||||
"gehören jedoch govoplan-addresses."
|
"gehören jedoch govoplan-addresses. Im Adressbuch steht Neuladen oben rechts unmittelbar vor "
|
||||||
|
"Adressbuch hinzufügen. Import / Export, Verbindungen und Adressqualität öffnen beschriftete, "
|
||||||
|
"kontextbezogene Werkzeuge. Verwalten bezieht sich auf das ausgewählte Adressbuch oder die Liste "
|
||||||
|
"und trennt Archivieren vom Bearbeiten. Die Exportversion wird unter Import / Export gewählt. "
|
||||||
|
"Kontakte werden weiterhin direkt neben der Kontaktliste angelegt. Nur Ordnersymbole klappen "
|
||||||
|
"den Baum auf oder zu; Beschriftungen wählen eine Gruppe, ein Adressbuch oder eine Liste aus, "
|
||||||
|
"ohne die Aufklappstellung zu ändern. Eine ausgewählte Gruppe dient der Navigation und ist "
|
||||||
|
"kein zusammengefasstes Adressbuch. Neuladen bewahrt zugeklappte Zweige. Berechtigungs- und "
|
||||||
|
"Schreibschutzgründe, Bestätigungen, Importsicherungen und das Ziehen von Kontakten in Listen gelten unverändert."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -748,6 +764,7 @@ manifest = ModuleManifest(
|
|||||||
"seed": True,
|
"seed": True,
|
||||||
"help_contexts": [
|
"help_contexts": [
|
||||||
"addresses.page",
|
"addresses.page",
|
||||||
|
"addresses.explorer.transfer",
|
||||||
"addresses.sources",
|
"addresses.sources",
|
||||||
"addresses.contacts",
|
"addresses.contacts",
|
||||||
"addresses.detail",
|
"addresses.detail",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/addresses-webui",
|
"name": "@govoplan/addresses-webui",
|
||||||
"version": "0.1.21",
|
"version": "0.1.22",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"test:import-run": "rm -rf .import-run-test-build && mkdir -p .import-run-test-build && printf '{\"type\":\"commonjs\"}\\n' > .import-run-test-build/package.json && ../../govoplan-core/webui/node_modules/.bin/tsc -p tsconfig.import-run-tests.json && node .import-run-test-build/tests/import-run-state.test.js"
|
"test:import-run": "rm -rf .import-run-test-build && mkdir -p .import-run-test-build && printf '{\"type\":\"commonjs\"}\\n' > .import-run-test-build/package.json && ../../govoplan-core/webui/node_modules/.bin/tsc -p tsconfig.import-run-tests.json && node .import-run-test-build/tests/import-run-state.test.js"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.45",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
"react-dom": ">=19.2.7 <20",
|
"react-dom": ">=19.2.7 <20",
|
||||||
|
|||||||
@@ -17,5 +17,14 @@ assert.match(page, /<SegmentedControl<ConflictMergeChoice>[\s\S]*role="group"[\s
|
|||||||
assert.doesNotMatch(page, /<button[\s\S]{0,160}(?:address-contact-row|address-sync-result-row)/);
|
assert.doesNotMatch(page, /<button[\s\S]{0,160}(?:address-contact-row|address-sync-result-row)/);
|
||||||
assert.doesNotMatch(styles, /\.address-conflict-choice button/);
|
assert.doesNotMatch(styles, /\.address-conflict-choice button/);
|
||||||
assert.doesNotMatch(styles, /\.address-contact-row:(?:hover|focus-visible)/);
|
assert.doesNotMatch(styles, /\.address-contact-row:(?:hover|focus-visible)/);
|
||||||
|
assert.match(page, /<PageActionBar[\s\S]*variant="collection"[\s\S]*reloadAction=[\s\S]*createAction=/);
|
||||||
|
assert.doesNotMatch(page, /renderSelectedBookActions|address-icon-actions/);
|
||||||
|
assert.match(page, /renderAddressActions\(\)/);
|
||||||
|
assert.match(page, /<FormSection variant="separated" title="i18n:govoplan-addresses\.explorer\.archive_section"/);
|
||||||
|
const openTreeNode = page.slice(page.indexOf(" function openTreeNode("), page.indexOf(" function toggleTreeNode("));
|
||||||
|
assert.doesNotMatch(openTreeNode, /toggleTreeNode\(/, "Labels only select, never expand or collapse.");
|
||||||
|
assert.match(openTreeNode, /setSelectedTreeGroup\(\{ id: node\.id, label: node\.label \}\)/);
|
||||||
|
assert.match(page, /selectedTreeGroup\?\.id \?\?/);
|
||||||
|
assert.match(page, /if \(!previousBranchIds\.has\(id\)\) next\.add\(id\)/, "Reload preserves collapsed branches.");
|
||||||
|
|
||||||
console.log("Address-book flat selections use central components.");
|
console.log("Address-book flat selections use central components.");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MetricGrid } from "@govoplan/core-webui";
|
import { MetricGrid } from "@govoplan/core-webui";
|
||||||
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, Settings2, 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, useRef, useState, type DragEvent as ReactDragEvent, type FormEvent } from "react";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import { DialogSection, DialogForm, FormGrid, ActionToolbar,
|
import { DialogSection, DialogForm, FormGrid, ActionToolbar,
|
||||||
ApiError,
|
ApiError,
|
||||||
@@ -18,6 +18,7 @@ import { DialogSection, DialogForm, FormGrid, ActionToolbar,
|
|||||||
FormSection,
|
FormSection,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
|
PageActionBar,
|
||||||
PasswordField,
|
PasswordField,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
SelectionList,
|
SelectionList,
|
||||||
@@ -659,7 +660,7 @@ function canMergeConflict(conflict: AddressSyncConflict): boolean {
|
|||||||
return Boolean(conflictPayload(conflict.local_value) && conflictRemotePayload(conflict));
|
return Boolean(conflictPayload(conflict.local_value) && conflictRemotePayload(conflict));
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultConflictMergeChoices(conflict: AddressSyncConflict): Record<string, ConflictMergeChoice> {
|
function defaultConflictMergeChoices(_conflict: AddressSyncConflict): Record<string, ConflictMergeChoice> {
|
||||||
return Object.fromEntries(CONFLICT_PAYLOAD_FIELDS.map((field) => [field, "local" as ConflictMergeChoice]));
|
return Object.fromEntries(CONFLICT_PAYLOAD_FIELDS.map((field) => [field, "local" as ConflictMergeChoice]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,7 +964,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
const [selectedBookId, setSelectedBookId] = useState("");
|
const [selectedBookId, setSelectedBookId] = useState("");
|
||||||
const [selectedListId, setSelectedListId] = useState("");
|
const [selectedListId, setSelectedListId] = useState("");
|
||||||
const [selectedContactId, setSelectedContactId] = useState("");
|
const [selectedContactId, setSelectedContactId] = useState("");
|
||||||
|
const [selectedTreeGroup, setSelectedTreeGroup] = useState<Pick<AddressTreeNode, "id" | "label"> | null>(null);
|
||||||
const [expandedTreeIds, setExpandedTreeIds] = useState<Set<string>>(() => new Set());
|
const [expandedTreeIds, setExpandedTreeIds] = useState<Set<string>>(() => new Set());
|
||||||
|
const knownTreeBranchIds = useRef(new Set<string>());
|
||||||
|
const [addressActionsOpen, setAddressActionsOpen] = useState<"book" | "transfer" | "connections" | null>(null);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -1136,8 +1140,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
.then((run) => {
|
.then((run) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
setImportRun(run);
|
setImportRun(run);
|
||||||
|
setSelectedTreeGroup(null);
|
||||||
setSelectedBookId(run.address_book_id);
|
setSelectedBookId(run.address_book_id);
|
||||||
setSelectedImportProfileId(run.profile_id);
|
setSelectedImportProfileId(run.profile_id ?? "");
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
@@ -1161,9 +1166,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
return () => {active = false;};
|
return () => {active = false;};
|
||||||
}, [auth.groups_loaded, auth.user.id, auth.active_tenant?.id, auth.tenant.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
}, [auth.groups_loaded, auth.user.id, auth.active_tenant?.id, auth.tenant.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
||||||
|
|
||||||
const selectedBook = books.find((book) => book.id === selectedBookId) ?? books[0] ?? null;
|
const selectedBook = selectedTreeGroup ? null : books.find((book) => book.id === selectedBookId) ?? books[0] ?? null;
|
||||||
const importLifecycle = importRun ? importRunLifecycle(importRun.status) : null;
|
const importLifecycle = importRun ? importRunLifecycle(importRun.status) : null;
|
||||||
const selectedList = addressLists.find((list) => list.id === selectedListId) ?? null;
|
const selectedList = selectedTreeGroup ? null : addressLists.find((list) => list.id === selectedListId) ?? null;
|
||||||
const selectedBookSyncSources = useMemo(
|
const selectedBookSyncSources = useMemo(
|
||||||
() => selectedBook ? syncSources.filter((source) => source.address_book_id === selectedBook.id) : [],
|
() => selectedBook ? syncSources.filter((source) => source.address_book_id === selectedBook.id) : [],
|
||||||
[selectedBook, syncSources]
|
[selectedBook, syncSources]
|
||||||
@@ -1172,7 +1177,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
const addressTreeNodes = useMemo(() => buildAddressTree(books, addressLists), [addressLists, books]);
|
const addressTreeNodes = useMemo(() => buildAddressTree(books, addressLists), [addressLists, books]);
|
||||||
const selectedListContactIds = useMemo(() => listEntryContactIds(addressListEntries), [addressListEntries]);
|
const selectedListContactIds = useMemo(() => listEntryContactIds(addressListEntries), [addressListEntries]);
|
||||||
const selectedListEntryKeys = useMemo(() => new Set(addressListEntries.map(addressListEntryKey)), [addressListEntries]);
|
const selectedListEntryKeys = useMemo(() => new Set(addressListEntries.map(addressListEntryKey)), [addressListEntries]);
|
||||||
const visibleContacts = contacts;
|
const visibleContacts = selectedTreeGroup ? [] : contacts;
|
||||||
const memberCandidateContacts = useMemo(() => {
|
const memberCandidateContacts = useMemo(() => {
|
||||||
const normalizedQuery = memberQuery.trim().toLowerCase();
|
const normalizedQuery = memberQuery.trim().toLowerCase();
|
||||||
return memberCandidates
|
return memberCandidates
|
||||||
@@ -1208,7 +1213,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
return [];
|
return [];
|
||||||
}, [channelRuleForm.channel, governanceContact]);
|
}, [channelRuleForm.channel, governanceContact]);
|
||||||
const selectedContactListEntries = selectedContact && selectedList ? contactListEntries(addressListEntries, selectedContact.id) : [];
|
const selectedContactListEntries = selectedContact && selectedList ? contactListEntries(addressListEntries, selectedContact.id) : [];
|
||||||
const activeTreeId = selectedList ? `list:${selectedList.id}` : selectedBook ? `book:${selectedBook.id}` : "";
|
const activeTreeId = selectedTreeGroup?.id ?? (selectedList ? `list:${selectedList.id}` : selectedBook ? `book:${selectedBook.id}` : "");
|
||||||
const loadingReason = loading ? "Address books are loading." : "";
|
const loadingReason = loading ? "Address books are loading." : "";
|
||||||
const savingReason = saving ? "An address-book action is already in progress." : "";
|
const savingReason = saving ? "An address-book action is already in progress." : "";
|
||||||
const createBookReason = disabledReason(
|
const createBookReason = disabledReason(
|
||||||
@@ -1570,10 +1575,13 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const defaultExpanded = expandedAddressTreeIds(addressTreeNodes);
|
const defaultExpanded = expandedAddressTreeIds(addressTreeNodes);
|
||||||
if (defaultExpanded.size === 0) return;
|
const previousBranchIds = knownTreeBranchIds.current;
|
||||||
|
knownTreeBranchIds.current = defaultExpanded;
|
||||||
setExpandedTreeIds((current) => {
|
setExpandedTreeIds((current) => {
|
||||||
const next = new Set(current);
|
const next = new Set([...current].filter((id) => defaultExpanded.has(id)));
|
||||||
for (const id of defaultExpanded) next.add(id);
|
for (const id of defaultExpanded) {
|
||||||
|
if (!previousBranchIds.has(id)) next.add(id);
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, [addressTreeNodes]);
|
}, [addressTreeNodes]);
|
||||||
@@ -1950,6 +1958,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
|
|
||||||
function openTreeNode(node: AddressTreeNode) {
|
function openTreeNode(node: AddressTreeNode) {
|
||||||
if (node.kind === "book" && node.book) {
|
if (node.kind === "book" && node.book) {
|
||||||
|
setSelectedTreeGroup(null);
|
||||||
setContactPage(1);
|
setContactPage(1);
|
||||||
setSelectedBookId(node.book.id);
|
setSelectedBookId(node.book.id);
|
||||||
setSelectedListId("");
|
setSelectedListId("");
|
||||||
@@ -1957,13 +1966,16 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (node.kind === "list" && node.list) {
|
if (node.kind === "list" && node.list) {
|
||||||
|
setSelectedTreeGroup(null);
|
||||||
setContactPage(1);
|
setContactPage(1);
|
||||||
setSelectedBookId(node.list.address_book_id);
|
setSelectedBookId(node.list.address_book_id);
|
||||||
setSelectedListId(node.list.id);
|
setSelectedListId(node.list.id);
|
||||||
setSelectedContactId("");
|
setSelectedContactId("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toggleTreeNode(node);
|
setSelectedTreeGroup({ id: node.id, label: node.label });
|
||||||
|
setSelectedListId("");
|
||||||
|
setSelectedContactId("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTreeNode(node: AddressTreeNode) {
|
function toggleTreeNode(node: AddressTreeNode) {
|
||||||
@@ -2450,8 +2462,9 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
try {
|
try {
|
||||||
const run = await getAddressImportRun(settings, importRun?.id || requestedImportRunId);
|
const run = await getAddressImportRun(settings, importRun?.id || requestedImportRunId);
|
||||||
retainImportRun(run);
|
retainImportRun(run);
|
||||||
|
setSelectedTreeGroup(null);
|
||||||
setSelectedBookId(run.address_book_id);
|
setSelectedBookId(run.address_book_id);
|
||||||
setSelectedImportProfileId(run.profile_id);
|
setSelectedImportProfileId(run.profile_id ?? "");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setImportRun(null);
|
setImportRun(null);
|
||||||
setImportRunUnavailable(
|
setImportRunUnavailable(
|
||||||
@@ -2555,7 +2568,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
|
|
||||||
function downloadImportCorrections() {
|
function downloadImportCorrections() {
|
||||||
if (!importRun) return;
|
if (!importRun) return;
|
||||||
const quote = (value: unknown) => `"${String(value ?? "").replaceAll('"', '""')}"`;
|
const quote = (value: unknown) => `"${String(value ?? "").replace(/"/g, '""')}"`;
|
||||||
const lines = [
|
const lines = [
|
||||||
["severity", "row", "field", "code", "message"].map(quote).join(","),
|
["severity", "row", "field", "code", "message"].map(quote).join(","),
|
||||||
...importRun.diagnostics.map((item) => [item.severity, item.row_number ?? "", item.field ?? "", item.code, item.message].map(quote).join(","))
|
...importRun.diagnostics.map((item) => [item.severity, item.row_number ?? "", item.field ?? "", item.code, item.message].map(quote).join(","))
|
||||||
@@ -2911,6 +2924,10 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSelectedBookPanel() {
|
function renderSelectedBookPanel() {
|
||||||
|
if (selectedTreeGroup) return <div className="address-source-summary">
|
||||||
|
<strong>{selectedTreeGroup.label}</strong>
|
||||||
|
<p>i18n:govoplan-addresses.explorer.choose_book_in_group</p>
|
||||||
|
</div>;
|
||||||
if (!selectedBook) return <p className="address-empty-note">No address book selected.</p>;
|
if (!selectedBook) return <p className="address-empty-note">No address book selected.</p>;
|
||||||
return (
|
return (
|
||||||
<div className="address-source-summary">
|
<div className="address-source-summary">
|
||||||
@@ -2952,40 +2969,68 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSelectedBookActions() {
|
function chooseAddressAction(action: () => void) {
|
||||||
return (
|
setAddressActionsOpen(null);
|
||||||
<>
|
action();
|
||||||
<Button type="button" title="Refresh address books" aria-label="Refresh address books" onClick={() => void refreshAll()} disabledReason={refreshReason}><RefreshCw size={15} /></Button>
|
}
|
||||||
<Button type="button" title="Review address quality" aria-label="Review address quality" onClick={() => void openQualityReview()} disabledReason={qualityDashboardReason}><ShieldCheck size={15} /></Button>
|
|
||||||
<Button type="button" title="Add address book" aria-label="Add address book" variant="primary" onClick={openCreateBookDialog} disabledReason={createBookReason}><Plus size={15} /></Button>
|
function renderAddressActions() {
|
||||||
<Button type="button" title="Add address list" aria-label="Add address list" onClick={openCreateListDialog} disabledReason={createListReason}><Plus size={15} /></Button>
|
return <Dialog
|
||||||
<Button type="button" title="Import contacts" aria-label="Import contacts" onClick={openImportDialog} disabledReason={importBookReason}><Upload size={15} /></Button>
|
open={addressActionsOpen !== null}
|
||||||
<select aria-label="vCard export version" value={vcardExportVersion} onChange={(event) => setVcardExportVersion(event.target.value as "3.0" | "4.0")}>
|
title={addressActionsOpen === "transfer" ? "i18n:govoplan-addresses.explorer.transfer" : addressActionsOpen === "connections" ? "i18n:govoplan-addresses.explorer.connections" : "i18n:govoplan-addresses.explorer.manage_selection"}
|
||||||
<option value="4.0">vCard 4.0</option>
|
description={selectedList?.name ?? selectedBook?.name ?? "i18n:govoplan-addresses.explorer.no_book"}
|
||||||
<option value="3.0">vCard 3.0</option>
|
onClose={() => setAddressActionsOpen(null)}
|
||||||
</select>
|
footer={<Button type="button" onClick={() => setAddressActionsOpen(null)}>i18n:govoplan-core.close.bbfa773e</Button>}>
|
||||||
<Button type="button" title={`Export selected ${selectedList ? "address list" : "address book"} as vCard ${vcardExportVersion}`} aria-label={`Export selected ${selectedList ? "address list" : "address book"} as vCard ${vcardExportVersion}`} onClick={() => void exportSelectedBook()} disabledReason={exportBookReason}><Download size={15} /></Button>
|
{addressActionsOpen === "book" && <>
|
||||||
<Button type="button" title="Connect CardDAV" aria-label="Connect CardDAV" onClick={openCardDavDialog} disabledReason={connectCardDavReason}><Link2 size={15} /></Button>
|
<FormSection title="i18n:govoplan-addresses.explorer.manage_selection">
|
||||||
<Button type="button" title="Connect LDAP or Active Directory" aria-label="Connect LDAP or Active Directory" onClick={openLdapDialog} disabledReason={connectLdapReason}><Network size={15} /></Button>
|
<ActionToolbar surface="plain">
|
||||||
<Button type="button" title="Inspect sync source" aria-label="Inspect sync source" onClick={() => selectedSyncSource && void openSyncInspector(selectedSyncSource)} disabledReason={inspectSyncReason}><Search size={15} /></Button>
|
<Button type="button" onClick={() => chooseAddressAction(openCreateListDialog)} disabledReason={createListReason}><Plus size={16} aria-hidden="true" /> Add address list</Button>
|
||||||
<Button type="button" title="Preview sync" aria-label="Preview sync" onClick={() => void previewSelectedSync()} disabledReason={previewSyncReason}>Preview</Button>
|
{selectedList?.deleted_at ?
|
||||||
<Button type="button" title="Run sync" aria-label="Run sync" onClick={() => void runSelectedSync()} disabledReason={runSyncReason}>Sync</Button>
|
<Button type="button" disabledReason={restoreListReason(selectedList)} onClick={() => chooseAddressAction(() => void restoreDeletedList(selectedList))}><RotateCcw size={16} aria-hidden="true" /> Restore address list</Button> : selectedList ?
|
||||||
{selectedList ?
|
<Button type="button" disabledReason={editListReason(selectedList)} onClick={() => chooseAddressAction(() => openEditListDialog(selectedList))}><Edit3 size={16} aria-hidden="true" /> Edit address list</Button> : selectedBook?.deleted_at ?
|
||||||
selectedList.deleted_at ?
|
<Button type="button" disabledReason={restoreBookReason(selectedBook)} onClick={() => chooseAddressAction(() => void restoreBook(selectedBook))}><RotateCcw size={16} aria-hidden="true" /> Restore address book</Button> :
|
||||||
<Button type="button" title="Restore address list" aria-label="Restore address list" disabledReason={restoreListReason(selectedList)} onClick={() => void restoreDeletedList(selectedList)}><RotateCcw size={15} /></Button> :
|
<Button type="button" disabledReason={selectedBook ? editBookReason(selectedBook) : "Select an address book before editing."} onClick={() => selectedBook && chooseAddressAction(() => openEditBookDialog(selectedBook))}><Edit3 size={16} aria-hidden="true" /> Edit address book</Button>}
|
||||||
<>
|
</ActionToolbar>
|
||||||
<Button type="button" title="Edit address list" aria-label="Edit address list" disabledReason={editListReason(selectedList)} onClick={() => openEditListDialog(selectedList)}><Edit3 size={15} /></Button>
|
</FormSection>
|
||||||
<Button type="button" variant="danger" title="Delete address list" aria-label="Delete address list" helpContextId="addresses.action.archive" helpModuleId="addresses" disabledReason={deleteListReason(selectedList)} onClick={() => setConfirmState({ kind: "list", list: selectedList })}><Trash2 size={15} /></Button>
|
{(selectedList ? !selectedList.deleted_at : selectedBook && !selectedBook.deleted_at) && <FormSection variant="separated" title="i18n:govoplan-addresses.explorer.archive_section">
|
||||||
</> :
|
<ActionToolbar surface="plain">
|
||||||
selectedBook?.deleted_at ?
|
{selectedList ?
|
||||||
<Button type="button" title="Restore address book" aria-label="Restore address book" disabledReason={restoreBookReason(selectedBook)} onClick={() => void restoreBook(selectedBook)}><RotateCcw size={15} /></Button> :
|
<Button type="button" variant="danger" helpContextId="addresses.action.archive" helpModuleId="addresses" disabledReason={deleteListReason(selectedList)} onClick={() => chooseAddressAction(() => setConfirmState({ kind: "list", list: selectedList }))}><Trash2 size={16} aria-hidden="true" /> Delete address list</Button> : selectedBook &&
|
||||||
<>
|
<Button type="button" variant="danger" helpContextId="addresses.action.archive" helpModuleId="addresses" disabledReason={deleteBookReason(selectedBook)} onClick={() => chooseAddressAction(() => setConfirmState({ kind: "book", book: selectedBook }))}><Trash2 size={16} aria-hidden="true" /> Delete address book</Button>}
|
||||||
<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>
|
</ActionToolbar>
|
||||||
<Button type="button" variant="danger" title="Delete address book" aria-label="Delete address book" helpContextId="addresses.action.archive" helpModuleId="addresses" disabledReason={selectedBook ? deleteBookReason(selectedBook) : "Select an address book before deleting."} onClick={() => selectedBook && setConfirmState({ kind: "book", book: selectedBook })}><Trash2 size={15} /></Button>
|
</FormSection>}
|
||||||
</>
|
</>}
|
||||||
}
|
{addressActionsOpen === "transfer" && <>
|
||||||
</>
|
<FormSection title="i18n:govoplan-addresses.explorer.import_section">
|
||||||
);
|
<ActionToolbar surface="plain"><Button type="button" onClick={() => chooseAddressAction(openImportDialog)} disabledReason={importBookReason}><Upload size={16} aria-hidden="true" /> Import contacts</Button></ActionToolbar>
|
||||||
|
</FormSection>
|
||||||
|
<FormSection variant="separated" title="i18n:govoplan-addresses.explorer.export_section">
|
||||||
|
<FormGrid columns={2} collapseAt="standard">
|
||||||
|
<FormField label="i18n:govoplan-addresses.explorer.vcard_version">
|
||||||
|
<select value={vcardExportVersion} onChange={(event) => setVcardExportVersion(event.target.value as "3.0" | "4.0")}>
|
||||||
|
<option value="4.0">vCard 4.0</option><option value="3.0">vCard 3.0</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
</FormGrid>
|
||||||
|
<ActionToolbar surface="plain"><Button type="button" onClick={() => chooseAddressAction(() => void exportSelectedBook())} disabledReason={exportBookReason}><Download size={16} aria-hidden="true" /> {selectedList ? "i18n:govoplan-addresses.explorer.export_list" : "i18n:govoplan-addresses.explorer.export_book"}</Button></ActionToolbar>
|
||||||
|
</FormSection>
|
||||||
|
</>}
|
||||||
|
{addressActionsOpen === "connections" && <>
|
||||||
|
<FormSection title="i18n:govoplan-addresses.explorer.connect_section">
|
||||||
|
<ActionToolbar surface="plain">
|
||||||
|
<Button type="button" onClick={() => chooseAddressAction(openCardDavDialog)} disabledReason={connectCardDavReason}><Link2 size={16} aria-hidden="true" /> Connect CardDAV</Button>
|
||||||
|
<Button type="button" onClick={() => chooseAddressAction(openLdapDialog)} disabledReason={connectLdapReason}><Network size={16} aria-hidden="true" /> Connect LDAP or Active Directory</Button>
|
||||||
|
</ActionToolbar>
|
||||||
|
</FormSection>
|
||||||
|
<FormSection variant="separated" title="i18n:govoplan-addresses.explorer.sync_section" description={selectedSyncSource ? syncSourceLabel(selectedSyncSource) : "i18n:govoplan-addresses.explorer.no_sync_source"}>
|
||||||
|
<ActionToolbar surface="plain">
|
||||||
|
<Button type="button" onClick={() => selectedSyncSource && chooseAddressAction(() => void openSyncInspector(selectedSyncSource))} disabledReason={inspectSyncReason}><Search size={16} aria-hidden="true" /> Inspect sync source</Button>
|
||||||
|
<Button type="button" onClick={() => chooseAddressAction(() => void previewSelectedSync())} disabledReason={previewSyncReason}>Preview sync</Button>
|
||||||
|
<Button type="button" onClick={() => chooseAddressAction(() => void runSelectedSync())} disabledReason={runSyncReason}><RefreshCw size={16} aria-hidden="true" /> Run sync</Button>
|
||||||
|
</ActionToolbar>
|
||||||
|
</FormSection>
|
||||||
|
</>}
|
||||||
|
</Dialog>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderContactRow(contact: Contact) {
|
function renderContactRow(contact: Contact) {
|
||||||
@@ -3196,6 +3241,20 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="workspace-data-page module-entry-page address-book-page address-book-fullscreen">
|
<div className="workspace-data-page module-entry-page address-book-page address-book-fullscreen">
|
||||||
|
<PageActionBar
|
||||||
|
variant="collection"
|
||||||
|
label="i18n:govoplan-addresses.explorer.page_actions"
|
||||||
|
className="address-page-actions"
|
||||||
|
refreshable
|
||||||
|
reloadAction={{ onReload: () => void refreshAll(), loading, disabledReason: refreshReason }}
|
||||||
|
contextActions={<>
|
||||||
|
<Button type="button" helpContextId="addresses.explorer.transfer" helpModuleId="addresses" helpTopicId="addresses.boundary" onClick={() => setAddressActionsOpen("transfer")} disabledReason={savingReason}><Upload size={16} aria-hidden="true" /> i18n:govoplan-addresses.explorer.transfer</Button>
|
||||||
|
<Button type="button" onClick={() => setAddressActionsOpen("connections")} disabledReason={savingReason}><Link2 size={16} aria-hidden="true" /> i18n:govoplan-addresses.explorer.connections</Button>
|
||||||
|
<Button type="button" onClick={() => void openQualityReview()} disabledReason={qualityDashboardReason}><ShieldCheck size={16} aria-hidden="true" /> i18n:govoplan-addresses.explorer.quality</Button>
|
||||||
|
</>}
|
||||||
|
helpAction={<DocumentationHelpLink reference={ADDRESSES_DOCUMENTATION} />}
|
||||||
|
createAction={<Button type="button" variant="primary" onClick={openCreateBookDialog} disabledReason={createBookReason}><Plus size={16} aria-hidden="true" /> Add address book</Button>}
|
||||||
|
/>
|
||||||
{error && <DismissibleAlert className="address-error" compact tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert className="address-error" compact tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
{notice && !error && <DismissibleAlert className="address-error" compact tone="success" resetKey={notice}>{notice}</DismissibleAlert>}
|
{notice && !error && <DismissibleAlert className="address-error" compact tone="success" resetKey={notice}>{notice}</DismissibleAlert>}
|
||||||
{!canWriteBooks && !canWriteLists && !canWriteContacts && <ActionBlockerHint
|
{!canWriteBooks && !canWriteLists && !canWriteContacts && <ActionBlockerHint
|
||||||
@@ -3219,10 +3278,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<h2>Address books</h2>
|
<h2>Address books</h2>
|
||||||
<p>{books.length} book{books.length === 1 ? "" : "s"}</p>
|
<p>{books.length} book{books.length === 1 ? "" : "s"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions address-icon-actions">
|
<Button type="button" onClick={() => setAddressActionsOpen("book")} disabledReason={savingReason || (!selectedBook ? "i18n:govoplan-addresses.explorer.no_book" : "")}><Settings2 size={16} aria-hidden="true" /> i18n:govoplan-addresses.explorer.manage</Button>
|
||||||
<DocumentationHelpLink reference={ADDRESSES_DOCUMENTATION} />
|
|
||||||
{renderSelectedBookActions()}
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<div className="address-tree-filter-row">
|
<div className="address-tree-filter-row">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@@ -3266,7 +3322,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<div>
|
<div>
|
||||||
<h2>{selectedList ? selectedList.name : selectedBook ? selectedBook.name : "Contacts"}</h2>
|
<h2>{selectedList ? selectedList.name : selectedBook ? selectedBook.name : "Contacts"}</h2>
|
||||||
<p>
|
<p>
|
||||||
{contactTotal} contact{contactTotal === 1 ? "" : "s"}
|
{selectedTreeGroup ? 0 : contactTotal} contact{!selectedTreeGroup && contactTotal === 1 ? "" : "s"}
|
||||||
{selectedList ? " in selected list" : selectedBook ? " in selected book" : ""}
|
{selectedList ? " in selected list" : selectedBook ? " in selected book" : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -3274,7 +3330,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
{selectedList &&
|
{selectedList &&
|
||||||
<Button type="button" onClick={() => void openAddMembersDialog()} disabledReason={addMembersReason}><UserPlus size={16} /> Add to list</Button>
|
<Button type="button" onClick={() => void openAddMembersDialog()} disabledReason={addMembersReason}><UserPlus size={16} /> Add to list</Button>
|
||||||
}
|
}
|
||||||
<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} /> Add contact</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<ActionToolbar className="address-contact-toolbar">
|
<ActionToolbar className="address-contact-toolbar">
|
||||||
@@ -3299,7 +3355,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<DataGridPaginationBar
|
<DataGridPaginationBar
|
||||||
page={contactPage}
|
page={contactPage}
|
||||||
pageSize={contactPageSize}
|
pageSize={contactPageSize}
|
||||||
totalRows={contactTotal}
|
totalRows={selectedTreeGroup ? 0 : contactTotal}
|
||||||
pageSizeOptions={[25, 50, 100, 200]}
|
pageSizeOptions={[25, 50, 100, 200]}
|
||||||
disabled={loading || saving || !selectedBook}
|
disabled={loading || saving || !selectedBook}
|
||||||
className="address-contact-pagination"
|
className="address-contact-pagination"
|
||||||
@@ -3317,6 +3373,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
</div>
|
</div>
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
|
|
||||||
|
{renderAddressActions()}
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={Boolean(bookDialog)}
|
open={Boolean(bookDialog)}
|
||||||
title={bookDialog?.mode === "edit" ? "Edit address book" : "Add address book"}
|
title={bookDialog?.mode === "edit" ? "Edit address book" : "Add address book"}
|
||||||
@@ -3480,7 +3538,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<article className="address-governance-rule" key={rule.id}>
|
<article className="address-governance-rule" key={rule.id}>
|
||||||
<div className="address-governance-rule-main">
|
<div className="address-governance-rule-main">
|
||||||
<span className="address-governance-rule-heading">
|
<span className="address-governance-rule-heading">
|
||||||
<strong>{rule.channel.replace("_", " ")} · {rule.decision.replaceAll("_", " ")}</strong>
|
<strong>{rule.channel.replace("_", " ")} · {rule.decision.replace(/_/g, " ")}</strong>
|
||||||
<StatusBadge status={state} />
|
<StatusBadge status={state} />
|
||||||
</span>
|
</span>
|
||||||
<span>{rule.purpose || "All purposes"}{rule.reason ? ` · ${rule.reason}` : ""}</span>
|
<span>{rule.purpose || "All purposes"}{rule.reason ? ` · ${rule.reason}` : ""}</span>
|
||||||
@@ -4480,7 +4538,7 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
|||||||
<StatusBadge status={conflictDialog.status} />
|
<StatusBadge status={conflictDialog.status} />
|
||||||
{conflictDialog.resolution && <StatusBadge status={conflictDialog.resolution} />}
|
{conflictDialog.resolution && <StatusBadge status={conflictDialog.resolution} />}
|
||||||
</div>
|
</div>
|
||||||
{conflictDialog.metadata?.message && <p className="muted">{String(conflictDialog.metadata.message)}</p>}
|
{Boolean(conflictDialog.metadata?.message) && <p className="muted">{String(conflictDialog.metadata.message)}</p>}
|
||||||
{!canApplyRemoteConflict(conflictDialog) && <DismissibleAlert tone="warning" dismissible={false}>This conflict predates stored field payloads or came from a stale write. It can be marked resolved or ignored, but the remote value cannot be applied automatically.</DismissibleAlert>}
|
{!canApplyRemoteConflict(conflictDialog) && <DismissibleAlert tone="warning" dismissible={false}>This conflict predates stored field payloads or came from a stale write. It can be marked resolved or ignored, but the remote value cannot be applied automatically.</DismissibleAlert>}
|
||||||
</div>
|
</div>
|
||||||
<div className="address-conflict-grid">
|
<div className="address-conflict-grid">
|
||||||
|
|||||||
@@ -2,6 +2,33 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
|||||||
|
|
||||||
export const generatedTranslations: PlatformTranslations = {
|
export const generatedTranslations: PlatformTranslations = {
|
||||||
en: {
|
en: {
|
||||||
|
"i18n:govoplan-addresses.explorer.page_actions": "Address book actions",
|
||||||
|
"i18n:govoplan-addresses.explorer.transfer": "Import / export",
|
||||||
|
"i18n:govoplan-addresses.explorer.connections": "Connections",
|
||||||
|
"i18n:govoplan-addresses.explorer.quality": "Address quality",
|
||||||
|
"i18n:govoplan-addresses.explorer.manage": "Manage",
|
||||||
|
"i18n:govoplan-addresses.explorer.manage_selection": "Manage selected book or list",
|
||||||
|
"i18n:govoplan-addresses.explorer.no_book": "Select an address book first.",
|
||||||
|
"i18n:govoplan-addresses.explorer.choose_book_in_group": "Select an address book in this group. Use the folder icon to expand or collapse it; clicking the label only selects it.",
|
||||||
|
"i18n:govoplan-addresses.explorer.archive_section": "Archive selected book or list",
|
||||||
|
"i18n:govoplan-addresses.explorer.import_section": "Import into the selected book",
|
||||||
|
"i18n:govoplan-addresses.explorer.export_section": "Export the selected book or list",
|
||||||
|
"i18n:govoplan-addresses.explorer.vcard_version": "vCard export version",
|
||||||
|
"i18n:govoplan-addresses.explorer.export_list": "Export address list",
|
||||||
|
"i18n:govoplan-addresses.explorer.export_book": "Export address book",
|
||||||
|
"i18n:govoplan-addresses.explorer.connect_section": "Connect an address source",
|
||||||
|
"i18n:govoplan-addresses.explorer.sync_section": "Synchronization for the selected book",
|
||||||
|
"i18n:govoplan-addresses.explorer.no_sync_source": "The selected book has no connected synchronization source.",
|
||||||
|
"Restore address list": "Restore address list",
|
||||||
|
"Restore address book": "Restore address book",
|
||||||
|
"Delete address list": "Delete address list",
|
||||||
|
"Delete address book": "Delete address book",
|
||||||
|
"Import contacts": "Import contacts",
|
||||||
|
"Connect CardDAV": "Connect CardDAV",
|
||||||
|
"Connect LDAP or Active Directory": "Connect LDAP or Active Directory",
|
||||||
|
"Inspect sync source": "Inspect sync source",
|
||||||
|
"Preview sync": "Preview sync",
|
||||||
|
"Run sync": "Run sync",
|
||||||
"i18n:govoplan-addresses.add_contact.6da0b4b8": "Add contact",
|
"i18n:govoplan-addresses.add_contact.6da0b4b8": "Add contact",
|
||||||
"i18n:govoplan-addresses.address_book.f6327f59": "Address Book",
|
"i18n:govoplan-addresses.address_book.f6327f59": "Address Book",
|
||||||
"i18n:govoplan-addresses.address_book_scopes.b0d0efde": "Address book scopes",
|
"i18n:govoplan-addresses.address_book_scopes.b0d0efde": "Address book scopes",
|
||||||
@@ -88,6 +115,33 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"Note": "Note"
|
"Note": "Note"
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
|
"i18n:govoplan-addresses.explorer.page_actions": "Adressbuchaktionen",
|
||||||
|
"i18n:govoplan-addresses.explorer.transfer": "Import / Export",
|
||||||
|
"i18n:govoplan-addresses.explorer.connections": "Verbindungen",
|
||||||
|
"i18n:govoplan-addresses.explorer.quality": "Adressqualität",
|
||||||
|
"i18n:govoplan-addresses.explorer.manage": "Verwalten",
|
||||||
|
"i18n:govoplan-addresses.explorer.manage_selection": "Ausgewähltes Adressbuch oder Liste verwalten",
|
||||||
|
"i18n:govoplan-addresses.explorer.no_book": "Wählen Sie zuerst ein Adressbuch aus.",
|
||||||
|
"i18n:govoplan-addresses.explorer.choose_book_in_group": "Wählen Sie ein Adressbuch in dieser Gruppe aus. Das Ordnersymbol klappt auf oder zu; ein Klick auf die Beschriftung wählt nur aus.",
|
||||||
|
"i18n:govoplan-addresses.explorer.archive_section": "Ausgewähltes Adressbuch oder Liste archivieren",
|
||||||
|
"i18n:govoplan-addresses.explorer.import_section": "In das ausgewählte Adressbuch importieren",
|
||||||
|
"i18n:govoplan-addresses.explorer.export_section": "Ausgewähltes Adressbuch oder Liste exportieren",
|
||||||
|
"i18n:govoplan-addresses.explorer.vcard_version": "vCard-Exportversion",
|
||||||
|
"i18n:govoplan-addresses.explorer.export_list": "Adressliste exportieren",
|
||||||
|
"i18n:govoplan-addresses.explorer.export_book": "Adressbuch exportieren",
|
||||||
|
"i18n:govoplan-addresses.explorer.connect_section": "Eine Adressquelle verbinden",
|
||||||
|
"i18n:govoplan-addresses.explorer.sync_section": "Synchronisierung des ausgewählten Adressbuchs",
|
||||||
|
"i18n:govoplan-addresses.explorer.no_sync_source": "Das ausgewählte Adressbuch hat keine verbundene Synchronisierungsquelle.",
|
||||||
|
"Restore address list": "Adressliste wiederherstellen",
|
||||||
|
"Restore address book": "Adressbuch wiederherstellen",
|
||||||
|
"Delete address list": "Adressliste löschen",
|
||||||
|
"Delete address book": "Adressbuch löschen",
|
||||||
|
"Import contacts": "Kontakte importieren",
|
||||||
|
"Connect CardDAV": "CardDAV verbinden",
|
||||||
|
"Connect LDAP or Active Directory": "LDAP oder Active Directory verbinden",
|
||||||
|
"Inspect sync source": "Synchronisierungsquelle prüfen",
|
||||||
|
"Preview sync": "Synchronisierungsvorschau",
|
||||||
|
"Run sync": "Synchronisierung starten",
|
||||||
"i18n:govoplan-addresses.add_contact.6da0b4b8": "Kontakt hinzufügen",
|
"i18n:govoplan-addresses.add_contact.6da0b4b8": "Kontakt hinzufügen",
|
||||||
"i18n:govoplan-addresses.address_book.f6327f59": "Adressbuch",
|
"i18n:govoplan-addresses.address_book.f6327f59": "Adressbuch",
|
||||||
"i18n:govoplan-addresses.address_book_scopes.b0d0efde": "Adressbuch-Bereiche",
|
"i18n:govoplan-addresses.address_book_scopes.b0d0efde": "Adressbuch-Bereiche",
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.address-book-page.address-book-fullscreen {
|
.address-book-page.address-book-fullscreen {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-rows: 1fr;
|
flex-direction: column;
|
||||||
height: calc(100vh - 115px);
|
height: calc(100vh - 115px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -43,7 +43,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.address-workspace-frame {
|
.address-workspace-frame {
|
||||||
height: 100%;
|
flex: 1 1 auto;
|
||||||
|
height: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,22 +74,10 @@
|
|||||||
background: var(--panel-header);
|
background: var(--panel-header);
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-tree-header .button-row {
|
.address-page-actions {
|
||||||
flex-wrap: wrap;
|
border-bottom: var(--border-line);
|
||||||
}
|
flex: 0 0 auto;
|
||||||
|
padding: var(--space-3);
|
||||||
.address-icon-actions {
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-icon-actions .btn {
|
|
||||||
align-items: center;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 30px;
|
|
||||||
padding: 0;
|
|
||||||
width: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-tree-filter-row {
|
.address-tree-filter-row {
|
||||||
|
|||||||
Reference in New Issue
Block a user