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