@nubitio/crud 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5310,12 +5310,22 @@ function useCrudPage(resource, externalFormRef) {
5310
5310
  }
5311
5311
  //#endregion
5312
5312
  //#region packages/crud/crud/AuditTrailPanel.tsx
5313
- const ACTION_COLORS = {
5314
- create: "#2e7d32",
5315
- update: "#1565c0",
5316
- delete: "#c62828"
5313
+ const ACTION_BADGE = {
5314
+ create: "success",
5315
+ update: "info",
5316
+ delete: "danger"
5317
5317
  };
5318
- function DefaultEntryRenderer({ entry }) {
5318
+ function formatAuditValue(value, yesLabel, noLabel) {
5319
+ if (value == null || value === "") return "—";
5320
+ if (typeof value === "boolean") return value ? yesLabel : noLabel;
5321
+ if (typeof value === "object") try {
5322
+ return JSON.stringify(value);
5323
+ } catch {
5324
+ return String(value);
5325
+ }
5326
+ return String(value);
5327
+ }
5328
+ function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5319
5329
  const { t } = (0, _nubitio_core.useCoreTranslation)();
5320
5330
  const date = new Date(entry.timestamp);
5321
5331
  const formatted = Number.isNaN(date.getTime()) ? entry.timestamp : date.toLocaleString((0, _nubitio_core.getCoreLocale)(), { timeZone: (0, _nubitio_core.getCoreTimezone)() });
@@ -5326,175 +5336,191 @@ function DefaultEntryRenderer({ entry }) {
5326
5336
  };
5327
5337
  const changeKeys = Object.keys(entry.changes);
5328
5338
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5329
- style: {
5330
- borderBottom: "1px solid #e0e0e0",
5331
- padding: "10px 0",
5332
- listStyle: "none"
5333
- },
5334
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5335
- style: {
5336
- display: "flex",
5337
- alignItems: "center",
5338
- gap: 8,
5339
- marginBottom: 4
5340
- },
5339
+ className: `nb-audit-trail__entry nb-audit-trail__entry--${entry.action}`,
5340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5341
+ className: "nb-audit-trail__marker",
5342
+ "aria-hidden": "true"
5343
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5344
+ className: "nb-audit-trail__meta",
5341
5345
  children: [
5342
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5343
- style: {
5344
- fontSize: 12,
5345
- color: "#757575"
5346
- },
5346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
5347
+ className: "nb-audit-trail__timestamp",
5348
+ dateTime: entry.timestamp,
5347
5349
  children: formatted
5348
5350
  }),
5349
5351
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5350
- style: {
5351
- fontSize: 12,
5352
- color: "#424242"
5353
- },
5352
+ className: "nb-audit-trail__user",
5354
5353
  children: entry.user
5355
5354
  }),
5356
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5357
- style: {
5358
- fontSize: 11,
5359
- fontWeight: 600,
5360
- padding: "1px 6px",
5361
- borderRadius: 4,
5362
- backgroundColor: ACTION_COLORS[entry.action],
5363
- color: "#fff"
5364
- },
5355
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Badge, {
5356
+ variant: ACTION_BADGE[entry.action],
5357
+ size: "sm",
5358
+ pill: true,
5365
5359
  children: actionLabels[entry.action]
5366
5360
  })
5367
5361
  ]
5368
5362
  }), changeKeys.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5369
- style: {
5370
- margin: "4px 0 0 0",
5371
- padding: "0 0 0 16px"
5372
- },
5363
+ className: "nb-audit-trail__changes",
5373
5364
  children: changeKeys.map((field) => {
5374
5365
  const { before, after } = entry.changes[field];
5375
5366
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5376
- style: {
5377
- fontSize: 12,
5378
- color: "#616161"
5379
- },
5380
- children: [
5381
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [field, ":"] }),
5382
- " ",
5383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5384
- style: { color: "#c62828" },
5385
- children: String(before ?? "—")
5386
- }),
5387
- " ",
5388
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5389
- style: { color: "#2e7d32" },
5390
- children: String(after ?? "")
5391
- })
5392
- ]
5367
+ className: "nb-audit-trail__change",
5368
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5369
+ className: "nb-audit-trail__field",
5370
+ children: resolveFieldLabel(field)
5371
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5372
+ className: "nb-audit-trail__diff",
5373
+ children: [
5374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5375
+ className: "nb-audit-trail__value nb-audit-trail__value--before",
5376
+ children: formatAuditValue(before, yesLabel, noLabel)
5377
+ }),
5378
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5379
+ className: "nb-audit-trail__arrow",
5380
+ "aria-hidden": "true",
5381
+ children: ""
5382
+ }),
5383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5384
+ className: "nb-audit-trail__value nb-audit-trail__value--after",
5385
+ children: formatAuditValue(after, yesLabel, noLabel)
5386
+ })
5387
+ ]
5388
+ })]
5393
5389
  }, field);
5394
5390
  })
5395
- })]
5391
+ })] })]
5392
+ });
5393
+ }
5394
+ function AuditTrailSkeleton() {
5395
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5396
+ className: "nb-audit-trail__skeleton",
5397
+ "aria-busy": "true",
5398
+ children: [
5399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
5400
+ variant: "rect",
5401
+ height: 72
5402
+ }),
5403
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
5404
+ variant: "rect",
5405
+ height: 72
5406
+ }),
5407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
5408
+ variant: "rect",
5409
+ height: 72
5410
+ })
5411
+ ]
5396
5412
  });
5397
5413
  }
5398
- function AuditTrailPanel({ url, renderEntry, visible, onClose }) {
5414
+ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, resolveFieldLabel, drawerSize = "sm" }) {
5399
5415
  const { t } = (0, _nubitio_core.useCoreTranslation)();
5400
5416
  const httpClient = (0, _nubitio_core.useCoreHttpClient)();
5401
5417
  const [fetchState, setFetchState] = (0, react.useState)({ status: "idle" });
5402
- (0, react.useEffect)(() => {
5403
- if (!visible || url === null) {
5418
+ const yesLabel = t("common.yes");
5419
+ const noLabel = t("common.no");
5420
+ const loadEntries = (0, react.useCallback)(() => {
5421
+ if (url === null) {
5404
5422
  setFetchState({ status: "idle" });
5405
5423
  return;
5406
5424
  }
5407
- let cancelled = false;
5408
5425
  setFetchState({ status: "loading" });
5409
5426
  httpClient.get(url).then((response) => {
5410
- if (!cancelled) setFetchState({
5427
+ setFetchState({
5411
5428
  status: "success",
5412
5429
  entries: response.data
5413
5430
  });
5414
5431
  }).catch(() => {
5415
- if (!cancelled) setFetchState({ status: "error" });
5432
+ setFetchState({ status: "error" });
5416
5433
  });
5417
- return () => {
5418
- cancelled = true;
5419
- };
5420
- }, [
5421
- httpClient,
5422
- url,
5423
- visible
5424
- ]);
5425
- if (!visible) return null;
5426
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
5427
- className: "nb-audit-trail-panel",
5428
- style: {
5429
- border: "1px solid #e0e0e0",
5430
- borderRadius: 4,
5431
- padding: "12px 16px",
5432
- backgroundColor: "#fafafa",
5433
- minWidth: 280
5434
- },
5435
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5436
- style: {
5437
- display: "flex",
5438
- alignItems: "center",
5439
- justifyContent: "space-between",
5440
- marginBottom: 12
5441
- },
5442
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
5443
- style: { fontSize: 14 },
5444
- children: t("auditTrail.title")
5445
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5446
- type: "button",
5447
- onClick: onClose,
5448
- "aria-label": t("auditTrail.closeButton"),
5449
- style: {
5450
- background: "none",
5451
- border: "none",
5452
- cursor: "pointer",
5453
- fontSize: 18,
5454
- lineHeight: 1,
5455
- padding: "0 4px",
5456
- color: "#616161"
5457
- },
5458
- children: "×"
5459
- })]
5460
- }), url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5461
- style: {
5462
- fontSize: 13,
5463
- color: "#757575",
5464
- margin: 0
5465
- },
5466
- children: t("auditTrail.selectRecord")
5467
- }) : fetchState.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5468
- style: {
5469
- fontSize: 13,
5470
- color: "#757575",
5471
- margin: 0
5472
- },
5473
- children: t("auditTrail.loading")
5474
- }) : fetchState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5475
- style: {
5476
- fontSize: 13,
5477
- color: "#c62828",
5478
- margin: 0
5479
- },
5480
- children: t("auditTrail.error")
5481
- }) : fetchState.status === "success" ? fetchState.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5482
- style: {
5483
- fontSize: 13,
5484
- color: "#757575",
5485
- margin: 0
5486
- },
5487
- children: t("auditTrail.empty")
5488
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5489
- style: {
5490
- margin: 0,
5491
- padding: 0
5492
- },
5493
- children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderEntry(entry) }, String(entry.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultEntryRenderer, { entry }, String(entry.id)))
5494
- }) : null]
5434
+ }, [httpClient, url]);
5435
+ (0, react.useEffect)(() => {
5436
+ if (!visible) {
5437
+ setFetchState({ status: "idle" });
5438
+ return;
5439
+ }
5440
+ loadEntries();
5441
+ }, [loadEntries, visible]);
5442
+ const drawerTitle = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("auditTrail.title") }), recordSubtitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5443
+ className: "nb-audit-trail__subtitle",
5444
+ children: recordSubtitle
5445
+ })] });
5446
+ const body = (() => {
5447
+ if (url === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5448
+ fill: true,
5449
+ icon: "cursor-click",
5450
+ title: t("auditTrail.selectRecord"),
5451
+ description: t("auditTrail.selectRecordHint"),
5452
+ size: "sm"
5453
+ });
5454
+ if (fetchState.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuditTrailSkeleton, {});
5455
+ if (fetchState.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5456
+ className: "nb-audit-trail__error",
5457
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5458
+ fill: true,
5459
+ variant: "danger",
5460
+ icon: "warning-circle",
5461
+ title: t("auditTrail.error"),
5462
+ size: "sm",
5463
+ action: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Button, {
5464
+ variant: "secondary",
5465
+ size: "sm",
5466
+ onClick: loadEntries,
5467
+ children: t("auditTrail.retry")
5468
+ })
5469
+ })
5470
+ });
5471
+ if (fetchState.status === "success" && fetchState.entries.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5472
+ fill: true,
5473
+ icon: "clock-counter-clockwise",
5474
+ title: t("auditTrail.empty"),
5475
+ description: t("auditTrail.emptyHint"),
5476
+ size: "sm"
5477
+ });
5478
+ if (fetchState.status === "success") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5479
+ className: "nb-audit-trail__timeline",
5480
+ children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
5481
+ className: "nb-audit-trail__entry",
5482
+ children: renderEntry(entry)
5483
+ }, String(entry.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultEntryRenderer, {
5484
+ entry,
5485
+ resolveFieldLabel,
5486
+ yesLabel,
5487
+ noLabel
5488
+ }, String(entry.id)))
5489
+ });
5490
+ return null;
5491
+ })();
5492
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Drawer, {
5493
+ isOpen: visible,
5494
+ onClose,
5495
+ title: drawerTitle,
5496
+ width: resolveDrawerWidth({ drawerSize }),
5497
+ side: "right",
5498
+ scrim: "subtle",
5499
+ closeLabel: t("auditTrail.closeButton"),
5500
+ "aria-label": t("auditTrail.title"),
5501
+ className: "nb-audit-trail-drawer",
5502
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5503
+ className: "nb-audit-trail",
5504
+ children: body
5505
+ })
5495
5506
  });
5496
5507
  }
5497
5508
  //#endregion
5509
+ //#region packages/crud/crud/AuditTrail.ts
5510
+ function createAuditFieldLabelResolver(config, fields) {
5511
+ const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5512
+ return (field) => {
5513
+ const fromConfig = config?.fieldLabels;
5514
+ if (fromConfig) {
5515
+ if (typeof fromConfig === "function") {
5516
+ const resolved = fromConfig(field);
5517
+ if (resolved) return resolved;
5518
+ } else if (fromConfig[field]) return fromConfig[field];
5519
+ }
5520
+ return fieldLabelByName.get(field) ?? field;
5521
+ };
5522
+ }
5523
+ //#endregion
5498
5524
  //#region packages/crud/crud/dialogStore.ts
5499
5525
  function initialDialogState() {
5500
5526
  return {
@@ -5842,13 +5868,30 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5842
5868
  if (hasAuditTrail) {
5843
5869
  const first = rows[0];
5844
5870
  if (first != null) setSelectedRowId(first["id"] ?? first["ID"] ?? null);
5871
+ else setSelectedRowId(null);
5845
5872
  }
5846
5873
  };
5874
+ const selectedRow = selectedRows[0] ?? null;
5847
5875
  const auditUrl = (0, react.useMemo)(() => {
5848
5876
  if (!resolvedResource.auditTrail?.enabled || selectedRowId == null) return null;
5849
5877
  const { apiUrl } = resolvedResource.auditTrail;
5850
5878
  return typeof apiUrl === "function" ? apiUrl(selectedRowId) : `${apiUrl}${selectedRowId}`;
5851
5879
  }, [resolvedResource.auditTrail, selectedRowId]);
5880
+ const resolveAuditFieldLabel = (0, react.useMemo)(() => createAuditFieldLabelResolver(resolvedResource.auditTrail, routeAwareGridFields), [resolvedResource.auditTrail, routeAwareGridFields]);
5881
+ const auditRecordSubtitle = (0, react.useMemo)(() => {
5882
+ if (!resolvedResource.auditTrail?.recordSubtitle || selectedRow == null) return void 0;
5883
+ return resolvedResource.auditTrail.recordSubtitle(selectedRow);
5884
+ }, [resolvedResource.auditTrail, selectedRow]);
5885
+ const openAuditTrail = (0, react.useCallback)((row) => {
5886
+ if (row != null) {
5887
+ const id = row["id"] ?? row["ID"] ?? null;
5888
+ if (id != null) {
5889
+ setSelectedRowId(id);
5890
+ setSelectedRows([row]);
5891
+ }
5892
+ }
5893
+ setAuditOpen(true);
5894
+ }, []);
5852
5895
  const executeBulkAction = (0, react.useCallback)(async (action) => {
5853
5896
  await action.onAction(selectionState.selectedIds);
5854
5897
  selectionState.clearSelection();
@@ -5862,7 +5905,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5862
5905
  else executeBulkAction(action);
5863
5906
  }, [executeBulkAction]);
5864
5907
  const toolbar = (0, react.useMemo)(() => {
5865
- return resolveResourceToolbar(resolvedResource, {
5908
+ const base = resolveResourceToolbar(resolvedResource, {
5866
5909
  resource: resolvedResource,
5867
5910
  selectedRow: selectedRows[0],
5868
5911
  selectedRows,
@@ -5870,14 +5913,48 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5870
5913
  formRef,
5871
5914
  events,
5872
5915
  emit
5873
- });
5916
+ }) ?? {};
5917
+ if (!hasAuditTrail) return base;
5918
+ return {
5919
+ ...base,
5920
+ utility: [...base.utility ?? [], {
5921
+ key: "audit-trail",
5922
+ text: t("crudPage.auditTrailButton"),
5923
+ icon: "ph-clock-counter-clockwise",
5924
+ hint: t("auditTrail.toolbarHint"),
5925
+ disabled: selectedRowId == null,
5926
+ onClick: () => openAuditTrail()
5927
+ }]
5928
+ };
5874
5929
  }, [
5875
5930
  emit,
5876
5931
  events,
5877
5932
  formRef,
5878
5933
  gridRef,
5934
+ hasAuditTrail,
5935
+ openAuditTrail,
5879
5936
  resolvedResource,
5880
- selectedRows
5937
+ selectedRowId,
5938
+ selectedRows,
5939
+ t
5940
+ ]);
5941
+ const rowActions = (0, react.useMemo)(() => {
5942
+ const base = resolvedResource.rowActions;
5943
+ if (!hasAuditTrail || resolvedResource.auditTrail?.rowAction === false) return base;
5944
+ const auditAction = (row) => ({
5945
+ key: "audit-trail",
5946
+ text: t("auditTrail.rowAction"),
5947
+ icon: "ph-clock-counter-clockwise",
5948
+ onClick: () => openAuditTrail(row)
5949
+ });
5950
+ if (typeof base === "function") return (row) => [...base(row), auditAction(row)];
5951
+ return (row) => [...base ?? [], auditAction(row)];
5952
+ }, [
5953
+ hasAuditTrail,
5954
+ openAuditTrail,
5955
+ resolvedResource.auditTrail?.rowAction,
5956
+ resolvedResource.rowActions,
5957
+ t
5881
5958
  ]);
5882
5959
  /** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
5883
5960
  const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColumnPresetSelector, {
@@ -5890,7 +5967,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5890
5967
  }) : void 0;
5891
5968
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5892
5969
  id: "wrapper",
5893
- className: [auditOpen && "wrapper--with-audit", viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5970
+ className: [viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5894
5971
  children: [
5895
5972
  (resolvedResource.bulkActions?.length ?? 0) > 0 && selectionState.hasSelection && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5896
5973
  className: "nb-bulk-toolbar",
@@ -5907,16 +5984,6 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5907
5984
  children: action.label
5908
5985
  }, action.key))]
5909
5986
  }),
5910
- hasAuditTrail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5911
- className: "nb-audit-trail-toolbar",
5912
- style: { marginBottom: 8 },
5913
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5914
- type: "button",
5915
- onClick: () => setAuditOpen((prev) => !prev),
5916
- "aria-pressed": auditOpen,
5917
- children: t("crudPage.auditTrailButton")
5918
- })
5919
- }),
5920
5987
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NativeDataGridView, {
5921
5988
  ref: gridRef,
5922
5989
  id: resolvedResource.id,
@@ -5941,7 +6008,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5941
6008
  mode: resolvedResource.mode,
5942
6009
  stateStoringEnabled: resolvedResource.stateStoring,
5943
6010
  toolbar,
5944
- rowActions: resolvedResource.rowActions,
6011
+ rowActions,
5945
6012
  onAdd: requestNew,
5946
6013
  onEdit: requestEdit,
5947
6014
  onView: requestView,
@@ -6014,7 +6081,10 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6014
6081
  url: auditUrl,
6015
6082
  renderEntry: resolvedResource.auditTrail.renderEntry,
6016
6083
  visible: auditOpen,
6017
- onClose: () => setAuditOpen(false)
6084
+ onClose: () => setAuditOpen(false),
6085
+ recordSubtitle: auditRecordSubtitle,
6086
+ resolveFieldLabel: resolveAuditFieldLabel,
6087
+ drawerSize: resolvedResource.auditTrail?.drawerSize
6018
6088
  }),
6019
6089
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ConfirmDialog, {
6020
6090
  open: confirmState.open,
@@ -6912,20 +6982,22 @@ function ResourceSchemaProvider({ children, resolver }) {
6912
6982
  children
6913
6983
  });
6914
6984
  }
6915
- function resolveWithRuntimeErrors(resolver, supportedOperations = []) {
6985
+ function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout) {
6916
6986
  try {
6917
6987
  return {
6918
6988
  fields: resolver(),
6919
6989
  isLoading: false,
6920
6990
  error: void 0,
6921
- supportedOperations
6991
+ supportedOperations,
6992
+ formLayout
6922
6993
  };
6923
6994
  } catch (runtimeError) {
6924
6995
  return {
6925
6996
  fields: [],
6926
6997
  isLoading: false,
6927
6998
  error: runtimeError instanceof Error ? runtimeError : new Error(String(runtimeError)),
6928
- supportedOperations
6999
+ supportedOperations,
7000
+ formLayout
6929
7001
  };
6930
7002
  }
6931
7003
  }
@@ -6951,7 +7023,7 @@ function useResolvedResourceFields({ apiUrl, manualFields, overrides, fieldContr
6951
7023
  baselineFields: baseline.fields,
6952
7024
  contract: fieldContract,
6953
7025
  legacyOverrides: fieldContract ? void 0 : overrides
6954
- }), baseline.supportedOperations);
7026
+ }), baseline.supportedOperations, baseline.formLayout);
6955
7027
  }, [
6956
7028
  baseline,
6957
7029
  fieldContract,
@@ -7031,7 +7103,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7031
7103
  const effectiveGridRef = gridRef ?? internalGridRef;
7032
7104
  const resolvedBaseResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
7033
7105
  const hasManualFields = !resource.fieldContract && Array.isArray(resource.fields) && resource.fields.length > 0;
7034
- const { fields, isLoading, error, supportedOperations } = useResolvedResourceFields({
7106
+ const { fields, isLoading, error, supportedOperations, formLayout: inferredFormLayout } = useResolvedResourceFields({
7035
7107
  apiUrl: resolvedBaseResource.apiUrl,
7036
7108
  manualFields: hasManualFields ? buildFields(resource.fields) : void 0,
7037
7109
  overrides: hasManualFields ? void 0 : fieldOverrides,
@@ -7066,11 +7138,13 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7066
7138
  apiUrl: normalizedApiUrl,
7067
7139
  fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7068
7140
  formFields: processedFields,
7141
+ formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7069
7142
  _supportedOperations: supportedOperations
7070
7143
  }), [
7071
7144
  fields,
7072
7145
  gridFields,
7073
7146
  hasManualFields,
7147
+ inferredFormLayout,
7074
7148
  normalizedApiUrl,
7075
7149
  processedFields,
7076
7150
  resolvedBaseResource,
package/dist/index.d.cts CHANGED
@@ -495,10 +495,23 @@ interface AuditEntry {
495
495
  after: unknown;
496
496
  }>;
497
497
  }
498
- interface AuditTrailConfig {
498
+ type AuditFieldLabelResolver = (field: string) => string;
499
+ interface AuditTrailConfig<T extends DataRecord$1 = DataRecord$1> {
499
500
  enabled: boolean;
500
501
  apiUrl: string | ((id: string | number) => string);
502
+ /** Custom entry renderer. Receives resolved field labels via context in the panel. */
501
503
  renderEntry?: (entry: AuditEntry) => ReactNode;
504
+ /**
505
+ * Maps API field keys to display labels. When omitted, CrudPage falls back to
506
+ * matching grid field labels from the active resource schema.
507
+ */
508
+ fieldLabels?: Record<string, string> | ((field: string) => string | undefined);
509
+ /** One-line context shown under the drawer title for the selected row. */
510
+ recordSubtitle?: (row: T) => string | undefined;
511
+ /** Drawer width token. Default `sm` (480px) — read-only secondary panel. */
512
+ drawerSize?: CrudDrawerSize;
513
+ /** Adds a row-menu action that opens the audit drawer. Default true. */
514
+ rowAction?: boolean;
502
515
  }
503
516
  //#endregion
504
517
  //#region packages/crud/crud/fieldOperationSemantics.d.ts
@@ -1149,13 +1162,19 @@ interface AuditTrailPanelProps {
1149
1162
  renderEntry?: (entry: AuditEntry) => ReactNode;
1150
1163
  visible: boolean;
1151
1164
  onClose: () => void;
1165
+ recordSubtitle?: string;
1166
+ resolveFieldLabel: AuditFieldLabelResolver;
1167
+ drawerSize?: CrudDrawerSize;
1152
1168
  }
1153
1169
  declare function AuditTrailPanel({
1154
1170
  url,
1155
1171
  renderEntry,
1156
1172
  visible,
1157
- onClose
1158
- }: AuditTrailPanelProps): React.JSX.Element | null;
1173
+ onClose,
1174
+ recordSubtitle,
1175
+ resolveFieldLabel,
1176
+ drawerSize
1177
+ }: AuditTrailPanelProps): React.JSX.Element;
1159
1178
  //#endregion
1160
1179
  //#region packages/crud/field/FieldBuilders.d.ts
1161
1180
  /**
@@ -1871,6 +1890,11 @@ interface ResourceSchemaResolution {
1871
1890
  isLoading: boolean;
1872
1891
  error: Error | undefined;
1873
1892
  supportedOperations: string[];
1893
+ /**
1894
+ * Backend-declared form layout (sections/tabs) from the API doc, when the
1895
+ * resource publishes one. Explicit `ResourceConfig.formLayout` wins.
1896
+ */
1897
+ formLayout?: FormLayout;
1874
1898
  }
1875
1899
  interface ResourceSchemaResolver {
1876
1900
  useResourceSchema(request: ResourceSchemaRequest): ResourceSchemaResolution;
@@ -1931,4 +1955,4 @@ declare function ResourceStoreProvider({
1931
1955
  }: ResourceStoreProviderProps): React.JSX.Element;
1932
1956
  declare function useResourceStoreFactory(): ResourceStoreFactory;
1933
1957
  //#endregion
1934
- export { type AuditEntry, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
1958
+ export { type AuditEntry, type AuditFieldLabelResolver, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };