@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 +150 -80
- package/dist/index.mjs +151 -81
- package/dist/style.css +13 -60
- package/package.json +3 -3
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
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
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
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
})
|
|
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 =
|
|
5443
|
-
className: "nb-audit-
|
|
5444
|
-
children:
|
|
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)(
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
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
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
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
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
})
|
|
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 =
|
|
5419
|
-
className: "nb-audit-
|
|
5420
|
-
children:
|
|
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(
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
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-
|
|
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:
|
|
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-
|
|
3356
|
-
|
|
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-
|
|
3360
|
-
|
|
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.
|
|
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"
|