@nubitio/crud 0.5.1 → 0.5.3

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
@@ -5309,12 +5309,91 @@ function useCrudPage(resource, externalFormRef) {
5309
5309
  };
5310
5310
  }
5311
5311
  //#endregion
5312
+ //#region packages/crud/crud/AuditTrail.ts
5313
+ function createAuditFieldLabelResolver(config, fields) {
5314
+ const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5315
+ return (field) => {
5316
+ const fromConfig = config?.fieldLabels;
5317
+ if (fromConfig) {
5318
+ if (typeof fromConfig === "function") {
5319
+ const resolved = fromConfig(field);
5320
+ if (resolved) return resolved;
5321
+ } else if (fromConfig[field]) return fromConfig[field];
5322
+ }
5323
+ return fieldLabelByName.get(field) ?? field;
5324
+ };
5325
+ }
5326
+ function auditValuesEqual(before, after) {
5327
+ return JSON.stringify(before) === JSON.stringify(after);
5328
+ }
5329
+ function mergeAuditEntryGroup(entries) {
5330
+ const chronological = [...entries].sort((left, right) => {
5331
+ if (left.id < right.id) return -1;
5332
+ if (left.id > right.id) return 1;
5333
+ return 0;
5334
+ });
5335
+ const mergedChanges = {};
5336
+ const fields = /* @__PURE__ */ new Set();
5337
+ for (const entry of chronological) for (const field of Object.keys(entry.changes)) fields.add(field);
5338
+ for (const field of fields) {
5339
+ const first = chronological.find((entry) => field in entry.changes);
5340
+ const last = [...chronological].reverse().find((entry) => field in entry.changes);
5341
+ if (!first || !last) continue;
5342
+ const before = first.changes[field].before;
5343
+ const after = last.changes[field].after;
5344
+ if (!auditValuesEqual(before, after)) mergedChanges[field] = {
5345
+ before,
5346
+ after
5347
+ };
5348
+ }
5349
+ if (Object.keys(mergedChanges).length === 0) return null;
5350
+ return {
5351
+ ...chronological[chronological.length - 1],
5352
+ changes: mergedChanges
5353
+ };
5354
+ }
5355
+ /**
5356
+ * Merges burst audit rows that share the same second, user, and action.
5357
+ * Keeps the earliest "before" and latest "after" per field, and drops
5358
+ * entries whose net diff is empty (e.g. clear-then-restore in one save).
5359
+ */
5360
+ function consolidateAuditEntries(entries) {
5361
+ if (entries.length <= 1) return entries;
5362
+ const consolidated = [];
5363
+ let group = [];
5364
+ const flushGroup = () => {
5365
+ if (group.length === 0) return;
5366
+ if (group.length === 1) consolidated.push(group[0]);
5367
+ else {
5368
+ const merged = mergeAuditEntryGroup(group);
5369
+ if (merged) consolidated.push(merged);
5370
+ }
5371
+ group = [];
5372
+ };
5373
+ const groupKey = (entry) => `${entry.timestamp.slice(0, 19)}|${entry.user}|${entry.action}`;
5374
+ for (const entry of entries) {
5375
+ if (group.length === 0 || groupKey(group[0]) === groupKey(entry)) {
5376
+ group.push(entry);
5377
+ continue;
5378
+ }
5379
+ flushGroup();
5380
+ group.push(entry);
5381
+ }
5382
+ flushGroup();
5383
+ return consolidated;
5384
+ }
5385
+ //#endregion
5312
5386
  //#region packages/crud/crud/AuditTrailPanel.tsx
5313
5387
  const ACTION_BADGE = {
5314
5388
  create: "success",
5315
5389
  update: "info",
5316
5390
  delete: "danger"
5317
5391
  };
5392
+ const ACTION_TONE = {
5393
+ create: "success",
5394
+ update: "info",
5395
+ delete: "danger"
5396
+ };
5318
5397
  function formatAuditValue(value, yesLabel, noLabel) {
5319
5398
  if (value == null || value === "") return "—";
5320
5399
  if (typeof value === "boolean") return value ? yesLabel : noLabel;
@@ -5325,6 +5404,40 @@ function formatAuditValue(value, yesLabel, noLabel) {
5325
5404
  }
5326
5405
  return String(value);
5327
5406
  }
5407
+ function DefaultEntryContent({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5408
+ const changeKeys = Object.keys(entry.changes);
5409
+ if (changeKeys.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
5410
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5411
+ className: "nb-audit-trail__changes",
5412
+ children: changeKeys.map((field) => {
5413
+ const { before, after } = entry.changes[field];
5414
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5415
+ className: "nb-audit-trail__change",
5416
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5417
+ className: "nb-audit-trail__field",
5418
+ children: resolveFieldLabel(field)
5419
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5420
+ className: "nb-audit-trail__diff",
5421
+ children: [
5422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5423
+ className: "nb-audit-trail__value nb-audit-trail__value--before",
5424
+ children: formatAuditValue(before, yesLabel, noLabel)
5425
+ }),
5426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5427
+ className: "nb-audit-trail__arrow",
5428
+ "aria-hidden": "true",
5429
+ children: "→"
5430
+ }),
5431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5432
+ className: "nb-audit-trail__value nb-audit-trail__value--after",
5433
+ children: formatAuditValue(after, yesLabel, noLabel)
5434
+ })
5435
+ ]
5436
+ })]
5437
+ }, field);
5438
+ })
5439
+ });
5440
+ }
5328
5441
  function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5329
5442
  const { t } = (0, _nubitio_core.useCoreTranslation)();
5330
5443
  const date = new Date(entry.timestamp);
@@ -5334,61 +5447,29 @@ function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5334
5447
  update: t("auditTrail.action.update"),
5335
5448
  delete: t("auditTrail.action.delete")
5336
5449
  };
5337
- const changeKeys = Object.keys(entry.changes);
5338
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
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", {
5450
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.TimelineItem, {
5451
+ status: "complete",
5452
+ tone: ACTION_TONE[entry.action],
5453
+ title: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5344
5454
  className: "nb-audit-trail__meta",
5345
- children: [
5346
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
5347
- className: "nb-audit-trail__timestamp",
5348
- dateTime: entry.timestamp,
5349
- children: formatted
5350
- }),
5351
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5352
- className: "nb-audit-trail__user",
5353
- children: entry.user
5354
- }),
5355
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Badge, {
5356
- variant: ACTION_BADGE[entry.action],
5357
- size: "sm",
5358
- pill: true,
5359
- children: actionLabels[entry.action]
5360
- })
5361
- ]
5362
- }), changeKeys.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5363
- className: "nb-audit-trail__changes",
5364
- children: changeKeys.map((field) => {
5365
- const { before, after } = entry.changes[field];
5366
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
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
- })]
5389
- }, field);
5390
- })
5391
- })] })]
5455
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5456
+ className: "nb-audit-trail__user",
5457
+ children: entry.user
5458
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Badge, {
5459
+ variant: ACTION_BADGE[entry.action],
5460
+ size: "sm",
5461
+ pill: true,
5462
+ children: actionLabels[entry.action]
5463
+ })]
5464
+ }),
5465
+ timestamp: formatted,
5466
+ dateTime: entry.timestamp,
5467
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultEntryContent, {
5468
+ entry,
5469
+ resolveFieldLabel,
5470
+ yesLabel,
5471
+ noLabel
5472
+ })
5392
5473
  });
5393
5474
  }
5394
5475
  function AuditTrailSkeleton() {
@@ -5426,7 +5507,7 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5426
5507
  httpClient.get(url).then((response) => {
5427
5508
  setFetchState({
5428
5509
  status: "success",
5429
- entries: response.data
5510
+ entries: consolidateAuditEntries(response.data)
5430
5511
  });
5431
5512
  }).catch(() => {
5432
5513
  setFetchState({ status: "error" });
@@ -5439,10 +5520,16 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5439
5520
  }
5440
5521
  loadEntries();
5441
5522
  }, [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
- })] });
5523
+ const drawerTitle = recordSubtitle ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5524
+ className: "nb-audit-trail__header",
5525
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5526
+ className: "nb-audit-trail__record",
5527
+ children: recordSubtitle
5528
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5529
+ className: "nb-audit-trail__drawer-label",
5530
+ children: t("auditTrail.title")
5531
+ })]
5532
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("auditTrail.title") });
5446
5533
  const body = (() => {
5447
5534
  if (url === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5448
5535
  fill: true,
@@ -5475,12 +5562,10 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5475
5562
  description: t("auditTrail.emptyHint"),
5476
5563
  size: "sm"
5477
5564
  });
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, {
5565
+ if (fetchState.status === "success") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Timeline, {
5566
+ variant: "log",
5567
+ "aria-label": t("auditTrail.title"),
5568
+ 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, {
5484
5569
  entry,
5485
5570
  resolveFieldLabel,
5486
5571
  yesLabel,
@@ -5506,21 +5591,6 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5506
5591
  });
5507
5592
  }
5508
5593
  //#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
5524
5594
  //#region packages/crud/crud/dialogStore.ts
5525
5595
  function initialDialogState() {
5526
5596
  return {
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, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, IconButton, Skeleton } from "@nubitio/ui";
4
+ import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, IconButton, Skeleton, Timeline, TimelineItem } 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";
@@ -5285,12 +5285,91 @@ function useCrudPage(resource, externalFormRef) {
5285
5285
  };
5286
5286
  }
5287
5287
  //#endregion
5288
+ //#region packages/crud/crud/AuditTrail.ts
5289
+ function createAuditFieldLabelResolver(config, fields) {
5290
+ const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5291
+ return (field) => {
5292
+ const fromConfig = config?.fieldLabels;
5293
+ if (fromConfig) {
5294
+ if (typeof fromConfig === "function") {
5295
+ const resolved = fromConfig(field);
5296
+ if (resolved) return resolved;
5297
+ } else if (fromConfig[field]) return fromConfig[field];
5298
+ }
5299
+ return fieldLabelByName.get(field) ?? field;
5300
+ };
5301
+ }
5302
+ function auditValuesEqual(before, after) {
5303
+ return JSON.stringify(before) === JSON.stringify(after);
5304
+ }
5305
+ function mergeAuditEntryGroup(entries) {
5306
+ const chronological = [...entries].sort((left, right) => {
5307
+ if (left.id < right.id) return -1;
5308
+ if (left.id > right.id) return 1;
5309
+ return 0;
5310
+ });
5311
+ const mergedChanges = {};
5312
+ const fields = /* @__PURE__ */ new Set();
5313
+ for (const entry of chronological) for (const field of Object.keys(entry.changes)) fields.add(field);
5314
+ for (const field of fields) {
5315
+ const first = chronological.find((entry) => field in entry.changes);
5316
+ const last = [...chronological].reverse().find((entry) => field in entry.changes);
5317
+ if (!first || !last) continue;
5318
+ const before = first.changes[field].before;
5319
+ const after = last.changes[field].after;
5320
+ if (!auditValuesEqual(before, after)) mergedChanges[field] = {
5321
+ before,
5322
+ after
5323
+ };
5324
+ }
5325
+ if (Object.keys(mergedChanges).length === 0) return null;
5326
+ return {
5327
+ ...chronological[chronological.length - 1],
5328
+ changes: mergedChanges
5329
+ };
5330
+ }
5331
+ /**
5332
+ * Merges burst audit rows that share the same second, user, and action.
5333
+ * Keeps the earliest "before" and latest "after" per field, and drops
5334
+ * entries whose net diff is empty (e.g. clear-then-restore in one save).
5335
+ */
5336
+ function consolidateAuditEntries(entries) {
5337
+ if (entries.length <= 1) return entries;
5338
+ const consolidated = [];
5339
+ let group = [];
5340
+ const flushGroup = () => {
5341
+ if (group.length === 0) return;
5342
+ if (group.length === 1) consolidated.push(group[0]);
5343
+ else {
5344
+ const merged = mergeAuditEntryGroup(group);
5345
+ if (merged) consolidated.push(merged);
5346
+ }
5347
+ group = [];
5348
+ };
5349
+ const groupKey = (entry) => `${entry.timestamp.slice(0, 19)}|${entry.user}|${entry.action}`;
5350
+ for (const entry of entries) {
5351
+ if (group.length === 0 || groupKey(group[0]) === groupKey(entry)) {
5352
+ group.push(entry);
5353
+ continue;
5354
+ }
5355
+ flushGroup();
5356
+ group.push(entry);
5357
+ }
5358
+ flushGroup();
5359
+ return consolidated;
5360
+ }
5361
+ //#endregion
5288
5362
  //#region packages/crud/crud/AuditTrailPanel.tsx
5289
5363
  const ACTION_BADGE = {
5290
5364
  create: "success",
5291
5365
  update: "info",
5292
5366
  delete: "danger"
5293
5367
  };
5368
+ const ACTION_TONE = {
5369
+ create: "success",
5370
+ update: "info",
5371
+ delete: "danger"
5372
+ };
5294
5373
  function formatAuditValue(value, yesLabel, noLabel) {
5295
5374
  if (value == null || value === "") return "—";
5296
5375
  if (typeof value === "boolean") return value ? yesLabel : noLabel;
@@ -5301,6 +5380,40 @@ function formatAuditValue(value, yesLabel, noLabel) {
5301
5380
  }
5302
5381
  return String(value);
5303
5382
  }
5383
+ function DefaultEntryContent({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5384
+ const changeKeys = Object.keys(entry.changes);
5385
+ if (changeKeys.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
5386
+ return /* @__PURE__ */ jsx("ul", {
5387
+ className: "nb-audit-trail__changes",
5388
+ children: changeKeys.map((field) => {
5389
+ const { before, after } = entry.changes[field];
5390
+ return /* @__PURE__ */ jsxs("li", {
5391
+ className: "nb-audit-trail__change",
5392
+ children: [/* @__PURE__ */ jsx("span", {
5393
+ className: "nb-audit-trail__field",
5394
+ children: resolveFieldLabel(field)
5395
+ }), /* @__PURE__ */ jsxs("div", {
5396
+ className: "nb-audit-trail__diff",
5397
+ children: [
5398
+ /* @__PURE__ */ jsx("span", {
5399
+ className: "nb-audit-trail__value nb-audit-trail__value--before",
5400
+ children: formatAuditValue(before, yesLabel, noLabel)
5401
+ }),
5402
+ /* @__PURE__ */ jsx("span", {
5403
+ className: "nb-audit-trail__arrow",
5404
+ "aria-hidden": "true",
5405
+ children: "→"
5406
+ }),
5407
+ /* @__PURE__ */ jsx("span", {
5408
+ className: "nb-audit-trail__value nb-audit-trail__value--after",
5409
+ children: formatAuditValue(after, yesLabel, noLabel)
5410
+ })
5411
+ ]
5412
+ })]
5413
+ }, field);
5414
+ })
5415
+ });
5416
+ }
5304
5417
  function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5305
5418
  const { t } = useCoreTranslation();
5306
5419
  const date = new Date(entry.timestamp);
@@ -5310,61 +5423,29 @@ function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5310
5423
  update: t("auditTrail.action.update"),
5311
5424
  delete: t("auditTrail.action.delete")
5312
5425
  };
5313
- const changeKeys = Object.keys(entry.changes);
5314
- return /* @__PURE__ */ jsxs("li", {
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", {
5426
+ return /* @__PURE__ */ jsx(TimelineItem, {
5427
+ status: "complete",
5428
+ tone: ACTION_TONE[entry.action],
5429
+ title: /* @__PURE__ */ jsxs("span", {
5320
5430
  className: "nb-audit-trail__meta",
5321
- children: [
5322
- /* @__PURE__ */ jsx("time", {
5323
- className: "nb-audit-trail__timestamp",
5324
- dateTime: entry.timestamp,
5325
- children: formatted
5326
- }),
5327
- /* @__PURE__ */ jsx("span", {
5328
- className: "nb-audit-trail__user",
5329
- children: entry.user
5330
- }),
5331
- /* @__PURE__ */ jsx(Badge, {
5332
- variant: ACTION_BADGE[entry.action],
5333
- size: "sm",
5334
- pill: true,
5335
- children: actionLabels[entry.action]
5336
- })
5337
- ]
5338
- }), changeKeys.length > 0 && /* @__PURE__ */ jsx("ul", {
5339
- className: "nb-audit-trail__changes",
5340
- children: changeKeys.map((field) => {
5341
- const { before, after } = entry.changes[field];
5342
- return /* @__PURE__ */ jsxs("li", {
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
- })]
5365
- }, field);
5366
- })
5367
- })] })]
5431
+ children: [/* @__PURE__ */ jsx("span", {
5432
+ className: "nb-audit-trail__user",
5433
+ children: entry.user
5434
+ }), /* @__PURE__ */ jsx(Badge, {
5435
+ variant: ACTION_BADGE[entry.action],
5436
+ size: "sm",
5437
+ pill: true,
5438
+ children: actionLabels[entry.action]
5439
+ })]
5440
+ }),
5441
+ timestamp: formatted,
5442
+ dateTime: entry.timestamp,
5443
+ children: /* @__PURE__ */ jsx(DefaultEntryContent, {
5444
+ entry,
5445
+ resolveFieldLabel,
5446
+ yesLabel,
5447
+ noLabel
5448
+ })
5368
5449
  });
5369
5450
  }
5370
5451
  function AuditTrailSkeleton() {
@@ -5402,7 +5483,7 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5402
5483
  httpClient.get(url).then((response) => {
5403
5484
  setFetchState({
5404
5485
  status: "success",
5405
- entries: response.data
5486
+ entries: consolidateAuditEntries(response.data)
5406
5487
  });
5407
5488
  }).catch(() => {
5408
5489
  setFetchState({ status: "error" });
@@ -5415,10 +5496,16 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5415
5496
  }
5416
5497
  loadEntries();
5417
5498
  }, [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
- })] });
5499
+ const drawerTitle = recordSubtitle ? /* @__PURE__ */ jsxs("div", {
5500
+ className: "nb-audit-trail__header",
5501
+ children: [/* @__PURE__ */ jsx("p", {
5502
+ className: "nb-audit-trail__record",
5503
+ children: recordSubtitle
5504
+ }), /* @__PURE__ */ jsx("p", {
5505
+ className: "nb-audit-trail__drawer-label",
5506
+ children: t("auditTrail.title")
5507
+ })]
5508
+ }) : /* @__PURE__ */ jsx("div", { children: t("auditTrail.title") });
5422
5509
  const body = (() => {
5423
5510
  if (url === null) return /* @__PURE__ */ jsx(EmptyState, {
5424
5511
  fill: true,
@@ -5451,12 +5538,10 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5451
5538
  description: t("auditTrail.emptyHint"),
5452
5539
  size: "sm"
5453
5540
  });
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, {
5541
+ if (fetchState.status === "success") return /* @__PURE__ */ jsx(Timeline, {
5542
+ variant: "log",
5543
+ "aria-label": t("auditTrail.title"),
5544
+ children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ jsx(React.Fragment, { children: renderEntry(entry) }, String(entry.id)) : /* @__PURE__ */ jsx(DefaultEntryRenderer, {
5460
5545
  entry,
5461
5546
  resolveFieldLabel,
5462
5547
  yesLabel,
@@ -5482,21 +5567,6 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5482
5567
  });
5483
5568
  }
5484
5569
  //#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
5500
5570
  //#region packages/crud/crud/dialogStore.ts
5501
5571
  function initialDialogState() {
5502
5572
  return {
package/dist/style.css CHANGED
@@ -3298,66 +3298,25 @@ html[data-density=compact] .nb-crud-page-shell__footer {
3298
3298
  min-height: 0;
3299
3299
  }
3300
3300
 
3301
- .nb-audit-trail__subtitle {
3302
- color: var(--text-secondary);
3303
- font-size: var(--font-size-sm, 0.8125rem);
3304
- line-height: 1.4;
3305
- margin: calc(var(--space-1) * -1) 0 0;
3306
- }
3307
-
3308
- .nb-audit-trail__timeline {
3301
+ .nb-audit-trail__header {
3309
3302
  display: flex;
3310
3303
  flex-direction: column;
3311
- gap: 0;
3312
- list-style: none;
3313
- margin: 0;
3314
- padding: 0;
3315
- }
3316
-
3317
- .nb-audit-trail__entry {
3318
- display: grid;
3319
- gap: var(--space-2);
3320
- grid-template-columns: 12px 1fr;
3321
- padding: var(--space-3) 0;
3322
- position: relative;
3323
- }
3324
- .nb-audit-trail__entry:not(:last-child) {
3325
- border-bottom: 1px solid var(--border-color);
3326
- }
3327
- .nb-audit-trail__entry::before {
3328
- background: var(--border-color);
3329
- bottom: 0;
3330
- content: "";
3331
- left: 5px;
3332
- position: absolute;
3333
- top: calc(var(--space-3) + 6px);
3334
- width: 2px;
3335
- }
3336
- .nb-audit-trail__entry:last-child::before {
3337
- display: none;
3338
- }
3339
-
3340
- .nb-audit-trail__marker {
3341
- background: var(--surface-2);
3342
- border: 2px solid var(--accent-color);
3343
- border-radius: 50%;
3344
- height: 12px;
3345
- margin-top: 4px;
3346
- position: relative;
3347
- width: 12px;
3348
- z-index: 1;
3349
- }
3350
-
3351
- .nb-audit-trail__entry--create .nb-audit-trail__marker {
3352
- border-color: var(--success-color);
3304
+ gap: var(--space-1);
3353
3305
  }
3354
3306
 
3355
- .nb-audit-trail__entry--update .nb-audit-trail__marker {
3356
- border-color: var(--info-color, var(--accent-color));
3307
+ .nb-audit-trail__record {
3308
+ color: var(--text-primary);
3309
+ font-size: var(--font-size-sm, 0.8125rem);
3310
+ font-weight: 600;
3311
+ line-height: 1.4;
3312
+ margin: 0;
3357
3313
  }
3358
3314
 
3359
- .nb-audit-trail__entry--delete .nb-audit-trail__marker {
3360
- border-color: var(--danger-color);
3315
+ .nb-audit-trail__drawer-label {
3316
+ color: var(--text-secondary);
3317
+ font-size: var(--font-size-xs, 0.75rem);
3318
+ line-height: 1.4;
3319
+ margin: 0;
3361
3320
  }
3362
3321
 
3363
3322
  .nb-audit-trail__meta {
@@ -3367,12 +3326,6 @@ html[data-density=compact] .nb-crud-page-shell__footer {
3367
3326
  gap: var(--space-2);
3368
3327
  }
3369
3328
 
3370
- .nb-audit-trail__timestamp {
3371
- color: var(--text-secondary);
3372
- font-size: var(--font-size-xs, 0.75rem);
3373
- font-variant-numeric: tabular-nums;
3374
- }
3375
-
3376
3329
  .nb-audit-trail__user {
3377
3330
  color: var(--text-primary);
3378
3331
  font-size: var(--font-size-sm, 0.8125rem);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/crud",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "description": "Declarative CRUD engine with field DSL, forms, datagrids, RBAC, conditional logic and pluggable adapters (Hydra/REST).",
6
6
  "license": "MIT",
@@ -56,8 +56,8 @@
56
56
  "react-dom": "^19.0.0",
57
57
  "react-i18next": "^14.0.0",
58
58
  "react-router-dom": "^6.0.0",
59
- "@nubitio/core": "^0.5.1",
60
- "@nubitio/ui": "^0.5.1"
59
+ "@nubitio/core": "^0.5.3",
60
+ "@nubitio/ui": "^0.5.3"
61
61
  },
62
62
  "dependencies": {
63
63
  "react-dropzone": "^15.0.0"