@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 +85 -20
- package/dist/index.mjs +85 -20
- package/dist/style.css +17 -3
- package/package.json +3 -3
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 =
|
|
5450
|
-
className: "nb-audit-
|
|
5451
|
-
children:
|
|
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 =
|
|
5426
|
-
className: "nb-audit-
|
|
5427
|
-
children:
|
|
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-
|
|
3302
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
60
|
-
"@nubitio/ui": "^0.5.
|
|
59
|
+
"@nubitio/core": "^0.5.3",
|
|
60
|
+
"@nubitio/ui": "^0.5.3"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"react-dropzone": "^15.0.0"
|