@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.
Files changed (67) hide show
  1. package/dist/components/admin/AdminContent.d.ts +5 -1
  2. package/dist/components/admin/AdminContent.d.ts.map +1 -1
  3. package/dist/components/admin/AdminContent.js +75 -50
  4. package/dist/components/admin/AdminSyncPage.d.ts.map +1 -1
  5. package/dist/components/admin/AdminSyncPage.js +202 -3
  6. package/dist/components/admin/AuditBoard.d.ts +6 -2
  7. package/dist/components/admin/AuditBoard.d.ts.map +1 -1
  8. package/dist/components/admin/AuditBoard.js +145 -3
  9. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  10. package/dist/components/admin/VertebrAdmin.js +14 -3
  11. package/dist/components/config/EnumInput.d.ts.map +1 -1
  12. package/dist/components/config/EnumInput.js +9 -2
  13. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  14. package/dist/components/config/form/ConfigForm.js +14 -2
  15. package/dist/components/dialog/SelectionTableDialog.d.ts +79 -0
  16. package/dist/components/dialog/SelectionTableDialog.d.ts.map +1 -0
  17. package/dist/components/dialog/SelectionTableDialog.js +177 -0
  18. package/dist/components/emails/EmailsActivityPanel.d.ts.map +1 -1
  19. package/dist/components/emails/EmailsActivityPanel.js +80 -2
  20. package/dist/components/metrics/MetricsBoard.d.ts.map +1 -1
  21. package/dist/components/metrics/MetricsBoard.js +7 -1
  22. package/dist/components/resources/ColumnVisibilityMenu.d.ts +4 -0
  23. package/dist/components/resources/ColumnVisibilityMenu.d.ts.map +1 -1
  24. package/dist/components/resources/ColumnVisibilityMenu.js +14 -7
  25. package/dist/components/resources/ResourceTable.d.ts +7 -1
  26. package/dist/components/resources/ResourceTable.d.ts.map +1 -1
  27. package/dist/components/resources/ResourceTable.js +84 -64
  28. package/dist/components/resources/SortableHeader.d.ts +9 -0
  29. package/dist/components/resources/SortableHeader.d.ts.map +1 -0
  30. package/dist/components/resources/SortableHeader.js +9 -0
  31. package/dist/components/table/TableFilters.d.ts +20 -0
  32. package/dist/components/table/TableFilters.d.ts.map +1 -0
  33. package/dist/components/table/TableFilters.js +55 -0
  34. package/dist/components/table/TableSearch.d.ts +11 -0
  35. package/dist/components/table/TableSearch.d.ts.map +1 -0
  36. package/dist/components/table/TableSearch.js +23 -0
  37. package/dist/components/table/TableSkeleton.d.ts +16 -0
  38. package/dist/components/table/TableSkeleton.d.ts.map +1 -0
  39. package/dist/components/table/TableSkeleton.js +29 -0
  40. package/dist/components/table/TableWrap.d.ts +23 -0
  41. package/dist/components/table/TableWrap.d.ts.map +1 -0
  42. package/dist/components/table/TableWrap.js +35 -0
  43. package/dist/components/table/filterModel.d.ts +35 -0
  44. package/dist/components/table/filterModel.d.ts.map +1 -0
  45. package/dist/components/table/filterModel.js +150 -0
  46. package/dist/components/table/index.d.ts +13 -0
  47. package/dist/components/table/index.d.ts.map +1 -0
  48. package/dist/components/table/index.js +6 -0
  49. package/dist/components/table/useTableOverflow.d.ts +17 -0
  50. package/dist/components/table/useTableOverflow.d.ts.map +1 -0
  51. package/dist/components/table/useTableOverflow.js +54 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -0
  55. package/dist/lib/sort.d.ts +10 -0
  56. package/dist/lib/sort.d.ts.map +1 -0
  57. package/dist/lib/sort.js +59 -0
  58. package/dist/styles/audit.css +11 -1
  59. package/dist/styles/columns.css +1 -84
  60. package/dist/styles/index.css +1 -0
  61. package/dist/styles/metrics.css +27 -4
  62. package/dist/styles/panel.css +143 -0
  63. package/dist/styles/select.css +16 -0
  64. package/dist/styles/selection-dialog.css +170 -0
  65. package/dist/styles/sync.css +21 -1
  66. package/dist/styles/table.css +115 -2
  67. 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
- export function AuditBoard({ client, label = "Audit" }) {
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
- return (_jsxs("div", { className: "vertebr-audit", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsx("span", { className: "vertebr-count", children: loading ? "…" : `${items.length} events` })] }), error ? _jsx("div", { className: "vertebr-error", children: error }) : null, loading && items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "Loading audit log\u2026" })) : items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No audit events yet." })) : (_jsx("div", { className: "vertebr-table-wrap", children: _jsxs("table", { className: "vertebr-table vertebr-audit-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "When" }), _jsx("th", { children: "Actor" }), _jsx("th", { children: "Action" }), _jsx("th", { children: "Target" }), _jsx("th", { children: "Summary" })] }) }), _jsx("tbody", { children: items.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "vertebr-audit-when", children: formatWhen(entry.at) }), _jsx("td", { children: entry.actor ?? "—" }), _jsx("td", { children: _jsx("code", { className: "vertebr-audit-action", children: entry.action }) }), _jsx("td", { children: _jsxs("span", { className: "vertebr-audit-target", children: [entry.targetType, entry.targetId ? ` · ${entry.targetId}` : ""] }) }), _jsx("td", { children: entry.summary })] }, entry.id))) })] }) }))] }));
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":"AAwCA,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,+BAiyBnB"}
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,+BAkDhB"}
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 current = raw && options.includes(raw) ? raw : (options[0] ?? "");
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, { children: optionLabel(current, field) }) }), _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))) }) }) })] }));
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,+BA2LjB"}
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
- onChange((values ?? {}));
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":"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"}
@@ -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;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;AAqFF,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,OAAO,EACP,WAAW,EACX,UAAU,EACV,IAAI,EACJ,QAAQ,GACT,EAAE,wBAAwB,+BA6L1B"}
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"}