@nubitio/crud 0.5.2 → 0.5.4

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,84 @@ 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 normalizeAuditScalar(value) {
5327
+ if (value == null || value === "") return null;
5328
+ return value;
5329
+ }
5330
+ function auditValuesEqual(before, after) {
5331
+ return JSON.stringify(normalizeAuditScalar(before)) === JSON.stringify(normalizeAuditScalar(after));
5332
+ }
5333
+ function mergeAuditEntryGroup(entries) {
5334
+ const chronological = [...entries].sort((left, right) => {
5335
+ if (left.id < right.id) return -1;
5336
+ if (left.id > right.id) return 1;
5337
+ return 0;
5338
+ });
5339
+ const mergedChanges = {};
5340
+ const fields = /* @__PURE__ */ new Set();
5341
+ for (const entry of chronological) for (const field of Object.keys(entry.changes)) fields.add(field);
5342
+ for (const field of fields) {
5343
+ const first = chronological.find((entry) => field in entry.changes);
5344
+ const last = [...chronological].reverse().find((entry) => field in entry.changes);
5345
+ if (!first || !last) continue;
5346
+ const before = first.changes[field].before;
5347
+ const after = last.changes[field].after;
5348
+ if (!auditValuesEqual(before, after)) mergedChanges[field] = {
5349
+ before,
5350
+ after
5351
+ };
5352
+ }
5353
+ if (Object.keys(mergedChanges).length === 0) return null;
5354
+ return {
5355
+ ...chronological[chronological.length - 1],
5356
+ changes: mergedChanges
5357
+ };
5358
+ }
5359
+ /**
5360
+ * Merges burst audit rows that share the same second, user, and action.
5361
+ * Keeps the earliest "before" and latest "after" per field, and drops
5362
+ * entries whose net diff is empty (e.g. clear-then-restore in one save).
5363
+ */
5364
+ function consolidateAuditEntries(entries) {
5365
+ if (entries.length <= 1) return entries;
5366
+ const consolidated = [];
5367
+ let group = [];
5368
+ const flushGroup = () => {
5369
+ if (group.length === 0) return;
5370
+ if (group.length === 1) consolidated.push(group[0]);
5371
+ else {
5372
+ const merged = mergeAuditEntryGroup(group);
5373
+ if (merged) consolidated.push(merged);
5374
+ }
5375
+ group = [];
5376
+ };
5377
+ const groupKey = (entry) => `${entry.timestamp.slice(0, 19)}|${entry.user}|${entry.action}`;
5378
+ for (const entry of entries) {
5379
+ if (group.length === 0 || groupKey(group[0]) === groupKey(entry)) {
5380
+ group.push(entry);
5381
+ continue;
5382
+ }
5383
+ flushGroup();
5384
+ group.push(entry);
5385
+ }
5386
+ flushGroup();
5387
+ return consolidated;
5388
+ }
5389
+ //#endregion
5312
5390
  //#region packages/crud/crud/AuditTrailPanel.tsx
5313
5391
  const ACTION_BADGE = {
5314
5392
  create: "success",
@@ -5433,7 +5511,7 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5433
5511
  httpClient.get(url).then((response) => {
5434
5512
  setFetchState({
5435
5513
  status: "success",
5436
- entries: response.data
5514
+ entries: consolidateAuditEntries(response.data)
5437
5515
  });
5438
5516
  }).catch(() => {
5439
5517
  setFetchState({ status: "error" });
@@ -5446,10 +5524,16 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5446
5524
  }
5447
5525
  loadEntries();
5448
5526
  }, [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
- })] });
5527
+ const drawerTitle = recordSubtitle ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5528
+ className: "nb-audit-trail__header",
5529
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5530
+ className: "nb-audit-trail__record",
5531
+ children: recordSubtitle
5532
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5533
+ className: "nb-audit-trail__drawer-label",
5534
+ children: t("auditTrail.title")
5535
+ })]
5536
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("auditTrail.title") });
5453
5537
  const body = (() => {
5454
5538
  if (url === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5455
5539
  fill: true,
@@ -5511,21 +5595,6 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5511
5595
  });
5512
5596
  }
5513
5597
  //#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
5598
  //#region packages/crud/crud/dialogStore.ts
5530
5599
  function initialDialogState() {
5531
5600
  return {
package/dist/index.mjs CHANGED
@@ -5285,6 +5285,84 @@ 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 normalizeAuditScalar(value) {
5303
+ if (value == null || value === "") return null;
5304
+ return value;
5305
+ }
5306
+ function auditValuesEqual(before, after) {
5307
+ return JSON.stringify(normalizeAuditScalar(before)) === JSON.stringify(normalizeAuditScalar(after));
5308
+ }
5309
+ function mergeAuditEntryGroup(entries) {
5310
+ const chronological = [...entries].sort((left, right) => {
5311
+ if (left.id < right.id) return -1;
5312
+ if (left.id > right.id) return 1;
5313
+ return 0;
5314
+ });
5315
+ const mergedChanges = {};
5316
+ const fields = /* @__PURE__ */ new Set();
5317
+ for (const entry of chronological) for (const field of Object.keys(entry.changes)) fields.add(field);
5318
+ for (const field of fields) {
5319
+ const first = chronological.find((entry) => field in entry.changes);
5320
+ const last = [...chronological].reverse().find((entry) => field in entry.changes);
5321
+ if (!first || !last) continue;
5322
+ const before = first.changes[field].before;
5323
+ const after = last.changes[field].after;
5324
+ if (!auditValuesEqual(before, after)) mergedChanges[field] = {
5325
+ before,
5326
+ after
5327
+ };
5328
+ }
5329
+ if (Object.keys(mergedChanges).length === 0) return null;
5330
+ return {
5331
+ ...chronological[chronological.length - 1],
5332
+ changes: mergedChanges
5333
+ };
5334
+ }
5335
+ /**
5336
+ * Merges burst audit rows that share the same second, user, and action.
5337
+ * Keeps the earliest "before" and latest "after" per field, and drops
5338
+ * entries whose net diff is empty (e.g. clear-then-restore in one save).
5339
+ */
5340
+ function consolidateAuditEntries(entries) {
5341
+ if (entries.length <= 1) return entries;
5342
+ const consolidated = [];
5343
+ let group = [];
5344
+ const flushGroup = () => {
5345
+ if (group.length === 0) return;
5346
+ if (group.length === 1) consolidated.push(group[0]);
5347
+ else {
5348
+ const merged = mergeAuditEntryGroup(group);
5349
+ if (merged) consolidated.push(merged);
5350
+ }
5351
+ group = [];
5352
+ };
5353
+ const groupKey = (entry) => `${entry.timestamp.slice(0, 19)}|${entry.user}|${entry.action}`;
5354
+ for (const entry of entries) {
5355
+ if (group.length === 0 || groupKey(group[0]) === groupKey(entry)) {
5356
+ group.push(entry);
5357
+ continue;
5358
+ }
5359
+ flushGroup();
5360
+ group.push(entry);
5361
+ }
5362
+ flushGroup();
5363
+ return consolidated;
5364
+ }
5365
+ //#endregion
5288
5366
  //#region packages/crud/crud/AuditTrailPanel.tsx
5289
5367
  const ACTION_BADGE = {
5290
5368
  create: "success",
@@ -5409,7 +5487,7 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5409
5487
  httpClient.get(url).then((response) => {
5410
5488
  setFetchState({
5411
5489
  status: "success",
5412
- entries: response.data
5490
+ entries: consolidateAuditEntries(response.data)
5413
5491
  });
5414
5492
  }).catch(() => {
5415
5493
  setFetchState({ status: "error" });
@@ -5422,10 +5500,16 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5422
5500
  }
5423
5501
  loadEntries();
5424
5502
  }, [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
- })] });
5503
+ const drawerTitle = recordSubtitle ? /* @__PURE__ */ jsxs("div", {
5504
+ className: "nb-audit-trail__header",
5505
+ children: [/* @__PURE__ */ jsx("p", {
5506
+ className: "nb-audit-trail__record",
5507
+ children: recordSubtitle
5508
+ }), /* @__PURE__ */ jsx("p", {
5509
+ className: "nb-audit-trail__drawer-label",
5510
+ children: t("auditTrail.title")
5511
+ })]
5512
+ }) : /* @__PURE__ */ jsx("div", { children: t("auditTrail.title") });
5429
5513
  const body = (() => {
5430
5514
  if (url === null) return /* @__PURE__ */ jsx(EmptyState, {
5431
5515
  fill: true,
@@ -5487,21 +5571,6 @@ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, r
5487
5571
  });
5488
5572
  }
5489
5573
  //#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
5574
  //#region packages/crud/crud/dialogStore.ts
5506
5575
  function initialDialogState() {
5507
5576
  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.4",
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.4",
60
+ "@nubitio/ui": "^0.5.4"
61
61
  },
62
62
  "dependencies": {
63
63
  "react-dropzone": "^15.0.0"