@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.d.mts 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 };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
2
2
  import { Route, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
3
3
  import { createPortal } from "react-dom";
4
- import { AppDialog, AppDropdown, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, IconButton, Skeleton } from "@nubitio/ui";
4
+ import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, IconButton, Skeleton } from "@nubitio/ui";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createCrudEvents, createScopedEventBus, getCoreCurrency, getCoreLocale, getCoreTimezone, useCoreHttpClient, useCoreRuntime, useCoreTranslation, useEvents, useMercureSubscription } from "@nubitio/core";
7
7
  import { useDropzone } from "react-dropzone";
@@ -5286,12 +5286,22 @@ function useCrudPage(resource, externalFormRef) {
5286
5286
  }
5287
5287
  //#endregion
5288
5288
  //#region packages/crud/crud/AuditTrailPanel.tsx
5289
- const ACTION_COLORS = {
5290
- create: "#2e7d32",
5291
- update: "#1565c0",
5292
- delete: "#c62828"
5289
+ const ACTION_BADGE = {
5290
+ create: "success",
5291
+ update: "info",
5292
+ delete: "danger"
5293
5293
  };
5294
- function DefaultEntryRenderer({ entry }) {
5294
+ function formatAuditValue(value, yesLabel, noLabel) {
5295
+ if (value == null || value === "") return "—";
5296
+ if (typeof value === "boolean") return value ? yesLabel : noLabel;
5297
+ if (typeof value === "object") try {
5298
+ return JSON.stringify(value);
5299
+ } catch {
5300
+ return String(value);
5301
+ }
5302
+ return String(value);
5303
+ }
5304
+ function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5295
5305
  const { t } = useCoreTranslation();
5296
5306
  const date = new Date(entry.timestamp);
5297
5307
  const formatted = Number.isNaN(date.getTime()) ? entry.timestamp : date.toLocaleString(getCoreLocale(), { timeZone: getCoreTimezone() });
@@ -5302,175 +5312,191 @@ function DefaultEntryRenderer({ entry }) {
5302
5312
  };
5303
5313
  const changeKeys = Object.keys(entry.changes);
5304
5314
  return /* @__PURE__ */ jsxs("li", {
5305
- style: {
5306
- borderBottom: "1px solid #e0e0e0",
5307
- padding: "10px 0",
5308
- listStyle: "none"
5309
- },
5310
- children: [/* @__PURE__ */ jsxs("div", {
5311
- style: {
5312
- display: "flex",
5313
- alignItems: "center",
5314
- gap: 8,
5315
- marginBottom: 4
5316
- },
5315
+ className: `nb-audit-trail__entry nb-audit-trail__entry--${entry.action}`,
5316
+ children: [/* @__PURE__ */ jsx("span", {
5317
+ className: "nb-audit-trail__marker",
5318
+ "aria-hidden": "true"
5319
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
5320
+ className: "nb-audit-trail__meta",
5317
5321
  children: [
5318
- /* @__PURE__ */ jsx("span", {
5319
- style: {
5320
- fontSize: 12,
5321
- color: "#757575"
5322
- },
5322
+ /* @__PURE__ */ jsx("time", {
5323
+ className: "nb-audit-trail__timestamp",
5324
+ dateTime: entry.timestamp,
5323
5325
  children: formatted
5324
5326
  }),
5325
5327
  /* @__PURE__ */ jsx("span", {
5326
- style: {
5327
- fontSize: 12,
5328
- color: "#424242"
5329
- },
5328
+ className: "nb-audit-trail__user",
5330
5329
  children: entry.user
5331
5330
  }),
5332
- /* @__PURE__ */ jsx("span", {
5333
- style: {
5334
- fontSize: 11,
5335
- fontWeight: 600,
5336
- padding: "1px 6px",
5337
- borderRadius: 4,
5338
- backgroundColor: ACTION_COLORS[entry.action],
5339
- color: "#fff"
5340
- },
5331
+ /* @__PURE__ */ jsx(Badge, {
5332
+ variant: ACTION_BADGE[entry.action],
5333
+ size: "sm",
5334
+ pill: true,
5341
5335
  children: actionLabels[entry.action]
5342
5336
  })
5343
5337
  ]
5344
5338
  }), changeKeys.length > 0 && /* @__PURE__ */ jsx("ul", {
5345
- style: {
5346
- margin: "4px 0 0 0",
5347
- padding: "0 0 0 16px"
5348
- },
5339
+ className: "nb-audit-trail__changes",
5349
5340
  children: changeKeys.map((field) => {
5350
5341
  const { before, after } = entry.changes[field];
5351
5342
  return /* @__PURE__ */ jsxs("li", {
5352
- style: {
5353
- fontSize: 12,
5354
- color: "#616161"
5355
- },
5356
- children: [
5357
- /* @__PURE__ */ jsxs("strong", { children: [field, ":"] }),
5358
- " ",
5359
- /* @__PURE__ */ jsx("span", {
5360
- style: { color: "#c62828" },
5361
- children: String(before ?? "—")
5362
- }),
5363
- " ",
5364
- /* @__PURE__ */ jsx("span", {
5365
- style: { color: "#2e7d32" },
5366
- children: String(after ?? "")
5367
- })
5368
- ]
5343
+ className: "nb-audit-trail__change",
5344
+ children: [/* @__PURE__ */ jsx("span", {
5345
+ className: "nb-audit-trail__field",
5346
+ children: resolveFieldLabel(field)
5347
+ }), /* @__PURE__ */ jsxs("div", {
5348
+ className: "nb-audit-trail__diff",
5349
+ children: [
5350
+ /* @__PURE__ */ jsx("span", {
5351
+ className: "nb-audit-trail__value nb-audit-trail__value--before",
5352
+ children: formatAuditValue(before, yesLabel, noLabel)
5353
+ }),
5354
+ /* @__PURE__ */ jsx("span", {
5355
+ className: "nb-audit-trail__arrow",
5356
+ "aria-hidden": "true",
5357
+ children: ""
5358
+ }),
5359
+ /* @__PURE__ */ jsx("span", {
5360
+ className: "nb-audit-trail__value nb-audit-trail__value--after",
5361
+ children: formatAuditValue(after, yesLabel, noLabel)
5362
+ })
5363
+ ]
5364
+ })]
5369
5365
  }, field);
5370
5366
  })
5371
- })]
5367
+ })] })]
5368
+ });
5369
+ }
5370
+ function AuditTrailSkeleton() {
5371
+ return /* @__PURE__ */ jsxs("div", {
5372
+ className: "nb-audit-trail__skeleton",
5373
+ "aria-busy": "true",
5374
+ children: [
5375
+ /* @__PURE__ */ jsx(Skeleton, {
5376
+ variant: "rect",
5377
+ height: 72
5378
+ }),
5379
+ /* @__PURE__ */ jsx(Skeleton, {
5380
+ variant: "rect",
5381
+ height: 72
5382
+ }),
5383
+ /* @__PURE__ */ jsx(Skeleton, {
5384
+ variant: "rect",
5385
+ height: 72
5386
+ })
5387
+ ]
5372
5388
  });
5373
5389
  }
5374
- function AuditTrailPanel({ url, renderEntry, visible, onClose }) {
5390
+ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, resolveFieldLabel, drawerSize = "sm" }) {
5375
5391
  const { t } = useCoreTranslation();
5376
5392
  const httpClient = useCoreHttpClient();
5377
5393
  const [fetchState, setFetchState] = useState({ status: "idle" });
5378
- useEffect(() => {
5379
- if (!visible || url === null) {
5394
+ const yesLabel = t("common.yes");
5395
+ const noLabel = t("common.no");
5396
+ const loadEntries = useCallback(() => {
5397
+ if (url === null) {
5380
5398
  setFetchState({ status: "idle" });
5381
5399
  return;
5382
5400
  }
5383
- let cancelled = false;
5384
5401
  setFetchState({ status: "loading" });
5385
5402
  httpClient.get(url).then((response) => {
5386
- if (!cancelled) setFetchState({
5403
+ setFetchState({
5387
5404
  status: "success",
5388
5405
  entries: response.data
5389
5406
  });
5390
5407
  }).catch(() => {
5391
- if (!cancelled) setFetchState({ status: "error" });
5408
+ setFetchState({ status: "error" });
5392
5409
  });
5393
- return () => {
5394
- cancelled = true;
5395
- };
5396
- }, [
5397
- httpClient,
5398
- url,
5399
- visible
5400
- ]);
5401
- if (!visible) return null;
5402
- return /* @__PURE__ */ jsxs("aside", {
5403
- className: "nb-audit-trail-panel",
5404
- style: {
5405
- border: "1px solid #e0e0e0",
5406
- borderRadius: 4,
5407
- padding: "12px 16px",
5408
- backgroundColor: "#fafafa",
5409
- minWidth: 280
5410
- },
5411
- children: [/* @__PURE__ */ jsxs("div", {
5412
- style: {
5413
- display: "flex",
5414
- alignItems: "center",
5415
- justifyContent: "space-between",
5416
- marginBottom: 12
5417
- },
5418
- children: [/* @__PURE__ */ jsx("strong", {
5419
- style: { fontSize: 14 },
5420
- children: t("auditTrail.title")
5421
- }), /* @__PURE__ */ jsx("button", {
5422
- type: "button",
5423
- onClick: onClose,
5424
- "aria-label": t("auditTrail.closeButton"),
5425
- style: {
5426
- background: "none",
5427
- border: "none",
5428
- cursor: "pointer",
5429
- fontSize: 18,
5430
- lineHeight: 1,
5431
- padding: "0 4px",
5432
- color: "#616161"
5433
- },
5434
- children: "×"
5435
- })]
5436
- }), url === null ? /* @__PURE__ */ jsx("p", {
5437
- style: {
5438
- fontSize: 13,
5439
- color: "#757575",
5440
- margin: 0
5441
- },
5442
- children: t("auditTrail.selectRecord")
5443
- }) : fetchState.status === "loading" ? /* @__PURE__ */ jsx("p", {
5444
- style: {
5445
- fontSize: 13,
5446
- color: "#757575",
5447
- margin: 0
5448
- },
5449
- children: t("auditTrail.loading")
5450
- }) : fetchState.status === "error" ? /* @__PURE__ */ jsx("p", {
5451
- style: {
5452
- fontSize: 13,
5453
- color: "#c62828",
5454
- margin: 0
5455
- },
5456
- children: t("auditTrail.error")
5457
- }) : fetchState.status === "success" ? fetchState.entries.length === 0 ? /* @__PURE__ */ jsx("p", {
5458
- style: {
5459
- fontSize: 13,
5460
- color: "#757575",
5461
- margin: 0
5462
- },
5463
- children: t("auditTrail.empty")
5464
- }) : /* @__PURE__ */ jsx("ul", {
5465
- style: {
5466
- margin: 0,
5467
- padding: 0
5468
- },
5469
- children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ jsx(React.Fragment, { children: renderEntry(entry) }, String(entry.id)) : /* @__PURE__ */ jsx(DefaultEntryRenderer, { entry }, String(entry.id)))
5470
- }) : null]
5410
+ }, [httpClient, url]);
5411
+ useEffect(() => {
5412
+ if (!visible) {
5413
+ setFetchState({ status: "idle" });
5414
+ return;
5415
+ }
5416
+ loadEntries();
5417
+ }, [loadEntries, visible]);
5418
+ const drawerTitle = /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: t("auditTrail.title") }), recordSubtitle && /* @__PURE__ */ jsx("p", {
5419
+ className: "nb-audit-trail__subtitle",
5420
+ children: recordSubtitle
5421
+ })] });
5422
+ const body = (() => {
5423
+ if (url === null) return /* @__PURE__ */ jsx(EmptyState, {
5424
+ fill: true,
5425
+ icon: "cursor-click",
5426
+ title: t("auditTrail.selectRecord"),
5427
+ description: t("auditTrail.selectRecordHint"),
5428
+ size: "sm"
5429
+ });
5430
+ if (fetchState.status === "loading") return /* @__PURE__ */ jsx(AuditTrailSkeleton, {});
5431
+ if (fetchState.status === "error") return /* @__PURE__ */ jsx("div", {
5432
+ className: "nb-audit-trail__error",
5433
+ children: /* @__PURE__ */ jsx(EmptyState, {
5434
+ fill: true,
5435
+ variant: "danger",
5436
+ icon: "warning-circle",
5437
+ title: t("auditTrail.error"),
5438
+ size: "sm",
5439
+ action: /* @__PURE__ */ jsx(Button, {
5440
+ variant: "secondary",
5441
+ size: "sm",
5442
+ onClick: loadEntries,
5443
+ children: t("auditTrail.retry")
5444
+ })
5445
+ })
5446
+ });
5447
+ if (fetchState.status === "success" && fetchState.entries.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
5448
+ fill: true,
5449
+ icon: "clock-counter-clockwise",
5450
+ title: t("auditTrail.empty"),
5451
+ description: t("auditTrail.emptyHint"),
5452
+ size: "sm"
5453
+ });
5454
+ if (fetchState.status === "success") return /* @__PURE__ */ jsx("ul", {
5455
+ className: "nb-audit-trail__timeline",
5456
+ children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ jsx("li", {
5457
+ className: "nb-audit-trail__entry",
5458
+ children: renderEntry(entry)
5459
+ }, String(entry.id)) : /* @__PURE__ */ jsx(DefaultEntryRenderer, {
5460
+ entry,
5461
+ resolveFieldLabel,
5462
+ yesLabel,
5463
+ noLabel
5464
+ }, String(entry.id)))
5465
+ });
5466
+ return null;
5467
+ })();
5468
+ return /* @__PURE__ */ jsx(Drawer, {
5469
+ isOpen: visible,
5470
+ onClose,
5471
+ title: drawerTitle,
5472
+ width: resolveDrawerWidth({ drawerSize }),
5473
+ side: "right",
5474
+ scrim: "subtle",
5475
+ closeLabel: t("auditTrail.closeButton"),
5476
+ "aria-label": t("auditTrail.title"),
5477
+ className: "nb-audit-trail-drawer",
5478
+ children: /* @__PURE__ */ jsx("div", {
5479
+ className: "nb-audit-trail",
5480
+ children: body
5481
+ })
5471
5482
  });
5472
5483
  }
5473
5484
  //#endregion
5485
+ //#region packages/crud/crud/AuditTrail.ts
5486
+ function createAuditFieldLabelResolver(config, fields) {
5487
+ const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5488
+ return (field) => {
5489
+ const fromConfig = config?.fieldLabels;
5490
+ if (fromConfig) {
5491
+ if (typeof fromConfig === "function") {
5492
+ const resolved = fromConfig(field);
5493
+ if (resolved) return resolved;
5494
+ } else if (fromConfig[field]) return fromConfig[field];
5495
+ }
5496
+ return fieldLabelByName.get(field) ?? field;
5497
+ };
5498
+ }
5499
+ //#endregion
5474
5500
  //#region packages/crud/crud/dialogStore.ts
5475
5501
  function initialDialogState() {
5476
5502
  return {
@@ -5818,13 +5844,30 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5818
5844
  if (hasAuditTrail) {
5819
5845
  const first = rows[0];
5820
5846
  if (first != null) setSelectedRowId(first["id"] ?? first["ID"] ?? null);
5847
+ else setSelectedRowId(null);
5821
5848
  }
5822
5849
  };
5850
+ const selectedRow = selectedRows[0] ?? null;
5823
5851
  const auditUrl = useMemo(() => {
5824
5852
  if (!resolvedResource.auditTrail?.enabled || selectedRowId == null) return null;
5825
5853
  const { apiUrl } = resolvedResource.auditTrail;
5826
5854
  return typeof apiUrl === "function" ? apiUrl(selectedRowId) : `${apiUrl}${selectedRowId}`;
5827
5855
  }, [resolvedResource.auditTrail, selectedRowId]);
5856
+ const resolveAuditFieldLabel = useMemo(() => createAuditFieldLabelResolver(resolvedResource.auditTrail, routeAwareGridFields), [resolvedResource.auditTrail, routeAwareGridFields]);
5857
+ const auditRecordSubtitle = useMemo(() => {
5858
+ if (!resolvedResource.auditTrail?.recordSubtitle || selectedRow == null) return void 0;
5859
+ return resolvedResource.auditTrail.recordSubtitle(selectedRow);
5860
+ }, [resolvedResource.auditTrail, selectedRow]);
5861
+ const openAuditTrail = useCallback((row) => {
5862
+ if (row != null) {
5863
+ const id = row["id"] ?? row["ID"] ?? null;
5864
+ if (id != null) {
5865
+ setSelectedRowId(id);
5866
+ setSelectedRows([row]);
5867
+ }
5868
+ }
5869
+ setAuditOpen(true);
5870
+ }, []);
5828
5871
  const executeBulkAction = useCallback(async (action) => {
5829
5872
  await action.onAction(selectionState.selectedIds);
5830
5873
  selectionState.clearSelection();
@@ -5838,7 +5881,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5838
5881
  else executeBulkAction(action);
5839
5882
  }, [executeBulkAction]);
5840
5883
  const toolbar = useMemo(() => {
5841
- return resolveResourceToolbar(resolvedResource, {
5884
+ const base = resolveResourceToolbar(resolvedResource, {
5842
5885
  resource: resolvedResource,
5843
5886
  selectedRow: selectedRows[0],
5844
5887
  selectedRows,
@@ -5846,14 +5889,48 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5846
5889
  formRef,
5847
5890
  events,
5848
5891
  emit
5849
- });
5892
+ }) ?? {};
5893
+ if (!hasAuditTrail) return base;
5894
+ return {
5895
+ ...base,
5896
+ utility: [...base.utility ?? [], {
5897
+ key: "audit-trail",
5898
+ text: t("crudPage.auditTrailButton"),
5899
+ icon: "ph-clock-counter-clockwise",
5900
+ hint: t("auditTrail.toolbarHint"),
5901
+ disabled: selectedRowId == null,
5902
+ onClick: () => openAuditTrail()
5903
+ }]
5904
+ };
5850
5905
  }, [
5851
5906
  emit,
5852
5907
  events,
5853
5908
  formRef,
5854
5909
  gridRef,
5910
+ hasAuditTrail,
5911
+ openAuditTrail,
5855
5912
  resolvedResource,
5856
- selectedRows
5913
+ selectedRowId,
5914
+ selectedRows,
5915
+ t
5916
+ ]);
5917
+ const rowActions = useMemo(() => {
5918
+ const base = resolvedResource.rowActions;
5919
+ if (!hasAuditTrail || resolvedResource.auditTrail?.rowAction === false) return base;
5920
+ const auditAction = (row) => ({
5921
+ key: "audit-trail",
5922
+ text: t("auditTrail.rowAction"),
5923
+ icon: "ph-clock-counter-clockwise",
5924
+ onClick: () => openAuditTrail(row)
5925
+ });
5926
+ if (typeof base === "function") return (row) => [...base(row), auditAction(row)];
5927
+ return (row) => [...base ?? [], auditAction(row)];
5928
+ }, [
5929
+ hasAuditTrail,
5930
+ openAuditTrail,
5931
+ resolvedResource.auditTrail?.rowAction,
5932
+ resolvedResource.rowActions,
5933
+ t
5857
5934
  ]);
5858
5935
  /** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
5859
5936
  const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ jsx(ColumnPresetSelector, {
@@ -5866,7 +5943,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5866
5943
  }) : void 0;
5867
5944
  return /* @__PURE__ */ jsxs("div", {
5868
5945
  id: "wrapper",
5869
- className: [auditOpen && "wrapper--with-audit", viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5946
+ className: [viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5870
5947
  children: [
5871
5948
  (resolvedResource.bulkActions?.length ?? 0) > 0 && selectionState.hasSelection && /* @__PURE__ */ jsxs("div", {
5872
5949
  className: "nb-bulk-toolbar",
@@ -5883,16 +5960,6 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5883
5960
  children: action.label
5884
5961
  }, action.key))]
5885
5962
  }),
5886
- hasAuditTrail && /* @__PURE__ */ jsx("div", {
5887
- className: "nb-audit-trail-toolbar",
5888
- style: { marginBottom: 8 },
5889
- children: /* @__PURE__ */ jsx("button", {
5890
- type: "button",
5891
- onClick: () => setAuditOpen((prev) => !prev),
5892
- "aria-pressed": auditOpen,
5893
- children: t("crudPage.auditTrailButton")
5894
- })
5895
- }),
5896
5963
  /* @__PURE__ */ jsx(NativeDataGridView, {
5897
5964
  ref: gridRef,
5898
5965
  id: resolvedResource.id,
@@ -5917,7 +5984,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5917
5984
  mode: resolvedResource.mode,
5918
5985
  stateStoringEnabled: resolvedResource.stateStoring,
5919
5986
  toolbar,
5920
- rowActions: resolvedResource.rowActions,
5987
+ rowActions,
5921
5988
  onAdd: requestNew,
5922
5989
  onEdit: requestEdit,
5923
5990
  onView: requestView,
@@ -5990,7 +6057,10 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5990
6057
  url: auditUrl,
5991
6058
  renderEntry: resolvedResource.auditTrail.renderEntry,
5992
6059
  visible: auditOpen,
5993
- onClose: () => setAuditOpen(false)
6060
+ onClose: () => setAuditOpen(false),
6061
+ recordSubtitle: auditRecordSubtitle,
6062
+ resolveFieldLabel: resolveAuditFieldLabel,
6063
+ drawerSize: resolvedResource.auditTrail?.drawerSize
5994
6064
  }),
5995
6065
  /* @__PURE__ */ jsx(ConfirmDialog, {
5996
6066
  open: confirmState.open,
@@ -6888,20 +6958,22 @@ function ResourceSchemaProvider({ children, resolver }) {
6888
6958
  children
6889
6959
  });
6890
6960
  }
6891
- function resolveWithRuntimeErrors(resolver, supportedOperations = []) {
6961
+ function resolveWithRuntimeErrors(resolver, supportedOperations = [], formLayout) {
6892
6962
  try {
6893
6963
  return {
6894
6964
  fields: resolver(),
6895
6965
  isLoading: false,
6896
6966
  error: void 0,
6897
- supportedOperations
6967
+ supportedOperations,
6968
+ formLayout
6898
6969
  };
6899
6970
  } catch (runtimeError) {
6900
6971
  return {
6901
6972
  fields: [],
6902
6973
  isLoading: false,
6903
6974
  error: runtimeError instanceof Error ? runtimeError : new Error(String(runtimeError)),
6904
- supportedOperations
6975
+ supportedOperations,
6976
+ formLayout
6905
6977
  };
6906
6978
  }
6907
6979
  }
@@ -6927,7 +6999,7 @@ function useResolvedResourceFields({ apiUrl, manualFields, overrides, fieldContr
6927
6999
  baselineFields: baseline.fields,
6928
7000
  contract: fieldContract,
6929
7001
  legacyOverrides: fieldContract ? void 0 : overrides
6930
- }), baseline.supportedOperations);
7002
+ }), baseline.supportedOperations, baseline.formLayout);
6931
7003
  }, [
6932
7004
  baseline,
6933
7005
  fieldContract,
@@ -7007,7 +7079,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7007
7079
  const effectiveGridRef = gridRef ?? internalGridRef;
7008
7080
  const resolvedBaseResource = useMemo(() => resolveCrudResource(resource), [resource]);
7009
7081
  const hasManualFields = !resource.fieldContract && Array.isArray(resource.fields) && resource.fields.length > 0;
7010
- const { fields, isLoading, error, supportedOperations } = useResolvedResourceFields({
7082
+ const { fields, isLoading, error, supportedOperations, formLayout: inferredFormLayout } = useResolvedResourceFields({
7011
7083
  apiUrl: resolvedBaseResource.apiUrl,
7012
7084
  manualFields: hasManualFields ? buildFields(resource.fields) : void 0,
7013
7085
  overrides: hasManualFields ? void 0 : fieldOverrides,
@@ -7042,11 +7114,13 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7042
7114
  apiUrl: normalizedApiUrl,
7043
7115
  fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7044
7116
  formFields: processedFields,
7117
+ formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7045
7118
  _supportedOperations: supportedOperations
7046
7119
  }), [
7047
7120
  fields,
7048
7121
  gridFields,
7049
7122
  hasManualFields,
7123
+ inferredFormLayout,
7050
7124
  normalizedApiUrl,
7051
7125
  processedFields,
7052
7126
  resolvedBaseResource,