@nubitio/crud 0.5.2 → 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,6 +5309,80 @@ 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",
@@ -5433,7 +5507,7 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5433
5507
  httpClient.get(url).then((response) => {
5434
5508
  setFetchState({
5435
5509
  status: "success",
5436
- entries: response.data
5510
+ entries: consolidateAuditEntries(response.data)
5437
5511
  });
5438
5512
  }).catch(() => {
5439
5513
  setFetchState({ status: "error" });
@@ -5446,10 +5520,16 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5446
5520
  }
5447
5521
  loadEntries();
5448
5522
  }, [loadEntries, visible]);
5449
- 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", {
5450
- className: "nb-audit-trail__subtitle",
5451
- children: recordSubtitle
5452
- })] });
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") });
5453
5533
  const body = (() => {
5454
5534
  if (url === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5455
5535
  fill: true,
@@ -5511,21 +5591,6 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5511
5591
  });
5512
5592
  }
5513
5593
  //#endregion
5514
- //#region packages/crud/crud/AuditTrail.ts
5515
- function createAuditFieldLabelResolver(config, fields) {
5516
- const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5517
- return (field) => {
5518
- const fromConfig = config?.fieldLabels;
5519
- if (fromConfig) {
5520
- if (typeof fromConfig === "function") {
5521
- const resolved = fromConfig(field);
5522
- if (resolved) return resolved;
5523
- } else if (fromConfig[field]) return fromConfig[field];
5524
- }
5525
- return fieldLabelByName.get(field) ?? field;
5526
- };
5527
- }
5528
- //#endregion
5529
5594
  //#region packages/crud/crud/dialogStore.ts
5530
5595
  function initialDialogState() {
5531
5596
  return {
package/dist/index.mjs CHANGED
@@ -5285,6 +5285,80 @@ 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",
@@ -5409,7 +5483,7 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5409
5483
  httpClient.get(url).then((response) => {
5410
5484
  setFetchState({
5411
5485
  status: "success",
5412
- entries: response.data
5486
+ entries: consolidateAuditEntries(response.data)
5413
5487
  });
5414
5488
  }).catch(() => {
5415
5489
  setFetchState({ status: "error" });
@@ -5422,10 +5496,16 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5422
5496
  }
5423
5497
  loadEntries();
5424
5498
  }, [loadEntries, visible]);
5425
- const drawerTitle = /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: t("auditTrail.title") }), recordSubtitle && /* @__PURE__ */ jsx("p", {
5426
- className: "nb-audit-trail__subtitle",
5427
- children: recordSubtitle
5428
- })] });
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") });
5429
5509
  const body = (() => {
5430
5510
  if (url === null) return /* @__PURE__ */ jsx(EmptyState, {
5431
5511
  fill: true,
@@ -5487,21 +5567,6 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5487
5567
  });
5488
5568
  }
5489
5569
  //#endregion
5490
- //#region packages/crud/crud/AuditTrail.ts
5491
- function createAuditFieldLabelResolver(config, fields) {
5492
- const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5493
- return (field) => {
5494
- const fromConfig = config?.fieldLabels;
5495
- if (fromConfig) {
5496
- if (typeof fromConfig === "function") {
5497
- const resolved = fromConfig(field);
5498
- if (resolved) return resolved;
5499
- } else if (fromConfig[field]) return fromConfig[field];
5500
- }
5501
- return fieldLabelByName.get(field) ?? field;
5502
- };
5503
- }
5504
- //#endregion
5505
5570
  //#region packages/crud/crud/dialogStore.ts
5506
5571
  function initialDialogState() {
5507
5572
  return {
package/dist/style.css CHANGED
@@ -3298,11 +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);
3301
+ .nb-audit-trail__header {
3302
+ display: flex;
3303
+ flex-direction: column;
3304
+ gap: var(--space-1);
3305
+ }
3306
+
3307
+ .nb-audit-trail__record {
3308
+ color: var(--text-primary);
3303
3309
  font-size: var(--font-size-sm, 0.8125rem);
3310
+ font-weight: 600;
3304
3311
  line-height: 1.4;
3305
- margin: calc(var(--space-1) * -1) 0 0;
3312
+ margin: 0;
3313
+ }
3314
+
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;
3306
3320
  }
3307
3321
 
3308
3322
  .nb-audit-trail__meta {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/crud",
3
- "version": "0.5.2",
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.2",
60
- "@nubitio/ui": "^0.5.2"
59
+ "@nubitio/core": "^0.5.3",
60
+ "@nubitio/ui": "^0.5.3"
61
61
  },
62
62
  "dependencies": {
63
63
  "react-dropzone": "^15.0.0"