@vertebr/react 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/admin/AdminContent.d.ts +5 -1
- package/dist/components/admin/AdminContent.d.ts.map +1 -1
- package/dist/components/admin/AdminContent.js +75 -50
- package/dist/components/admin/AdminSyncPage.d.ts.map +1 -1
- package/dist/components/admin/AdminSyncPage.js +202 -3
- package/dist/components/admin/AuditBoard.d.ts +6 -2
- package/dist/components/admin/AuditBoard.d.ts.map +1 -1
- package/dist/components/admin/AuditBoard.js +145 -3
- package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
- package/dist/components/admin/VertebrAdmin.js +14 -3
- package/dist/components/config/EnumInput.d.ts.map +1 -1
- package/dist/components/config/EnumInput.js +9 -2
- package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
- package/dist/components/config/form/ConfigForm.js +14 -2
- package/dist/components/dialog/SelectionTableDialog.d.ts +79 -0
- package/dist/components/dialog/SelectionTableDialog.d.ts.map +1 -0
- package/dist/components/dialog/SelectionTableDialog.js +177 -0
- package/dist/components/emails/EmailsActivityPanel.d.ts.map +1 -1
- package/dist/components/emails/EmailsActivityPanel.js +80 -2
- package/dist/components/metrics/MetricsBoard.d.ts.map +1 -1
- package/dist/components/metrics/MetricsBoard.js +7 -1
- package/dist/components/resources/ColumnVisibilityMenu.d.ts +4 -0
- package/dist/components/resources/ColumnVisibilityMenu.d.ts.map +1 -1
- package/dist/components/resources/ColumnVisibilityMenu.js +14 -7
- package/dist/components/resources/ResourceTable.d.ts +7 -1
- package/dist/components/resources/ResourceTable.d.ts.map +1 -1
- package/dist/components/resources/ResourceTable.js +84 -64
- package/dist/components/resources/SortableHeader.d.ts +9 -0
- package/dist/components/resources/SortableHeader.d.ts.map +1 -0
- package/dist/components/resources/SortableHeader.js +9 -0
- package/dist/components/table/TableFilters.d.ts +20 -0
- package/dist/components/table/TableFilters.d.ts.map +1 -0
- package/dist/components/table/TableFilters.js +55 -0
- package/dist/components/table/TableSearch.d.ts +11 -0
- package/dist/components/table/TableSearch.d.ts.map +1 -0
- package/dist/components/table/TableSearch.js +23 -0
- package/dist/components/table/TableSkeleton.d.ts +16 -0
- package/dist/components/table/TableSkeleton.d.ts.map +1 -0
- package/dist/components/table/TableSkeleton.js +29 -0
- package/dist/components/table/TableWrap.d.ts +23 -0
- package/dist/components/table/TableWrap.d.ts.map +1 -0
- package/dist/components/table/TableWrap.js +35 -0
- package/dist/components/table/filterModel.d.ts +35 -0
- package/dist/components/table/filterModel.d.ts.map +1 -0
- package/dist/components/table/filterModel.js +150 -0
- package/dist/components/table/index.d.ts +13 -0
- package/dist/components/table/index.d.ts.map +1 -0
- package/dist/components/table/index.js +6 -0
- package/dist/components/table/useTableOverflow.d.ts +17 -0
- package/dist/components/table/useTableOverflow.d.ts.map +1 -0
- package/dist/components/table/useTableOverflow.js +54 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/lib/sort.d.ts +10 -0
- package/dist/lib/sort.d.ts.map +1 -0
- package/dist/lib/sort.js +59 -0
- package/dist/styles/audit.css +11 -1
- package/dist/styles/columns.css +1 -84
- package/dist/styles/index.css +1 -0
- package/dist/styles/metrics.css +27 -4
- package/dist/styles/panel.css +143 -0
- package/dist/styles/select.css +16 -0
- package/dist/styles/selection-dialog.css +170 -0
- package/dist/styles/sync.css +21 -1
- package/dist/styles/table.css +115 -2
- package/package.json +1 -1
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { SortableHeader } from "../resources/SortableHeader";
|
|
4
|
+
import { TableFilters, TableScrollControls, TableSearch, TableSkeleton, TableWrap, matchesTableFilters, matchesTableQuery, resolveAuditFilter, useTableOverflow, } from "../table";
|
|
5
|
+
import { compareFieldValues, nextTableSort, } from "../../lib/sort";
|
|
6
|
+
/** Fallback when catalog omits columns — keep in sync with `defaultAuditColumns`. */
|
|
7
|
+
const DEFAULT_COLUMNS = [
|
|
8
|
+
{ id: "at", label: "When", path: "at", type: "datetime" },
|
|
9
|
+
{ id: "actor", label: "Email", path: "actor" },
|
|
10
|
+
{
|
|
11
|
+
id: "action",
|
|
12
|
+
label: "Action",
|
|
13
|
+
path: "action",
|
|
14
|
+
filter: {
|
|
15
|
+
options: [
|
|
16
|
+
"config.set",
|
|
17
|
+
"config.publish",
|
|
18
|
+
"config.revision.save",
|
|
19
|
+
"config.revision.publish",
|
|
20
|
+
"config.revision.restore",
|
|
21
|
+
"addon.publish",
|
|
22
|
+
"resource.create",
|
|
23
|
+
"resource.update",
|
|
24
|
+
"resource.delete",
|
|
25
|
+
"sync.run",
|
|
26
|
+
],
|
|
27
|
+
optionLabels: {
|
|
28
|
+
"config.set": "Config set",
|
|
29
|
+
"config.publish": "Config publish",
|
|
30
|
+
"config.revision.save": "Revision save",
|
|
31
|
+
"config.revision.publish": "Revision publish",
|
|
32
|
+
"config.revision.restore": "Revision restore",
|
|
33
|
+
"addon.publish": "Addon publish",
|
|
34
|
+
"resource.create": "Resource create",
|
|
35
|
+
"resource.update": "Resource update",
|
|
36
|
+
"resource.delete": "Resource delete",
|
|
37
|
+
"sync.run": "Sync run",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{ id: "target", label: "Target", path: "target" },
|
|
42
|
+
{ id: "summary", label: "Summary", path: "summary" },
|
|
43
|
+
];
|
|
3
44
|
function formatWhen(at) {
|
|
4
45
|
const date = new Date(at);
|
|
5
46
|
if (Number.isNaN(date.getTime()))
|
|
@@ -11,10 +52,72 @@ function formatWhen(at) {
|
|
|
11
52
|
minute: "2-digit",
|
|
12
53
|
});
|
|
13
54
|
}
|
|
14
|
-
|
|
55
|
+
function formatCellDisplay(value, column) {
|
|
56
|
+
if (value == null || value === "")
|
|
57
|
+
return "—";
|
|
58
|
+
if (column.type === "datetime" || column.path === "at") {
|
|
59
|
+
return formatWhen(String(value));
|
|
60
|
+
}
|
|
61
|
+
if (column.type === "boolean") {
|
|
62
|
+
return value ? "Yes" : "No";
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === "object") {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(value);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return String(value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return String(value);
|
|
73
|
+
}
|
|
74
|
+
/** Resolve a column path against an audit entry (`meta.x`, `target`, or field). */
|
|
75
|
+
export function resolveAuditColumnValue(entry, path) {
|
|
76
|
+
if (path === "target") {
|
|
77
|
+
return `${entry.targetType}${entry.targetId ? ` · ${entry.targetId}` : ""}`;
|
|
78
|
+
}
|
|
79
|
+
if (path.startsWith("meta.")) {
|
|
80
|
+
const key = path.slice("meta.".length);
|
|
81
|
+
return entry.meta?.[key];
|
|
82
|
+
}
|
|
83
|
+
return entry[path];
|
|
84
|
+
}
|
|
85
|
+
function columnSortType(column) {
|
|
86
|
+
if (column.type === "datetime" || column.path === "at")
|
|
87
|
+
return "datetime";
|
|
88
|
+
if (column.type === "number")
|
|
89
|
+
return "number";
|
|
90
|
+
return "string";
|
|
91
|
+
}
|
|
92
|
+
function AuditCell({ entry, column, }) {
|
|
93
|
+
const value = resolveAuditColumnValue(entry, column.path);
|
|
94
|
+
const display = formatCellDisplay(value, column);
|
|
95
|
+
if (column.path === "at" || column.type === "datetime") {
|
|
96
|
+
return _jsx("td", { className: "vertebr-audit-when", children: display });
|
|
97
|
+
}
|
|
98
|
+
if (column.path === "actor") {
|
|
99
|
+
return _jsx("td", { className: "vertebr-audit-actor", children: display });
|
|
100
|
+
}
|
|
101
|
+
if (column.path === "action") {
|
|
102
|
+
return (_jsx("td", { children: display === "—" ? (display) : (_jsx("code", { className: "vertebr-audit-action", children: display })) }));
|
|
103
|
+
}
|
|
104
|
+
if (column.path === "target" || column.path === "targetType") {
|
|
105
|
+
return (_jsx("td", { children: _jsx("span", { className: "vertebr-audit-target", children: display }) }));
|
|
106
|
+
}
|
|
107
|
+
return _jsx("td", { children: display });
|
|
108
|
+
}
|
|
109
|
+
export function AuditBoard({ client, label = "Audit", columns: columnsProp, }) {
|
|
110
|
+
const columns = columnsProp && columnsProp.length > 0 ? columnsProp : DEFAULT_COLUMNS;
|
|
15
111
|
const [items, setItems] = useState([]);
|
|
16
112
|
const [loading, setLoading] = useState(true);
|
|
17
113
|
const [error, setError] = useState(null);
|
|
114
|
+
const [query, setQuery] = useState("");
|
|
115
|
+
const [filters, setFilters] = useState({});
|
|
116
|
+
const [sort, setSort] = useState(() => ({
|
|
117
|
+
field: columns.find((column) => column.path === "at")?.id ?? columns[0].id,
|
|
118
|
+
order: "desc",
|
|
119
|
+
}));
|
|
120
|
+
const tableOverflow = useTableOverflow(items.length);
|
|
18
121
|
useEffect(() => {
|
|
19
122
|
let cancelled = false;
|
|
20
123
|
(async () => {
|
|
@@ -39,5 +142,44 @@ export function AuditBoard({ client, label = "Audit" }) {
|
|
|
39
142
|
cancelled = true;
|
|
40
143
|
};
|
|
41
144
|
}, [client]);
|
|
42
|
-
|
|
145
|
+
const filterColumns = useMemo(() => columns
|
|
146
|
+
.map((column) => resolveAuditFilter(column.id, column.label, column.filter))
|
|
147
|
+
.filter((column) => Boolean(column)), [columns]);
|
|
148
|
+
const filterGetters = useMemo(() => Object.fromEntries(filterColumns.map((column) => {
|
|
149
|
+
const source = columns.find((item) => item.id === column.id);
|
|
150
|
+
return [
|
|
151
|
+
column.id,
|
|
152
|
+
(entry) => resolveAuditColumnValue(entry, source?.path ?? column.id),
|
|
153
|
+
];
|
|
154
|
+
})), [filterColumns, columns]);
|
|
155
|
+
const searchedItems = useMemo(() => {
|
|
156
|
+
if (!query.trim())
|
|
157
|
+
return items;
|
|
158
|
+
return items.filter((entry) => columns.some((col) => matchesTableQuery(resolveAuditColumnValue(entry, col.path), query)));
|
|
159
|
+
}, [items, query, columns]);
|
|
160
|
+
const sortedItems = useMemo(() => {
|
|
161
|
+
const column = columns.find((item) => item.id === sort.field) ?? columns[0];
|
|
162
|
+
const filtered = searchedItems.filter((entry) => matchesTableFilters(entry, filters, filterGetters));
|
|
163
|
+
if (!column)
|
|
164
|
+
return filtered;
|
|
165
|
+
const type = columnSortType(column);
|
|
166
|
+
return [...filtered].sort((left, right) => compareFieldValues(resolveAuditColumnValue(left, column.path), resolveAuditColumnValue(right, column.path), type, sort.order));
|
|
167
|
+
}, [searchedItems, sort, columns, filters, filterGetters]);
|
|
168
|
+
return (_jsxs("div", { className: "vertebr-audit", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsxs("div", { className: "vertebr-panel-head-actions", children: [filterColumns.length > 0 ? (_jsx(TableFilters, { columns: filterColumns, selected: filters, onChange: setFilters })) : null, _jsx(TableSearch, { value: query, onChange: setQuery, "aria-label": "Search audit" }), _jsx(TableScrollControls, { canLeft: tableOverflow.canLeft, canRight: tableOverflow.canRight, onScroll: tableOverflow.scrollBy }), _jsx("span", { className: "vertebr-count", children: loading
|
|
169
|
+
? "…"
|
|
170
|
+
: sortedItems.length !== items.length
|
|
171
|
+
? `${sortedItems.length} of ${items.length}`
|
|
172
|
+
: `${items.length} events` })] })] }), error ? _jsx("div", { className: "vertebr-error", children: error }) : null, loading && items.length === 0 ? (_jsx(TableSkeleton, { columns: columns.map((column) => ({
|
|
173
|
+
id: column.id,
|
|
174
|
+
label: column.label,
|
|
175
|
+
})) })) : items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No audit events yet." })) : sortedItems.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No matching events." })) : (_jsx(TableWrap, { overflow: tableOverflow, transitionKey: JSON.stringify(filters), children: _jsxs("table", { className: "vertebr-table vertebr-audit-table", children: [_jsx("thead", { children: _jsx("tr", { children: columns.map((column) => {
|
|
176
|
+
const sortable = column.sortable !== false;
|
|
177
|
+
const active = sort.field === column.id;
|
|
178
|
+
if (!sortable) {
|
|
179
|
+
return _jsx("th", { children: column.label }, column.id);
|
|
180
|
+
}
|
|
181
|
+
return (_jsx("th", { children: _jsx(SortableHeader, { label: column.label, active: active, order: active ? sort.order : undefined, onSort: () => setSort(nextTableSort(sort, column.id, column.type === "datetime" || column.path === "at"
|
|
182
|
+
? "desc"
|
|
183
|
+
: "asc")) }) }, column.id));
|
|
184
|
+
}) }) }), _jsx("tbody", { children: sortedItems.map((entry) => (_jsx("tr", { children: columns.map((column) => (_jsx(AuditCell, { entry: entry, column: column }, column.id))) }, entry.id))) })] }) }))] }));
|
|
43
185
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../../../src/components/admin/VertebrAdmin.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../../../src/components/admin/VertebrAdmin.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,wBAAgB,YAAY,CAAC,EAC3B,OAAsB,EACtB,QAAQ,EAAE,YAAY,EACtB,KAAuB,EACvB,aAA6B,EAC7B,eAAiC,EACjC,eAAiC,EACjC,sBAAgD,EAChD,cAAc,EACd,aAAa,EACb,SAAS,EACT,YAAY,EACZ,gBAAgB,GACjB,EAAE,iBAAiB,+BA+yBnB"}
|
|
@@ -40,7 +40,9 @@ export function VertebrAdmin({ apiBase = "/admin/api", basePath: basePathProp, t
|
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
const [resourceData, setResourceData] = useState(null);
|
|
43
|
+
const [resourceLoading, setResourceLoading] = useState(false);
|
|
43
44
|
const [resourceSearch, setResourceSearch] = useState("");
|
|
45
|
+
const [resourceSort, setResourceSort] = useState(null);
|
|
44
46
|
const [configData, setConfigData] = useState(null);
|
|
45
47
|
const [metricsData, setMetricsData] = useState(null);
|
|
46
48
|
const [metricsLoading, setMetricsLoading] = useState(false);
|
|
@@ -213,8 +215,12 @@ export function VertebrAdmin({ apiBase = "/admin/api", basePath: basePathProp, t
|
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
217
|
else if (selection.kind === "resource") {
|
|
218
|
+
setResourceLoading(true);
|
|
216
219
|
const data = await client.listResource(selection.id, {
|
|
217
220
|
...(resourceSearch.trim() ? { q: resourceSearch.trim() } : {}),
|
|
221
|
+
...(resourceSort
|
|
222
|
+
? { sort: resourceSort.field, order: resourceSort.order }
|
|
223
|
+
: {}),
|
|
218
224
|
fresh: true,
|
|
219
225
|
});
|
|
220
226
|
if (!cancelled) {
|
|
@@ -272,16 +278,18 @@ export function VertebrAdmin({ apiBase = "/admin/api", basePath: basePathProp, t
|
|
|
272
278
|
setGalleryLoading(false);
|
|
273
279
|
setFontLibraryLoading(false);
|
|
274
280
|
setEmailsLoading(false);
|
|
281
|
+
setResourceLoading(false);
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
})();
|
|
278
285
|
return () => {
|
|
279
286
|
cancelled = true;
|
|
280
287
|
};
|
|
281
|
-
}, [client, selection, catalog, environment, metricsRange, resourceSearch]);
|
|
288
|
+
}, [client, selection, catalog, environment, metricsRange, resourceSearch, resourceSort]);
|
|
282
289
|
const resourceSelectionId = selection?.kind === "resource" ? selection.id : null;
|
|
283
290
|
useEffect(() => {
|
|
284
291
|
setResourceSearch("");
|
|
292
|
+
setResourceSort(null);
|
|
285
293
|
}, [resourceSelectionId]);
|
|
286
294
|
const refreshHealth = useCallback(async () => {
|
|
287
295
|
if (!catalog?.health) {
|
|
@@ -317,10 +325,13 @@ export function VertebrAdmin({ apiBase = "/admin/api", basePath: basePathProp, t
|
|
|
317
325
|
return;
|
|
318
326
|
const data = await client.listResource(selection.id, {
|
|
319
327
|
...(resourceSearch.trim() ? { q: resourceSearch.trim() } : {}),
|
|
328
|
+
...(resourceSort
|
|
329
|
+
? { sort: resourceSort.field, order: resourceSort.order }
|
|
330
|
+
: {}),
|
|
320
331
|
fresh: true,
|
|
321
332
|
});
|
|
322
333
|
setResourceData(data);
|
|
323
|
-
}, [client, selection, resourceSearch]);
|
|
334
|
+
}, [client, selection, resourceSearch, resourceSort]);
|
|
324
335
|
const refreshConfig = useCallback(async () => {
|
|
325
336
|
if (selection?.kind !== "config" && selection?.kind !== "addon")
|
|
326
337
|
return;
|
|
@@ -669,5 +680,5 @@ export function VertebrAdmin({ apiBase = "/admin/api", basePath: basePathProp, t
|
|
|
669
680
|
: selection?.kind === "addon"
|
|
670
681
|
? catalog?.addons?.find((item) => item.id === selection.id)?.description
|
|
671
682
|
: undefined;
|
|
672
|
-
return (_jsx(SaveBarProvider, { children: _jsx(ToastProvider, { children: _jsxs("div", { className: "vertebr-root", "data-env": environment, "data-sidebar-collapsed": sidebarCollapsed || undefined, children: [_jsx(AdminTopBar, { brand: bootstrap?.brand ?? catalog?.brand ?? "Vertebr", brandLogoUrl: brandLogoUrl, brandLogoUrlDark: brandLogoUrlDark, hostName: bootstrap?.appearance?.hostName ?? catalog?.appearance?.hostName, hostLogoUrl: bootstrap?.appearance?.hostLogoUrl ?? catalog?.appearance?.hostLogoUrl, envList: envList, environment: environment, onEnvironmentChange: switchEnvironment, authUserLabel: authUserLabel, onSignOut: onSignOut, getAccessToken: getAccessToken, authViaTokenOnly: authViaTokenOnly, authRequired: authRequired, token: token, tokenDraft: tokenDraft, onTokenDraftChange: setTokenDraft, onSaveToken: saveToken, onClearToken: clearToken, theme: theme, onThemeChange: setTheme, sidebarCollapsed: sidebarCollapsed, onToggleSidebar: () => setSidebarCollapsed((prev) => !prev), syncEnabled: syncEnabled, onOpenSync: () => setSelection({ kind: "sync" }), onOpenSearch: () => setSearchOpen(true), healthChecks: healthChecks, onRefreshHealth: catalog?.health ? refreshHealth : undefined }), catalog && searchOpen ? (_jsx(AdminCommandPalette, { catalog: catalog, onClose: () => setSearchOpen(false), onSelect: setSelection })) : null, _jsx(AdminSidebar, { navGroups: navGroups, collapsed: sidebarCollapsed }), _jsxs("main", { className: "vertebr-main", children: [_jsx(AdminHeader, { activeLabel: activeLabel ?? (catalog ? "Dashboard" : "Sign in"), catalogLoaded: Boolean(catalog), authRequired: authRequired, selection: selection, isPageSelection: isPageSelection, hasPagePreview: hasPagePreview, selectionDescription: selectionDescription, error: error, breadcrumbs: breadcrumbsForSelection(selection, catalog) }), _jsx(AdminContent, { client: client, catalog: catalog, selection: selection, onSelectionChange: setSelection, authRequired: authRequired, loading: loading, isPageSelection: isPageSelection, metricsData: metricsData, metricsLoading: metricsLoading, metricsRange: metricsRange, onMetricsRangeChange: changeMetricsRange, onRefreshMetrics: refreshMetrics, galleryData: galleryData, galleryLoading: galleryLoading, onRefreshGallery: refreshGallery, fontLibraryData: fontLibraryData, fontLibraryLoading: fontLibraryLoading, onRefreshFonts: refreshFonts, emailsData: emailsData, emailsLoading: emailsLoading, onRefreshEmails: refreshEmails, resourceData: resourceData, resourceSearch: resourceSearch, onResourceSearchChange: setResourceSearch, onRefreshResource: refreshResource, configData: configData, onConfigSaved: setConfigData, onRefreshConfig: refreshConfig, galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker }), _jsx(AdminSaveBar, {})] }), _jsx(AdminToastStack, {})] }) }) }));
|
|
683
|
+
return (_jsx(SaveBarProvider, { children: _jsx(ToastProvider, { children: _jsxs("div", { className: "vertebr-root", "data-env": environment, "data-sidebar-collapsed": sidebarCollapsed || undefined, children: [_jsx(AdminTopBar, { brand: bootstrap?.brand ?? catalog?.brand ?? "Vertebr", brandLogoUrl: brandLogoUrl, brandLogoUrlDark: brandLogoUrlDark, hostName: bootstrap?.appearance?.hostName ?? catalog?.appearance?.hostName, hostLogoUrl: bootstrap?.appearance?.hostLogoUrl ?? catalog?.appearance?.hostLogoUrl, envList: envList, environment: environment, onEnvironmentChange: switchEnvironment, authUserLabel: authUserLabel, onSignOut: onSignOut, getAccessToken: getAccessToken, authViaTokenOnly: authViaTokenOnly, authRequired: authRequired, token: token, tokenDraft: tokenDraft, onTokenDraftChange: setTokenDraft, onSaveToken: saveToken, onClearToken: clearToken, theme: theme, onThemeChange: setTheme, sidebarCollapsed: sidebarCollapsed, onToggleSidebar: () => setSidebarCollapsed((prev) => !prev), syncEnabled: syncEnabled, onOpenSync: () => setSelection({ kind: "sync" }), onOpenSearch: () => setSearchOpen(true), healthChecks: healthChecks, onRefreshHealth: catalog?.health ? refreshHealth : undefined }), catalog && searchOpen ? (_jsx(AdminCommandPalette, { catalog: catalog, onClose: () => setSearchOpen(false), onSelect: setSelection })) : null, _jsx(AdminSidebar, { navGroups: navGroups, collapsed: sidebarCollapsed }), _jsxs("main", { className: "vertebr-main", children: [_jsx(AdminHeader, { activeLabel: activeLabel ?? (catalog ? "Dashboard" : "Sign in"), catalogLoaded: Boolean(catalog), authRequired: authRequired, selection: selection, isPageSelection: isPageSelection, hasPagePreview: hasPagePreview, selectionDescription: selectionDescription, error: error, breadcrumbs: breadcrumbsForSelection(selection, catalog) }), _jsx(AdminContent, { client: client, catalog: catalog, selection: selection, onSelectionChange: setSelection, authRequired: authRequired, loading: loading, isPageSelection: isPageSelection, metricsData: metricsData, metricsLoading: metricsLoading, metricsRange: metricsRange, onMetricsRangeChange: changeMetricsRange, onRefreshMetrics: refreshMetrics, galleryData: galleryData, galleryLoading: galleryLoading, onRefreshGallery: refreshGallery, fontLibraryData: fontLibraryData, fontLibraryLoading: fontLibraryLoading, onRefreshFonts: refreshFonts, emailsData: emailsData, emailsLoading: emailsLoading, onRefreshEmails: refreshEmails, resourceData: resourceData, resourceLoading: resourceLoading, resourceSearch: resourceSearch, onResourceSearchChange: setResourceSearch, resourceSort: resourceSort, onResourceSortChange: setResourceSort, onRefreshResource: refreshResource, configData: configData, onConfigSaved: setConfigData, onRefreshConfig: refreshConfig, galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker }), _jsx(AdminSaveBar, {})] }), _jsx(AdminToastStack, {})] }) }) }));
|
|
673
684
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EnumInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/EnumInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAgB,SAAS,CAAC,EACxB,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,cAAc,+
|
|
1
|
+
{"version":3,"file":"EnumInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/EnumInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAgB,SAAS,CAAC,EACxB,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,cAAc,+BAsEhB"}
|
|
@@ -5,11 +5,18 @@ export function EnumInput({ id, value, field, readOnly, onChange, }) {
|
|
|
5
5
|
const baseOptions = field.options?.filter(Boolean) ?? [];
|
|
6
6
|
const raw = typeof value === "string" ? value : "";
|
|
7
7
|
const options = raw && !baseOptions.includes(raw) ? [raw, ...baseOptions] : baseOptions;
|
|
8
|
-
const
|
|
8
|
+
const allowClear = field.required !== true;
|
|
9
|
+
// Required enums still fall back to the first option when empty.
|
|
10
|
+
// Optional enums keep an empty value so links/CTAs can be cleared.
|
|
11
|
+
const current = raw && options.includes(raw)
|
|
12
|
+
? raw
|
|
13
|
+
: allowClear
|
|
14
|
+
? ""
|
|
15
|
+
: (options[0] ?? "");
|
|
9
16
|
if (options.length === 0) {
|
|
10
17
|
return (_jsx("input", { id: id, type: "text", readOnly: readOnly, value: current, disabled: true }));
|
|
11
18
|
}
|
|
12
|
-
return (_jsxs(Select.Root, { value: current, onValueChange: onChange, disabled: readOnly, children: [_jsxs(Select.Trigger, { id: id, className: "vertebr-select-trigger", children: [_jsx("span", { className: "vertebr-select-value", children: _jsx(Select.Value, {
|
|
19
|
+
return (_jsxs("div", { className: "vertebr-enum", children: [_jsxs(Select.Root, { value: current || undefined, onValueChange: onChange, disabled: readOnly, children: [_jsxs(Select.Trigger, { id: id, className: "vertebr-select-trigger", children: [_jsx("span", { className: "vertebr-select-value", children: _jsx(Select.Value, { placeholder: "None" }) }), _jsx(Select.Icon, { className: "vertebr-select-icon", children: _jsx(ChevronIcon, {}) })] }), _jsx(Select.Portal, { children: _jsx(Select.Content, { className: "vertebr-select-content", position: "popper", sideOffset: 6, children: _jsx(Select.Viewport, { className: "vertebr-select-viewport", children: options.map((option) => (_jsxs(Select.Item, { value: option, className: "vertebr-select-item", children: [_jsx(Select.ItemText, { children: optionLabel(option, field) }), _jsx(Select.ItemIndicator, { className: "vertebr-select-check", children: _jsx(CheckIcon, {}) })] }, option))) }) }) })] }), allowClear && current && !readOnly ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-enum-clear", onClick: () => onChange(""), children: "Clear" })) : null] }));
|
|
13
20
|
}
|
|
14
21
|
function ChevronIcon() {
|
|
15
22
|
return (_jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: _jsx("path", { d: "M2.5 4.5 6 8l3.5-3.5", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfigForm.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigForm.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAElE,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,KAAK,EACL,MAAM,EACN,WAAW,EACX,OAAkB,EAClB,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAe,EACf,gBAAwB,EACxB,SAAS,EACT,UAAkB,GACnB,EAAE,eAAe,+
|
|
1
|
+
{"version":3,"file":"ConfigForm.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigForm.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAElE,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,KAAK,EACL,MAAM,EACN,WAAW,EACX,OAAkB,EAClB,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAe,EACf,gBAAwB,EACxB,SAAS,EACT,UAAkB,GACnB,EAAE,eAAe,+BAqMjB"}
|
|
@@ -28,11 +28,23 @@ export function ConfigForm({ schema, value, onSave, submitLabel, variant = "conf
|
|
|
28
28
|
if (!onChange)
|
|
29
29
|
return;
|
|
30
30
|
const subscription = form.watch((values) => {
|
|
31
|
-
|
|
31
|
+
// Keep empty strings for optional fields so clears reach the host preview
|
|
32
|
+
// (react-hook-form can emit undefined for cleared optional inputs).
|
|
33
|
+
const next = { ...(values ?? {}) };
|
|
34
|
+
for (const [key, field] of Object.entries(editorSchema)) {
|
|
35
|
+
if (field.type !== "string" && field.type !== "richtext" && field.type !== "enum") {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (field.required)
|
|
39
|
+
continue;
|
|
40
|
+
if (next[key] == null)
|
|
41
|
+
next[key] = "";
|
|
42
|
+
}
|
|
43
|
+
onChange(next);
|
|
32
44
|
});
|
|
33
45
|
onChange((form.getValues() ?? {}));
|
|
34
46
|
return () => subscription.unsubscribe();
|
|
35
|
-
}, [form, onChange]);
|
|
47
|
+
}, [form, onChange, editorSchema]);
|
|
36
48
|
useEffect(() => {
|
|
37
49
|
form.reset(editorValue);
|
|
38
50
|
}, [editorValue, form]);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { AdminEnvironment } from "@vertebr/client";
|
|
3
|
+
import { type TableFilterColumn } from "../table";
|
|
4
|
+
/** One selectable row in {@link SelectionTableDialog}. */
|
|
5
|
+
export type SelectionTableRow = {
|
|
6
|
+
id: string;
|
|
7
|
+
/** Primary column (id / code / slug). */
|
|
8
|
+
primary: string;
|
|
9
|
+
/** Secondary column (name / offer / label). */
|
|
10
|
+
secondary?: string;
|
|
11
|
+
/** Env ids where this row is already present. */
|
|
12
|
+
environments?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Extra values for custom filter columns (keyed by filter id).
|
|
15
|
+
* Built-in `destination` uses `"missing" | "present"`.
|
|
16
|
+
*/
|
|
17
|
+
filterValues?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
export type SelectionTableColumnLabels = {
|
|
20
|
+
primary?: string;
|
|
21
|
+
secondary?: string;
|
|
22
|
+
environments?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Optional copy / behavior overrides for {@link SelectionTableDialog}.
|
|
26
|
+
* Omit a key to keep the default.
|
|
27
|
+
*/
|
|
28
|
+
export type SelectionTableDialogOverrides = {
|
|
29
|
+
eyebrow?: string;
|
|
30
|
+
columns?: SelectionTableColumnLabels;
|
|
31
|
+
/** Show the environments column + chips. @default true when `environments` is non-empty */
|
|
32
|
+
showEnvironments?: boolean;
|
|
33
|
+
showEnvironmentFilter?: boolean;
|
|
34
|
+
showDestinationFilter?: boolean;
|
|
35
|
+
showSelectShown?: boolean;
|
|
36
|
+
showScrollControls?: boolean;
|
|
37
|
+
showRemove?: boolean;
|
|
38
|
+
searchPlaceholder?: string;
|
|
39
|
+
emptyLabel?: string;
|
|
40
|
+
noMatchLabel?: string;
|
|
41
|
+
/** Footer meta when nothing is checked (sync-all semantics). */
|
|
42
|
+
allSelectedFooter?: string;
|
|
43
|
+
/** Footer meta when one or more rows are checked. */
|
|
44
|
+
selectedFooter?: (count: number) => string;
|
|
45
|
+
applyLabel?: string;
|
|
46
|
+
cancelLabel?: string;
|
|
47
|
+
removeLabel?: string;
|
|
48
|
+
/** Extra filter columns merged after built-in ones. */
|
|
49
|
+
extraFilters?: TableFilterColumn[];
|
|
50
|
+
/** Replace built-in filters entirely. */
|
|
51
|
+
filters?: TableFilterColumn[];
|
|
52
|
+
/** Custom cell render for the secondary column. */
|
|
53
|
+
renderSecondary?: (row: SelectionTableRow) => ReactNode;
|
|
54
|
+
/** Custom cell render for an extra column after environments. */
|
|
55
|
+
renderExtra?: (row: SelectionTableRow) => ReactNode;
|
|
56
|
+
extraColumnLabel?: string;
|
|
57
|
+
};
|
|
58
|
+
export type SelectionTableDialogProps = {
|
|
59
|
+
title: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
/** Shown next to filters, e.g. "Source: Qual". */
|
|
62
|
+
sourceLabel?: string;
|
|
63
|
+
rows: SelectionTableRow[];
|
|
64
|
+
/** Catalog environments — drives env chips + default filters. */
|
|
65
|
+
environments: AdminEnvironment[];
|
|
66
|
+
/** Env ids treated as destinations for the “missing / present” filter. */
|
|
67
|
+
destinationEnvIds?: string[];
|
|
68
|
+
selectedIds: string[];
|
|
69
|
+
overrides?: SelectionTableDialogOverrides;
|
|
70
|
+
onClose: () => void;
|
|
71
|
+
onApply: (ids: string[]) => void;
|
|
72
|
+
onRemove?: () => void;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Standard multi-select table dialog (search, filter chips, sort).
|
|
76
|
+
* Used by Sync target pickers; overrides let hosts tune copy and columns.
|
|
77
|
+
*/
|
|
78
|
+
export declare function SelectionTableDialog({ title, description, sourceLabel, rows, environments, destinationEnvIds, selectedIds, overrides, onClose, onApply, onRemove, }: SelectionTableDialogProps): import("react").JSX.Element;
|
|
79
|
+
//# sourceMappingURL=SelectionTableDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectionTableDialog.d.ts","sourceRoot":"","sources":["../../../src/components/dialog/SelectionTableDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,OAAO,EAQL,KAAK,iBAAiB,EAEvB,MAAM,UAAU,CAAC;AAOlB,0DAA0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,YAAY,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACnC,yCAAyC;IACzC,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC9B,mDAAmD;IACnD,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,SAAS,CAAC;IACxD,iEAAiE;IACjE,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,SAAS,CAAC;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,iEAAiE;IACjE,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,6BAA6B,CAAC;IAC1C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAkDF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,WAAW,EACX,WAAW,EACX,IAAI,EACJ,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,SAAc,EACd,OAAO,EACP,OAAO,EACP,QAAQ,GACT,EAAE,yBAAyB,+BA2Z3B"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { SortableHeader } from "../resources/SortableHeader";
|
|
4
|
+
import { TableFilters, TableScrollControls, TableSearch, TableWrap, matchesTableFilters, matchesTableQuery, useTableOverflow, } from "../table";
|
|
5
|
+
import { compareFieldValues, nextTableSort, } from "../../lib/sort";
|
|
6
|
+
function defaultDestinationIds(environments, destinationEnvIds, sourceLabel) {
|
|
7
|
+
if (destinationEnvIds?.length)
|
|
8
|
+
return destinationEnvIds;
|
|
9
|
+
// Best-effort: every env except one matching the source label text.
|
|
10
|
+
if (!sourceLabel)
|
|
11
|
+
return environments.map((env) => env.id).slice(1);
|
|
12
|
+
const source = environments.find((env) => env.label === sourceLabel ||
|
|
13
|
+
env.id === sourceLabel ||
|
|
14
|
+
sourceLabel.toLowerCase().includes(env.label.toLowerCase()));
|
|
15
|
+
return environments
|
|
16
|
+
.map((env) => env.id)
|
|
17
|
+
.filter((id) => id !== source?.id);
|
|
18
|
+
}
|
|
19
|
+
function EnvChips({ activeIds, columns, }) {
|
|
20
|
+
const active = new Set(activeIds);
|
|
21
|
+
return (_jsx("span", { className: "vertebr-env-chips", "aria-label": "Active environments", children: columns.map((env) => {
|
|
22
|
+
const on = active.has(env.id);
|
|
23
|
+
return (_jsxs("span", { className: "vertebr-env-chip", "data-on": on ? "true" : "false", "data-tone": env.tone ?? "neutral", title: on ? `Active in ${env.label}` : `Not in ${env.label}`, children: [_jsx("span", { className: "vertebr-env-chip-dot", "aria-hidden": "true" }), env.label] }, env.id));
|
|
24
|
+
}) }));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Standard multi-select table dialog (search, filter chips, sort).
|
|
28
|
+
* Used by Sync target pickers; overrides let hosts tune copy and columns.
|
|
29
|
+
*/
|
|
30
|
+
export function SelectionTableDialog({ title, description, sourceLabel, rows, environments, destinationEnvIds, selectedIds, overrides = {}, onClose, onApply, onRemove, }) {
|
|
31
|
+
const { eyebrow = "Selection", columns: columnLabels = {}, showEnvironments = environments.length > 0, showEnvironmentFilter = true, showDestinationFilter = true, showSelectShown = true, showScrollControls = false, showRemove = Boolean(onRemove), searchPlaceholder = "Search…", emptyLabel = "Nothing available.", noMatchLabel = "No matching rows.", allSelectedFooter = "All items", selectedFooter = (count) => `${count} selected`, applyLabel = "Apply", cancelLabel = "Cancel", removeLabel = "Remove", extraFilters, filters: filtersOverride, renderSecondary, renderExtra, extraColumnLabel, } = overrides;
|
|
32
|
+
const primaryLabel = columnLabels.primary ?? "Id";
|
|
33
|
+
const secondaryLabel = columnLabels.secondary ?? "Name";
|
|
34
|
+
const environmentsLabel = columnLabels.environments ?? "Environments";
|
|
35
|
+
const destIds = useMemo(() => defaultDestinationIds(environments, destinationEnvIds, sourceLabel), [environments, destinationEnvIds, sourceLabel]);
|
|
36
|
+
const normalizedRows = useMemo(() => {
|
|
37
|
+
return rows.map((row) => {
|
|
38
|
+
const activeEnvs = row.environments ?? [];
|
|
39
|
+
const missingOnDest = destIds.length > 0 &&
|
|
40
|
+
destIds.some((envId) => !activeEnvs.includes(envId));
|
|
41
|
+
const destination = String(row.filterValues?.destination ?? "") ||
|
|
42
|
+
(missingOnDest ? "missing" : "present");
|
|
43
|
+
const envSortKey = activeEnvs
|
|
44
|
+
.map((envId) => environments.find((env) => env.id === envId)?.label ?? envId)
|
|
45
|
+
.join(", ");
|
|
46
|
+
return {
|
|
47
|
+
...row,
|
|
48
|
+
environments: activeEnvs,
|
|
49
|
+
destination,
|
|
50
|
+
envSortKey,
|
|
51
|
+
filterValues: {
|
|
52
|
+
...(row.filterValues ?? {}),
|
|
53
|
+
destination,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}, [rows, destIds, environments]);
|
|
58
|
+
const [draft, setDraft] = useState(() => [...selectedIds]);
|
|
59
|
+
const [query, setQuery] = useState("");
|
|
60
|
+
const [filters, setFilters] = useState({});
|
|
61
|
+
const [sort, setSort] = useState({
|
|
62
|
+
field: "primary",
|
|
63
|
+
order: "asc",
|
|
64
|
+
});
|
|
65
|
+
const tableOverflow = useTableOverflow(normalizedRows.length);
|
|
66
|
+
const filterColumns = useMemo(() => {
|
|
67
|
+
if (filtersOverride)
|
|
68
|
+
return filtersOverride;
|
|
69
|
+
const cols = [];
|
|
70
|
+
if (showEnvironmentFilter && environments.length > 0) {
|
|
71
|
+
cols.push({
|
|
72
|
+
id: "environments",
|
|
73
|
+
label: "Environment",
|
|
74
|
+
multiselect: true,
|
|
75
|
+
options: environments.map((env) => ({
|
|
76
|
+
value: env.id,
|
|
77
|
+
label: env.label,
|
|
78
|
+
})),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (showDestinationFilter && destIds.length > 0) {
|
|
82
|
+
cols.push({
|
|
83
|
+
id: "destination",
|
|
84
|
+
label: "Destination",
|
|
85
|
+
multiselect: true,
|
|
86
|
+
options: [
|
|
87
|
+
{ value: "missing", label: "Missing on destination" },
|
|
88
|
+
{ value: "present", label: "Already on destination" },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (extraFilters?.length)
|
|
93
|
+
cols.push(...extraFilters);
|
|
94
|
+
return cols;
|
|
95
|
+
}, [
|
|
96
|
+
filtersOverride,
|
|
97
|
+
showEnvironmentFilter,
|
|
98
|
+
showDestinationFilter,
|
|
99
|
+
environments,
|
|
100
|
+
destIds.length,
|
|
101
|
+
extraFilters,
|
|
102
|
+
]);
|
|
103
|
+
const filterGetters = useMemo(() => ({
|
|
104
|
+
environments: (row) => row.environments ?? [],
|
|
105
|
+
destination: (row) => row.filterValues?.destination ?? row.destination,
|
|
106
|
+
}), []);
|
|
107
|
+
const visibleRows = useMemo(() => {
|
|
108
|
+
const searched = query.trim()
|
|
109
|
+
? normalizedRows.filter((row) => matchesTableQuery(row.primary, query) ||
|
|
110
|
+
matchesTableQuery(row.secondary, query) ||
|
|
111
|
+
matchesTableQuery(row.envSortKey, query))
|
|
112
|
+
: normalizedRows;
|
|
113
|
+
const filtered = searched.filter((row) => matchesTableFilters(row, filters, {
|
|
114
|
+
...filterGetters,
|
|
115
|
+
...Object.fromEntries((extraFilters ?? []).map((column) => [
|
|
116
|
+
column.id,
|
|
117
|
+
(item) => item.filterValues?.[column.id],
|
|
118
|
+
])),
|
|
119
|
+
}));
|
|
120
|
+
return [...filtered].sort((left, right) => {
|
|
121
|
+
if (sort.field === "primary") {
|
|
122
|
+
return compareFieldValues(left.primary, right.primary, "string", sort.order);
|
|
123
|
+
}
|
|
124
|
+
if (sort.field === "secondary") {
|
|
125
|
+
return compareFieldValues(left.secondary, right.secondary, "string", sort.order);
|
|
126
|
+
}
|
|
127
|
+
if (sort.field === "environments") {
|
|
128
|
+
return compareFieldValues(left.envSortKey, right.envSortKey, "string", sort.order);
|
|
129
|
+
}
|
|
130
|
+
return 0;
|
|
131
|
+
});
|
|
132
|
+
}, [
|
|
133
|
+
normalizedRows,
|
|
134
|
+
query,
|
|
135
|
+
filters,
|
|
136
|
+
filterGetters,
|
|
137
|
+
extraFilters,
|
|
138
|
+
sort,
|
|
139
|
+
]);
|
|
140
|
+
const allVisibleSelected = visibleRows.length > 0 &&
|
|
141
|
+
visibleRows.every((row) => draft.includes(row.id));
|
|
142
|
+
const toggle = (id) => {
|
|
143
|
+
setDraft((current) => current.includes(id)
|
|
144
|
+
? current.filter((item) => item !== id)
|
|
145
|
+
: [...current, id]);
|
|
146
|
+
};
|
|
147
|
+
const toggleAllVisible = () => {
|
|
148
|
+
setDraft((current) => {
|
|
149
|
+
if (allVisibleSelected) {
|
|
150
|
+
const drop = new Set(visibleRows.map((row) => row.id));
|
|
151
|
+
return current.filter((id) => !drop.has(id));
|
|
152
|
+
}
|
|
153
|
+
const next = new Set(current);
|
|
154
|
+
for (const row of visibleRows)
|
|
155
|
+
next.add(row.id);
|
|
156
|
+
return [...next];
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const sortHeader = (field, label, preferredFirst = "asc") => {
|
|
160
|
+
const active = sort.field === field;
|
|
161
|
+
return (_jsx("th", { children: _jsx(SortableHeader, { label: label, active: active, order: active ? sort.order : undefined, onSort: () => setSort(nextTableSort(sort, field, preferredFirst)) }) }));
|
|
162
|
+
};
|
|
163
|
+
return (_jsx("div", { className: "vertebr-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "vertebr-modal vertebr-selection-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "vertebr-selection-dialog-title", onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "vertebr-modal-head vertebr-selection-dialog-head", children: [_jsxs("div", { className: "vertebr-modal-heading", children: [eyebrow ? (_jsx("p", { className: "vertebr-modal-eyebrow", children: eyebrow })) : null, _jsx("h2", { id: "vertebr-selection-dialog-title", children: title }), description ? (_jsx("p", { className: "vertebr-selection-dialog-sub", children: description })) : null] }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), _jsxs("div", { className: "vertebr-selection-dialog-body", children: [_jsxs("div", { className: "vertebr-table-toolbar vertebr-selection-dialog-toolbar", children: [_jsxs("div", { className: "vertebr-table-toolbar-start", children: [sourceLabel ? (_jsx("span", { className: "vertebr-selection-dialog-source", children: sourceLabel.startsWith("Source")
|
|
164
|
+
? sourceLabel
|
|
165
|
+
: `Source: ${sourceLabel}` })) : null, filterColumns.length > 0 ? (_jsx(TableFilters, { columns: filterColumns, selected: filters, onChange: setFilters })) : null] }), _jsxs("div", { className: "vertebr-selection-dialog-toolbar-end", children: [_jsx(TableSearch, { value: query, onChange: setQuery, "aria-label": searchPlaceholder, placeholder: searchPlaceholder }), showSelectShown ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: visibleRows.length === 0, onClick: toggleAllVisible, children: allVisibleSelected ? "Clear shown" : "Select shown" })) : null, showScrollControls ? (_jsx(TableScrollControls, { canLeft: tableOverflow.canLeft, canRight: tableOverflow.canRight, onScroll: tableOverflow.scrollBy })) : null, _jsx("span", { className: "vertebr-count", children: visibleRows.length !== normalizedRows.length
|
|
166
|
+
? `${visibleRows.length} of ${normalizedRows.length}`
|
|
167
|
+
: normalizedRows.length })] })] }), normalizedRows.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: emptyLabel })) : visibleRows.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: noMatchLabel })) : (_jsx(TableWrap, { overflow: tableOverflow, transitionKey: JSON.stringify(filters), children: _jsxs("table", { className: "vertebr-table vertebr-selection-dialog-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: "vertebr-selection-dialog-col-check", children: _jsx("span", { className: "vertebr-sr-only", children: "Select" }) }), sortHeader("primary", primaryLabel), sortHeader("secondary", secondaryLabel), showEnvironments
|
|
168
|
+
? sortHeader("environments", environmentsLabel)
|
|
169
|
+
: null, renderExtra ? (_jsx("th", { children: extraColumnLabel ?? "" })) : null] }) }), _jsx("tbody", { children: visibleRows.map((row) => {
|
|
170
|
+
const on = draft.includes(row.id);
|
|
171
|
+
return (_jsxs("tr", { "data-selected": on || undefined, onClick: () => toggle(row.id), children: [_jsx("td", { className: "vertebr-selection-dialog-col-check", children: _jsx("input", { type: "checkbox", checked: on, onChange: () => toggle(row.id), onClick: (event) => event.stopPropagation(), "aria-label": `Select ${row.primary}` }) }), _jsx("td", { children: _jsx("code", { className: "vertebr-selection-dialog-primary", children: row.primary }) }), _jsx("td", { children: renderSecondary
|
|
172
|
+
? renderSecondary(row)
|
|
173
|
+
: row.secondary || "—" }), showEnvironments ? (_jsx("td", { children: _jsx(EnvChips, { activeIds: row.environments ?? [], columns: environments }) })) : null, renderExtra ? _jsx("td", { children: renderExtra(row) }) : null] }, row.id));
|
|
174
|
+
}) })] }) }))] }), _jsxs("footer", { className: "vertebr-modal-foot vertebr-selection-dialog-foot", children: [_jsx("div", { className: "vertebr-selection-dialog-foot-meta", children: draft.length === 0
|
|
175
|
+
? allSelectedFooter
|
|
176
|
+
: selectedFooter(draft.length) }), _jsxs("div", { className: "vertebr-selection-dialog-foot-actions", children: [showRemove && onRemove ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: onRemove, children: removeLabel })) : null, _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: onClose, children: cancelLabel }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", onClick: () => onApply(draft), children: applyLabel })] })] })] }) }));
|
|
177
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailsActivityPanel.d.ts","sourceRoot":"","sources":["../../../src/components/emails/EmailsActivityPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"EmailsActivityPanel.d.ts","sourceRoot":"","sources":["../../../src/components/emails/EmailsActivityPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAetE,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAgIF,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,OAAO,EACP,WAAW,EACX,UAAU,EACV,IAAI,EACJ,QAAQ,GACT,EAAE,wBAAwB,+BAyR1B"}
|