@vertebr/react 0.18.1 → 0.19.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 (60) 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/AuditBoard.d.ts +6 -2
  5. package/dist/components/admin/AuditBoard.d.ts.map +1 -1
  6. package/dist/components/admin/AuditBoard.js +145 -3
  7. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  8. package/dist/components/admin/VertebrAdmin.js +14 -3
  9. package/dist/components/config/EnumInput.d.ts.map +1 -1
  10. package/dist/components/config/EnumInput.js +9 -2
  11. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  12. package/dist/components/config/form/ConfigForm.js +14 -2
  13. package/dist/components/emails/EmailsActivityPanel.d.ts.map +1 -1
  14. package/dist/components/emails/EmailsActivityPanel.js +80 -2
  15. package/dist/components/metrics/MetricsBoard.d.ts.map +1 -1
  16. package/dist/components/metrics/MetricsBoard.js +7 -1
  17. package/dist/components/resources/ColumnVisibilityMenu.d.ts +4 -0
  18. package/dist/components/resources/ColumnVisibilityMenu.d.ts.map +1 -1
  19. package/dist/components/resources/ColumnVisibilityMenu.js +14 -7
  20. package/dist/components/resources/ResourceTable.d.ts +7 -1
  21. package/dist/components/resources/ResourceTable.d.ts.map +1 -1
  22. package/dist/components/resources/ResourceTable.js +84 -64
  23. package/dist/components/resources/SortableHeader.d.ts +9 -0
  24. package/dist/components/resources/SortableHeader.d.ts.map +1 -0
  25. package/dist/components/resources/SortableHeader.js +9 -0
  26. package/dist/components/table/TableFilters.d.ts +20 -0
  27. package/dist/components/table/TableFilters.d.ts.map +1 -0
  28. package/dist/components/table/TableFilters.js +55 -0
  29. package/dist/components/table/TableSearch.d.ts +11 -0
  30. package/dist/components/table/TableSearch.d.ts.map +1 -0
  31. package/dist/components/table/TableSearch.js +23 -0
  32. package/dist/components/table/TableSkeleton.d.ts +16 -0
  33. package/dist/components/table/TableSkeleton.d.ts.map +1 -0
  34. package/dist/components/table/TableSkeleton.js +29 -0
  35. package/dist/components/table/TableWrap.d.ts +23 -0
  36. package/dist/components/table/TableWrap.d.ts.map +1 -0
  37. package/dist/components/table/TableWrap.js +35 -0
  38. package/dist/components/table/filterModel.d.ts +35 -0
  39. package/dist/components/table/filterModel.d.ts.map +1 -0
  40. package/dist/components/table/filterModel.js +150 -0
  41. package/dist/components/table/index.d.ts +13 -0
  42. package/dist/components/table/index.d.ts.map +1 -0
  43. package/dist/components/table/index.js +6 -0
  44. package/dist/components/table/useTableOverflow.d.ts +17 -0
  45. package/dist/components/table/useTableOverflow.d.ts.map +1 -0
  46. package/dist/components/table/useTableOverflow.js +54 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +2 -0
  50. package/dist/lib/sort.d.ts +10 -0
  51. package/dist/lib/sort.d.ts.map +1 -0
  52. package/dist/lib/sort.js +59 -0
  53. package/dist/styles/audit.css +11 -1
  54. package/dist/styles/columns.css +1 -84
  55. package/dist/styles/metrics.css +27 -4
  56. package/dist/styles/panel.css +143 -0
  57. package/dist/styles/select.css +16 -0
  58. package/dist/styles/sync.css +4 -1
  59. package/dist/styles/table.css +115 -2
  60. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { CatalogResponse, ConfigResponse, EmailsBoardResponse, FontLibraryResponse, GalleryResponse, MetricsResponse, ResourceListResponse, VertebrClient } from "@vertebr/client";
2
2
  import type { MetricsRangeId } from "../../lib/metricsRanges";
3
+ import type { TableSortState } from "../../lib/sort";
3
4
  import type { Selection } from "./types";
4
5
  export type AdminContentProps = {
5
6
  client: VertebrClient;
@@ -24,8 +25,11 @@ export type AdminContentProps = {
24
25
  emailsLoading: boolean;
25
26
  onRefreshEmails: () => Promise<void>;
26
27
  resourceData: ResourceListResponse | null;
28
+ resourceLoading?: boolean;
27
29
  resourceSearch?: string;
28
30
  onResourceSearchChange?: (q: string) => void;
31
+ resourceSort?: TableSortState | null;
32
+ onResourceSortChange?: (sort: TableSortState) => void;
29
33
  onRefreshResource: () => Promise<void>;
30
34
  onRefreshConfig?: () => Promise<void>;
31
35
  configData: ConfigResponse | null;
@@ -45,5 +49,5 @@ export type AdminContentProps = {
45
49
  onRefreshFonts?: () => Promise<void>;
46
50
  };
47
51
  };
48
- export declare function AdminContent({ client, catalog, selection, onSelectionChange, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, fontLibraryData, fontLibraryLoading, onRefreshFonts, emailsData, emailsLoading, onRefreshEmails, resourceData, resourceSearch, onResourceSearchChange, onRefreshResource, onRefreshConfig, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }: AdminContentProps): import("react").JSX.Element;
52
+ export declare function AdminContent({ client, catalog, selection, onSelectionChange, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, fontLibraryData, fontLibraryLoading, onRefreshFonts, emailsData, emailsLoading, onRefreshEmails, resourceData, resourceLoading, resourceSearch, onResourceSearchChange, resourceSort, onResourceSortChange, onRefreshResource, onRefreshConfig, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }: AdminContentProps): import("react").JSX.Element;
49
53
  //# sourceMappingURL=AdminContent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdminContent.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminContent.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AdminContent.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminContent.tsx"],"names":[],"mappings":"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"}
@@ -57,8 +57,9 @@ function ConfigEditor({ client, catalog, configData, isPageSelection, previewPat
57
57
  };
58
58
  }, [client, configData.id, relatedKey, relatedAddons]);
59
59
  const onChange = useCallback((value) => {
60
- setDraft(value);
61
- }, []);
60
+ // Merge — react-hook-form watch can omit unchanged / empty keys.
61
+ setDraft((prev) => ({ ...(prev ?? configData.value), ...value }));
62
+ }, [configData.value]);
62
63
  const form = (_jsx(ConfigForm, { schema: configData.schema, value: configData.value, client: client, canWrite: canWrite, revisionsEnabled: showRevisions, canPublish: canPublish && Boolean(configData.draftRevisionId), onPublish: canPublish && configData.draftRevisionId
63
64
  ? async () => {
64
65
  const published = await client.publishRevision(configData.id, configData.draftRevisionId);
@@ -76,7 +77,7 @@ function ConfigEditor({ client, catalog, configData, isPageSelection, previewPat
76
77
  return (_jsxs("button", { type: "button", className: "vertebr-page-related-addon", "data-enabled": enabled ? "true" : "false", onClick: () => onSelectAddon?.(addon.id), children: [_jsx("span", { children: addon.label }), _jsx("em", { children: enabled ? "on" : "off" })] }, addon.id));
77
78
  }) })] })) : null, showPreview && previewPath ? (_jsxs("div", { className: "vertebr-page-editor", children: [_jsx("div", { className: "vertebr-page-editor-form", children: form }), _jsx(PagePreviewPane, { previewPath: previewPath, controlId: configData.id, draft: draft ?? configData.value, relatedDrafts: Object.keys(relatedDrafts).length ? relatedDrafts : undefined })] })) : (form)] }));
78
79
  }
79
- export function AdminContent({ client, catalog, selection, onSelectionChange, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, fontLibraryData, fontLibraryLoading, onRefreshFonts, emailsData, emailsLoading, onRefreshEmails, resourceData, resourceSearch, onResourceSearchChange, onRefreshResource, onRefreshConfig, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }) {
80
+ export function AdminContent({ client, catalog, selection, onSelectionChange, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, fontLibraryData, fontLibraryLoading, onRefreshFonts, emailsData, emailsLoading, onRefreshEmails, resourceData, resourceLoading = false, resourceSearch, onResourceSearchChange, resourceSort = null, onResourceSortChange, onRefreshResource, onRefreshConfig, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }) {
80
81
  if (!catalog) {
81
82
  return (_jsx("div", { className: "vertebr-content", children: _jsx("section", { className: "vertebr-panel", children: _jsx("div", { className: "vertebr-empty", children: authRequired
82
83
  ? "Authenticated access required for this environment."
@@ -84,7 +85,7 @@ export function AdminContent({ client, catalog, selection, onSelectionChange, au
84
85
  ? "Loading…"
85
86
  : "Unable to load catalog." }) }) }));
86
87
  }
87
- return (_jsx("div", { className: "vertebr-content", children: _jsxs("section", { className: "vertebr-panel", children: [selection?.kind === "overview" ? (_jsx(MetricsBoard, { label: metricsData?.label ?? catalog.metrics?.label, windowLabel: metricsData?.windowLabel, compareLabel: metricsData?.compareLabel, metrics: metricsData?.metrics ?? [], charts: metricsData?.charts ?? [], range: metricsRange, onRangeChange: onMetricsRangeChange, loading: metricsLoading, onRefresh: () => void onRefreshMetrics() })) : null, selection?.kind === "audit" ? (_jsx(AuditBoard, { client: client, label: catalog.audit?.label })) : null, selection?.kind === "sync" ? (_jsx(AdminSyncPage, { client: client, catalog: catalog })) : null, selection?.kind === "gallery" && galleryData ? (_jsx(GalleryBoard, { label: galleryData.label, items: galleryData.items, accept: galleryData.accept, maxBytes: galleryData.maxBytes, canDelete: galleryData.canDelete, loading: galleryLoading, onRefresh: () => void onRefreshGallery(), onUpload: async (files) => {
88
+ return (_jsx("div", { className: "vertebr-content", children: _jsxs("section", { className: "vertebr-panel", children: [selection?.kind === "overview" ? (_jsx(MetricsBoard, { label: metricsData?.label ?? catalog.metrics?.label, windowLabel: metricsData?.windowLabel, compareLabel: metricsData?.compareLabel, metrics: metricsData?.metrics ?? [], charts: metricsData?.charts ?? [], range: metricsRange, onRangeChange: onMetricsRangeChange, loading: metricsLoading, onRefresh: () => void onRefreshMetrics() })) : null, selection?.kind === "audit" ? (_jsx(AuditBoard, { client: client, label: catalog.audit?.label, columns: catalog.audit?.columns })) : null, selection?.kind === "sync" ? (_jsx(AdminSyncPage, { client: client, catalog: catalog })) : null, selection?.kind === "gallery" && galleryData ? (_jsx(GalleryBoard, { label: galleryData.label, items: galleryData.items, accept: galleryData.accept, maxBytes: galleryData.maxBytes, canDelete: galleryData.canDelete, loading: galleryLoading, onRefresh: () => void onRefreshGallery(), onUpload: async (files) => {
88
89
  for (const file of files) {
89
90
  await client.uploadGalleryFile(file);
90
91
  }
@@ -151,52 +152,76 @@ export function AdminContent({ client, catalog, selection, onSelectionChange, au
151
152
  ? async (id) => {
152
153
  await client.deleteEmailLibraryTemplate(id);
153
154
  }
154
- : undefined })) : null, selection?.kind === "resource" && resourceData ? (_jsx(ResourcePanel, { schema: resourceData.schema, items: resourceData.items, label: resourceData.label, listQuery: resourceData.listQuery, total: resourceData.total, searchQuery: resourceSearch, onSearchChange: onResourceSearchChange, initialEditRowId: selection.rowId && selection.id === resourceData.id
155
- ? selection.rowId
156
- : undefined, onEditRowIdChange: (rowId) => {
157
- onSelectionChange?.({
158
- kind: "resource",
159
- id: resourceData.id,
160
- rowId: rowId ?? undefined,
161
- });
162
- }, permissions: resourceData.permissions, actions: resourceData.actions, collectionActions: resourceData.collectionActions, bulkActions: resourceData.bulkActions, uploads: resourceData.uploads, storageKey: resourceData.id, onUpdate: resourceData.permissions.update
163
- ? async (rowId, value) => {
164
- await client.updateResource(resourceData.id, rowId, value);
165
- await onRefreshResource();
166
- }
167
- : undefined, onCreate: resourceData.permissions.create
168
- ? async (value) => {
169
- await client.createResource(resourceData.id, value);
170
- await onRefreshResource();
171
- }
172
- : undefined, onDelete: resourceData.permissions.delete
173
- ? async (rowId) => {
174
- await client.deleteResource(resourceData.id, rowId);
175
- await onRefreshResource();
176
- }
177
- : undefined, onAction: async (rowId, actionId, input) => {
178
- await client.runResourceAction(resourceData.id, rowId, actionId, input);
179
- }, onActionDone: onRefreshResource, onPreviewMail: async (rowId, actionId, input) => {
180
- return client.previewResourceMail(resourceData.id, rowId, actionId, input);
181
- }, onCollectionAction: async (actionId, input) => {
182
- return client.runCollectionAction(resourceData.id, actionId, input);
183
- }, onBulkAction: resourceData.bulkActions
184
- ? async (actionId, rowIds, input) => {
185
- await client.runBulkAction(resourceData.id, actionId, {
186
- rowIds,
187
- input,
188
- });
189
- }
190
- : undefined, onUpload: resourceData.uploads
191
- ? async (field, files) => {
192
- const uploaded = [];
193
- for (const file of files) {
194
- const result = await client.uploadResourceFile(resourceData.id, field, file);
195
- uploaded.push({ url: result.url, name: result.name });
196
- }
197
- return uploaded;
198
- }
199
- : undefined, ...galleryPicker })) : null, selection?.kind === "addons" ? (_jsx(AddonOverview, { client: client, catalog: catalog, onSelectAddon: (id) => onSelectionChange?.({ kind: "addon", id }) })) : null, selection?.kind === "config" && configData ? (_jsx(ConfigEditor, { client: client, catalog: catalog, configData: configData, isPageSelection: isPageSelection, previewPath: catalog.configs.find((item) => item.id === configData.id)
155
+ : undefined })) : null, selection?.kind === "resource"
156
+ ? (() => {
157
+ const catalogResource = catalog.resources.find((item) => item.id === selection.id);
158
+ const ready = resourceData != null && resourceData.id === selection.id;
159
+ const schema = ready
160
+ ? resourceData.schema
161
+ : (catalogResource?.schema ?? {});
162
+ if (!ready && Object.keys(schema).length === 0)
163
+ return null;
164
+ return (_jsx(ResourcePanel, { schema: schema, items: ready ? resourceData.items : [], label: ready
165
+ ? resourceData.label
166
+ : (catalogResource?.label ?? selection.id), listQuery: ready
167
+ ? resourceData.listQuery
168
+ : catalogResource?.listQuery, total: ready ? resourceData.total : undefined, loading: resourceLoading || !ready, searchQuery: resourceSearch, onSearchChange: onResourceSearchChange, sort: resourceSort, onSortChange: onResourceSortChange, initialEditRowId: ready && selection.rowId ? selection.rowId : undefined, onEditRowIdChange: (rowId) => {
169
+ onSelectionChange?.({
170
+ kind: "resource",
171
+ id: selection.id,
172
+ rowId: rowId ?? undefined,
173
+ });
174
+ }, permissions: ready
175
+ ? resourceData.permissions
176
+ : { create: false, update: false, delete: false }, actions: ready ? resourceData.actions : {}, collectionActions: ready ? resourceData.collectionActions : {}, bulkActions: ready ? resourceData.bulkActions : {}, uploads: ready ? resourceData.uploads : undefined, storageKey: selection.id, onUpdate: ready && resourceData.permissions.update
177
+ ? async (rowId, value) => {
178
+ await client.updateResource(resourceData.id, rowId, value);
179
+ await onRefreshResource();
180
+ }
181
+ : undefined, onCreate: ready && resourceData.permissions.create
182
+ ? async (value) => {
183
+ await client.createResource(resourceData.id, value);
184
+ await onRefreshResource();
185
+ }
186
+ : undefined, onDelete: ready && resourceData.permissions.delete
187
+ ? async (rowId) => {
188
+ await client.deleteResource(resourceData.id, rowId);
189
+ await onRefreshResource();
190
+ }
191
+ : undefined, onAction: ready
192
+ ? async (rowId, actionId, input) => {
193
+ await client.runResourceAction(resourceData.id, rowId, actionId, input);
194
+ }
195
+ : undefined, onActionDone: onRefreshResource, onPreviewMail: ready
196
+ ? async (rowId, actionId, input) => {
197
+ return client.previewResourceMail(resourceData.id, rowId, actionId, input);
198
+ }
199
+ : undefined, onCollectionAction: ready
200
+ ? async (actionId, input) => {
201
+ return client.runCollectionAction(resourceData.id, actionId, input);
202
+ }
203
+ : undefined, onBulkAction: ready && resourceData.bulkActions
204
+ ? async (actionId, rowIds, input) => {
205
+ await client.runBulkAction(resourceData.id, actionId, {
206
+ rowIds,
207
+ input,
208
+ });
209
+ }
210
+ : undefined, onUpload: ready && resourceData.uploads
211
+ ? async (field, files) => {
212
+ const uploaded = [];
213
+ for (const file of files) {
214
+ const result = await client.uploadResourceFile(resourceData.id, field, file);
215
+ uploaded.push({
216
+ url: result.url,
217
+ name: result.name,
218
+ });
219
+ }
220
+ return uploaded;
221
+ }
222
+ : undefined, ...galleryPicker }));
223
+ })()
224
+ : null, selection?.kind === "addons" ? (_jsx(AddonOverview, { client: client, catalog: catalog, onSelectAddon: (id) => onSelectionChange?.({ kind: "addon", id }) })) : null, selection?.kind === "config" && configData ? (_jsx(ConfigEditor, { client: client, catalog: catalog, configData: configData, isPageSelection: isPageSelection, previewPath: catalog.configs.find((item) => item.id === configData.id)
200
225
  ?.previewPath, relatedAddons: catalog.configs.find((item) => item.id === configData.id)
201
226
  ?.relatedAddons, onConfigSaved: onConfigSaved, onRefreshConfig: onRefreshConfig, onSelectAddon: (id) => onSelectionChange?.({ kind: "addon", id }), galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker }, configData.id)) : null, selection?.kind === "addon" && configData ? ((() => {
202
227
  const addon = catalog.addons?.find((item) => item.id === configData.id);
@@ -1,7 +1,11 @@
1
- import type { VertebrClient } from "@vertebr/client";
1
+ import type { AuditColumn, AuditEntry, VertebrClient } from "@vertebr/client";
2
2
  export type AuditBoardProps = {
3
3
  client: VertebrClient;
4
4
  label?: string;
5
+ /** From `catalog.audit.columns`. Omit for Vertebr defaults. */
6
+ columns?: AuditColumn[];
5
7
  };
6
- export declare function AuditBoard({ client, label }: AuditBoardProps): import("react").JSX.Element;
8
+ /** Resolve a column path against an audit entry (`meta.x`, `target`, or field). */
9
+ export declare function resolveAuditColumnValue(entry: AuditEntry, path: string): unknown;
10
+ export declare function AuditBoard({ client, label, columns: columnsProp, }: AuditBoardProps): import("react").JSX.Element;
7
11
  //# sourceMappingURL=AuditBoard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuditBoard.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AuditBoard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEjE,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAaF,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,KAAe,EAAE,EAAE,eAAe,+BA4EtE"}
1
+ {"version":3,"file":"AuditBoard.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AuditBoard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,aAAa,EACd,MAAM,iBAAiB,CAAC;AAoBzB,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB,CAAC;AAsEF,mFAAmF;AACnF,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAST;AA6CD,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,KAAe,EACf,OAAO,EAAE,WAAW,GACrB,EAAE,eAAe,+BA2LjB"}
@@ -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]);
@@ -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"}