@nubitio/crud 0.5.23 → 0.5.26
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 +717 -224
- package/dist/index.d.cts +43 -24
- package/dist/index.d.mts +43 -24
- package/dist/index.mjs +658 -168
- package/dist/style.css +146 -0
- package/package.json +17 -3
package/dist/index.cjs
CHANGED
|
@@ -22,12 +22,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
24
|
let react = require("react");
|
|
25
|
-
react = __toESM(react, 1);
|
|
25
|
+
let react$1 = __toESM(react, 1);
|
|
26
|
+
react = __toESM(react);
|
|
26
27
|
let react_router_dom = require("react-router-dom");
|
|
27
28
|
let react_dom = require("react-dom");
|
|
28
29
|
let _nubitio_ui = require("@nubitio/ui");
|
|
29
30
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
30
31
|
let _nubitio_core = require("@nubitio/core");
|
|
32
|
+
let _tiptap_react = require("@tiptap/react");
|
|
33
|
+
let _tiptap_starter_kit = require("@tiptap/starter-kit");
|
|
34
|
+
_tiptap_starter_kit = __toESM(_tiptap_starter_kit);
|
|
35
|
+
let _tiptap_extension_link = require("@tiptap/extension-link");
|
|
36
|
+
_tiptap_extension_link = __toESM(_tiptap_extension_link);
|
|
31
37
|
let _tanstack_react_query = require("@tanstack/react-query");
|
|
32
38
|
//#region packages/crud/crud/defineResource.ts
|
|
33
39
|
const stringResourceCache = /* @__PURE__ */ new Map();
|
|
@@ -1452,19 +1458,179 @@ const fileTypeModule = {
|
|
|
1452
1458
|
}
|
|
1453
1459
|
};
|
|
1454
1460
|
//#endregion
|
|
1461
|
+
//#region packages/crud/field/registry/types/HtmlEditor.tsx
|
|
1462
|
+
function ToolbarButton({ active, disabled, title, onClick, children }) {
|
|
1463
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1464
|
+
type: "button",
|
|
1465
|
+
title,
|
|
1466
|
+
disabled,
|
|
1467
|
+
className: `nb-html-editor__btn${active ? " nb-html-editor__btn--active" : ""}`,
|
|
1468
|
+
onMouseDown: (e) => {
|
|
1469
|
+
e.preventDefault();
|
|
1470
|
+
onClick();
|
|
1471
|
+
},
|
|
1472
|
+
children
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
function ToolbarDivider() {
|
|
1476
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1477
|
+
className: "nb-html-editor__divider",
|
|
1478
|
+
"aria-hidden": true
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
function HtmlEditor({ id, name, value, disabled, readOnly, hasError, onChange }) {
|
|
1482
|
+
const editable = !disabled && !readOnly;
|
|
1483
|
+
const editor = (0, _tiptap_react.useEditor)({
|
|
1484
|
+
extensions: [_tiptap_starter_kit.default, _tiptap_extension_link.default.configure({
|
|
1485
|
+
openOnClick: false,
|
|
1486
|
+
autolink: true
|
|
1487
|
+
})],
|
|
1488
|
+
content: value,
|
|
1489
|
+
editable,
|
|
1490
|
+
onUpdate: ({ editor: e }) => {
|
|
1491
|
+
onChange(e.isEmpty ? "" : e.getHTML());
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
(0, react.useEffect)(() => {
|
|
1495
|
+
if (!editor) return;
|
|
1496
|
+
if ((editor.isEmpty ? "" : editor.getHTML()) !== value) editor.commands.setContent(value ?? "");
|
|
1497
|
+
}, [value, editor]);
|
|
1498
|
+
(0, react.useEffect)(() => {
|
|
1499
|
+
editor?.setEditable(editable);
|
|
1500
|
+
}, [editable, editor]);
|
|
1501
|
+
const handleLinkToggle = (0, react.useCallback)(() => {
|
|
1502
|
+
if (!editor) return;
|
|
1503
|
+
if (editor.isActive("link")) editor.chain().focus().unsetLink().run();
|
|
1504
|
+
else {
|
|
1505
|
+
const url = window.prompt("URL");
|
|
1506
|
+
if (url) editor.chain().focus().setLink({ href: url }).run();
|
|
1507
|
+
}
|
|
1508
|
+
}, [editor]);
|
|
1509
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1510
|
+
className: `nb-html-editor${hasError ? " nb-html-editor--error" : ""}${!editable ? " nb-html-editor--readonly" : ""}`,
|
|
1511
|
+
children: [
|
|
1512
|
+
editable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1513
|
+
className: "nb-html-editor__toolbar",
|
|
1514
|
+
role: "toolbar",
|
|
1515
|
+
"aria-label": "Text formatting",
|
|
1516
|
+
children: [
|
|
1517
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1518
|
+
title: "Bold (Ctrl+B)",
|
|
1519
|
+
active: editor?.isActive("bold"),
|
|
1520
|
+
onClick: () => editor?.chain().focus().toggleBold().run(),
|
|
1521
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "B" })
|
|
1522
|
+
}),
|
|
1523
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1524
|
+
title: "Italic (Ctrl+I)",
|
|
1525
|
+
active: editor?.isActive("italic"),
|
|
1526
|
+
onClick: () => editor?.chain().focus().toggleItalic().run(),
|
|
1527
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: "I" })
|
|
1528
|
+
}),
|
|
1529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1530
|
+
title: "Strikethrough",
|
|
1531
|
+
active: editor?.isActive("strike"),
|
|
1532
|
+
onClick: () => editor?.chain().focus().toggleStrike().run(),
|
|
1533
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("s", { children: "S" })
|
|
1534
|
+
}),
|
|
1535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarDivider, {}),
|
|
1536
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1537
|
+
title: "Heading 2",
|
|
1538
|
+
active: editor?.isActive("heading", { level: 2 }),
|
|
1539
|
+
onClick: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
1540
|
+
children: "H2"
|
|
1541
|
+
}),
|
|
1542
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1543
|
+
title: "Heading 3",
|
|
1544
|
+
active: editor?.isActive("heading", { level: 3 }),
|
|
1545
|
+
onClick: () => editor?.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
1546
|
+
children: "H3"
|
|
1547
|
+
}),
|
|
1548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarDivider, {}),
|
|
1549
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1550
|
+
title: "Bullet list",
|
|
1551
|
+
active: editor?.isActive("bulletList"),
|
|
1552
|
+
onClick: () => editor?.chain().focus().toggleBulletList().run(),
|
|
1553
|
+
children: "≡"
|
|
1554
|
+
}),
|
|
1555
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1556
|
+
title: "Ordered list",
|
|
1557
|
+
active: editor?.isActive("orderedList"),
|
|
1558
|
+
onClick: () => editor?.chain().focus().toggleOrderedList().run(),
|
|
1559
|
+
children: "1."
|
|
1560
|
+
}),
|
|
1561
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarDivider, {}),
|
|
1562
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1563
|
+
title: "Blockquote",
|
|
1564
|
+
active: editor?.isActive("blockquote"),
|
|
1565
|
+
onClick: () => editor?.chain().focus().toggleBlockquote().run(),
|
|
1566
|
+
children: "“"
|
|
1567
|
+
}),
|
|
1568
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1569
|
+
title: editor?.isActive("link") ? "Remove link" : "Add link",
|
|
1570
|
+
active: editor?.isActive("link"),
|
|
1571
|
+
onClick: handleLinkToggle,
|
|
1572
|
+
children: "🔗"
|
|
1573
|
+
}),
|
|
1574
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarDivider, {}),
|
|
1575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1576
|
+
title: "Undo (Ctrl+Z)",
|
|
1577
|
+
disabled: !editor?.can().undo(),
|
|
1578
|
+
onClick: () => editor?.chain().focus().undo().run(),
|
|
1579
|
+
children: "↩"
|
|
1580
|
+
}),
|
|
1581
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
1582
|
+
title: "Redo (Ctrl+Y)",
|
|
1583
|
+
disabled: !editor?.can().redo(),
|
|
1584
|
+
onClick: () => editor?.chain().focus().redo().run(),
|
|
1585
|
+
children: "↪"
|
|
1586
|
+
})
|
|
1587
|
+
]
|
|
1588
|
+
}),
|
|
1589
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1590
|
+
type: "hidden",
|
|
1591
|
+
id,
|
|
1592
|
+
name,
|
|
1593
|
+
value,
|
|
1594
|
+
readOnly: true
|
|
1595
|
+
}),
|
|
1596
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_tiptap_react.EditorContent, {
|
|
1597
|
+
editor,
|
|
1598
|
+
className: "nb-html-editor__content"
|
|
1599
|
+
})
|
|
1600
|
+
]
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
//#endregion
|
|
1455
1604
|
//#region packages/crud/field/registry/types/html.tsx
|
|
1605
|
+
function stripTags(html) {
|
|
1606
|
+
if (html === null || html === void 0) return "";
|
|
1607
|
+
const str = String(html);
|
|
1608
|
+
if (!str.includes("<")) return str;
|
|
1609
|
+
try {
|
|
1610
|
+
return new DOMParser().parseFromString(str, "text/html").body.textContent ?? "";
|
|
1611
|
+
} catch {
|
|
1612
|
+
return str.replace(/<[^>]*>/g, "");
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1456
1615
|
const htmlTypeModule = {
|
|
1457
1616
|
defaultFilterOperator: "contains",
|
|
1458
1617
|
filterOperators: TEXT_OPERATORS,
|
|
1459
1618
|
buildFilterTerms: defaultBuildFilterTerms,
|
|
1460
|
-
cellText: (_field, value
|
|
1619
|
+
cellText: (_field, value) => stripTags(value),
|
|
1461
1620
|
serializeFormValue: () => KEEP,
|
|
1462
1621
|
serializeDetailValue: () => KEEP,
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1622
|
+
CellRender: ({ value }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1623
|
+
className: "nb-datagrid__html-cell",
|
|
1624
|
+
dangerouslySetInnerHTML: { __html: String(value ?? "") }
|
|
1625
|
+
}),
|
|
1626
|
+
ControlRender: ({ field, value, commonProps, disabled, errorClass, setFieldValue }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HtmlEditor, {
|
|
1627
|
+
id: commonProps.id,
|
|
1628
|
+
name: field.name,
|
|
1629
|
+
value: String(value ?? ""),
|
|
1630
|
+
disabled,
|
|
1631
|
+
readOnly: commonProps.readOnly,
|
|
1632
|
+
hasError: errorClass !== "",
|
|
1633
|
+
onChange: (html) => setFieldValue(field.name, html)
|
|
1468
1634
|
})
|
|
1469
1635
|
};
|
|
1470
1636
|
//#endregion
|
|
@@ -1761,7 +1927,13 @@ function renderCell(field, row, rowIndex, columnIndex, entityOptions, yesLabel =
|
|
|
1761
1927
|
rowIndex,
|
|
1762
1928
|
columnIndex
|
|
1763
1929
|
});
|
|
1764
|
-
|
|
1930
|
+
const typeModule = getFieldTypeModule(field.type);
|
|
1931
|
+
if (typeModule.CellRender) return react$1.default.createElement(typeModule.CellRender, {
|
|
1932
|
+
field,
|
|
1933
|
+
value,
|
|
1934
|
+
row
|
|
1935
|
+
});
|
|
1936
|
+
return typeModule.cellText(field, value, {
|
|
1765
1937
|
entityOptions,
|
|
1766
1938
|
yesLabel,
|
|
1767
1939
|
noLabel
|
|
@@ -1791,8 +1963,8 @@ const getIsMobile = () => typeof window !== "undefined" && window.matchMedia(MOB
|
|
|
1791
1963
|
* layout to the card list, and popovers become bottom sheets.
|
|
1792
1964
|
*/
|
|
1793
1965
|
function useIsMobile() {
|
|
1794
|
-
const [isMobile, setIsMobile] = (0, react.useState)(getIsMobile);
|
|
1795
|
-
(0, react.useEffect)(() => {
|
|
1966
|
+
const [isMobile, setIsMobile] = (0, react$1.useState)(getIsMobile);
|
|
1967
|
+
(0, react$1.useEffect)(() => {
|
|
1796
1968
|
if (typeof window === "undefined") return;
|
|
1797
1969
|
const media = window.matchMedia(MOBILE_QUERY);
|
|
1798
1970
|
const onChange = () => setIsMobile(media.matches);
|
|
@@ -1862,6 +2034,337 @@ function DetailGridSection({ fields, url }) {
|
|
|
1862
2034
|
});
|
|
1863
2035
|
}
|
|
1864
2036
|
//#endregion
|
|
2037
|
+
//#region packages/crud/adapter/HydraAdapter.ts
|
|
2038
|
+
function trimTrailingSlash(value) {
|
|
2039
|
+
return value.replace(/\/+$/, "");
|
|
2040
|
+
}
|
|
2041
|
+
function buildIRI(url, value) {
|
|
2042
|
+
if (value.startsWith("/")) return value;
|
|
2043
|
+
return `${url}/${value}`;
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Default backend adapter for API Platform / JSON-LD + Hydra backends.
|
|
2047
|
+
*
|
|
2048
|
+
* Conventions assumed:
|
|
2049
|
+
* - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
|
|
2050
|
+
* - Entity fields are serialized as IRI strings in POST/PATCH bodies.
|
|
2051
|
+
* - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
|
|
2052
|
+
* - `_iri` is a synthetic alias for `@id` used internally by the engine.
|
|
2053
|
+
*/
|
|
2054
|
+
const HydraAdapter = {
|
|
2055
|
+
getRowId(record, idField) {
|
|
2056
|
+
const direct = record[idField];
|
|
2057
|
+
if (direct !== void 0 && direct !== null) return String(direct);
|
|
2058
|
+
const iri = record["@id"] ?? record["_iri"];
|
|
2059
|
+
if (iri !== void 0 && iri !== null) return String(iri);
|
|
2060
|
+
return String(record["id"] ?? "");
|
|
2061
|
+
},
|
|
2062
|
+
buildItemUrl(baseUrl, id) {
|
|
2063
|
+
const str = String(id);
|
|
2064
|
+
if (str.startsWith("/")) return str;
|
|
2065
|
+
return `${trimTrailingSlash(baseUrl)}/${str}`;
|
|
2066
|
+
},
|
|
2067
|
+
serializeEntityRef(field, rawValue) {
|
|
2068
|
+
if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
|
|
2069
|
+
if (typeof rawValue === "object") {
|
|
2070
|
+
const entity = rawValue;
|
|
2071
|
+
const atId = entity["@id"];
|
|
2072
|
+
if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
|
|
2073
|
+
const idValue = entity["id"];
|
|
2074
|
+
const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
|
|
2075
|
+
return buildIRI(field.url ?? "", resolvedId);
|
|
2076
|
+
}
|
|
2077
|
+
return buildIRI(field.url ?? "", String(rawValue));
|
|
2078
|
+
},
|
|
2079
|
+
normalizeEntityValue(rawValue, field) {
|
|
2080
|
+
if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
|
|
2081
|
+
if (typeof rawValue === "object" && rawValue !== null) {
|
|
2082
|
+
const entity = rawValue;
|
|
2083
|
+
const atId = entity["@id"];
|
|
2084
|
+
if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
|
|
2085
|
+
const directValue = entity[field.valueField];
|
|
2086
|
+
if (directValue !== void 0 && directValue !== null) return directValue;
|
|
2087
|
+
}
|
|
2088
|
+
return rawValue;
|
|
2089
|
+
},
|
|
2090
|
+
getEntityOptionKey(item, field) {
|
|
2091
|
+
if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
|
|
2092
|
+
return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
|
|
2093
|
+
},
|
|
2094
|
+
parseListResponse(response) {
|
|
2095
|
+
const r = response;
|
|
2096
|
+
const member = r["hydra:member"];
|
|
2097
|
+
if (Array.isArray(member)) return {
|
|
2098
|
+
items: member,
|
|
2099
|
+
total: Number(r["hydra:totalItems"] ?? member.length)
|
|
2100
|
+
};
|
|
2101
|
+
if (Array.isArray(response)) return {
|
|
2102
|
+
items: response,
|
|
2103
|
+
total: response.length
|
|
2104
|
+
};
|
|
2105
|
+
return {
|
|
2106
|
+
items: [],
|
|
2107
|
+
total: 0
|
|
2108
|
+
};
|
|
2109
|
+
},
|
|
2110
|
+
synthesizeEntityKey(field, entityValue) {
|
|
2111
|
+
if (!field.url) return void 0;
|
|
2112
|
+
const base = trimTrailingSlash(field.url);
|
|
2113
|
+
const directId = entityValue["id"];
|
|
2114
|
+
if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
|
|
2115
|
+
const directValue = entityValue[field.valueField];
|
|
2116
|
+
if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
|
|
2117
|
+
for (const key of [
|
|
2118
|
+
"code",
|
|
2119
|
+
"uuid",
|
|
2120
|
+
"slug"
|
|
2121
|
+
]) {
|
|
2122
|
+
const candidate = entityValue[key];
|
|
2123
|
+
if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
//#endregion
|
|
2128
|
+
//#region packages/crud/form/serializeFormData.ts
|
|
2129
|
+
function applySerializedValue(formData, field, result) {
|
|
2130
|
+
if (result.kind === "set") formData[field.name] = result.value;
|
|
2131
|
+
else if (result.kind === "omit") delete formData[field.name];
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Pure serialization of form data before HTTP submission.
|
|
2135
|
+
*
|
|
2136
|
+
* Applies uploaded file references and computed fields, then delegates the
|
|
2137
|
+
* per-type wire format (entity refs, business dates, numeric coercion, file
|
|
2138
|
+
* handling, NONE stripping) to each field's Field-Type module.
|
|
2139
|
+
*
|
|
2140
|
+
* This function is extracted from useFormSubmit for testability.
|
|
2141
|
+
*/
|
|
2142
|
+
function serializeFormFields(rawData, fields, ctx) {
|
|
2143
|
+
const formData = { ...rawData };
|
|
2144
|
+
ctx.uploadedFiles.forEach((file) => {
|
|
2145
|
+
formData[file.name] = file.iri;
|
|
2146
|
+
});
|
|
2147
|
+
fields.forEach((field) => {
|
|
2148
|
+
if (!field.computed) return;
|
|
2149
|
+
const computedValue = field.computed(formData);
|
|
2150
|
+
if (computedValue !== void 0) formData[field.name] = computedValue;
|
|
2151
|
+
});
|
|
2152
|
+
const moduleCtx = {
|
|
2153
|
+
adapter: ctx.adapter ?? HydraAdapter,
|
|
2154
|
+
format: ctx.format,
|
|
2155
|
+
getFieldValue: ctx.getFieldValue
|
|
2156
|
+
};
|
|
2157
|
+
fields.forEach((field) => {
|
|
2158
|
+
applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
|
|
2159
|
+
});
|
|
2160
|
+
return formData;
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
|
|
2164
|
+
* Pure function — operates on an array of row records.
|
|
2165
|
+
*/
|
|
2166
|
+
function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
|
|
2167
|
+
const details = structuredClone(rows);
|
|
2168
|
+
detailFields.forEach((field) => {
|
|
2169
|
+
const typeModule = getFieldTypeModule(field.type);
|
|
2170
|
+
details.forEach((detail) => {
|
|
2171
|
+
applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
|
|
2172
|
+
});
|
|
2173
|
+
});
|
|
2174
|
+
details.forEach((detail) => {
|
|
2175
|
+
if (!isEditMode) delete detail[detailIdField];
|
|
2176
|
+
else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
|
|
2177
|
+
});
|
|
2178
|
+
return details;
|
|
2179
|
+
}
|
|
2180
|
+
//#endregion
|
|
2181
|
+
//#region packages/crud/datagrid/useInlineEdit.ts
|
|
2182
|
+
/** Fields that are safe to edit inline (identity/readonly/file types are excluded). */
|
|
2183
|
+
function canEditFieldInline(field) {
|
|
2184
|
+
return !field.isIdentity && !field.readonly && field.type !== "file" && field.visibleOnForm !== false;
|
|
2185
|
+
}
|
|
2186
|
+
function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError }) {
|
|
2187
|
+
const [draftRows, setDraftRows] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
2188
|
+
const [savingRows, setSavingRows] = (0, react$1.useState)(/* @__PURE__ */ new Set());
|
|
2189
|
+
const [rowErrors, setRowErrors] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
2190
|
+
const draftRowsRef = (0, react$1.useRef)(draftRows);
|
|
2191
|
+
draftRowsRef.current = draftRows;
|
|
2192
|
+
const optsRef = (0, react$1.useRef)({
|
|
2193
|
+
url,
|
|
2194
|
+
idField,
|
|
2195
|
+
adapter,
|
|
2196
|
+
httpClient,
|
|
2197
|
+
fields,
|
|
2198
|
+
onSaveSuccess,
|
|
2199
|
+
onSaveError
|
|
2200
|
+
});
|
|
2201
|
+
optsRef.current = {
|
|
2202
|
+
url,
|
|
2203
|
+
idField,
|
|
2204
|
+
adapter,
|
|
2205
|
+
httpClient,
|
|
2206
|
+
fields,
|
|
2207
|
+
onSaveSuccess,
|
|
2208
|
+
onSaveError
|
|
2209
|
+
};
|
|
2210
|
+
const isEditing = (0, react$1.useCallback)((key) => draftRowsRef.current.has(key), []);
|
|
2211
|
+
const startEdit = (0, react$1.useCallback)((row) => {
|
|
2212
|
+
const key = row[optsRef.current.idField];
|
|
2213
|
+
setDraftRows((prev) => {
|
|
2214
|
+
const base = mode === "row" ? [] : Array.from(prev.entries());
|
|
2215
|
+
return new Map([...base, [key, { ...row }]]);
|
|
2216
|
+
});
|
|
2217
|
+
setRowErrors((prev) => {
|
|
2218
|
+
const next = new Map(prev);
|
|
2219
|
+
next.delete(key);
|
|
2220
|
+
return next;
|
|
2221
|
+
});
|
|
2222
|
+
}, [mode]);
|
|
2223
|
+
const cancelEdit = (0, react$1.useCallback)((key) => {
|
|
2224
|
+
setDraftRows((prev) => {
|
|
2225
|
+
const next = new Map(prev);
|
|
2226
|
+
next.delete(key);
|
|
2227
|
+
return next;
|
|
2228
|
+
});
|
|
2229
|
+
setRowErrors((prev) => {
|
|
2230
|
+
const next = new Map(prev);
|
|
2231
|
+
next.delete(key);
|
|
2232
|
+
return next;
|
|
2233
|
+
});
|
|
2234
|
+
}, []);
|
|
2235
|
+
const discardAll = (0, react$1.useCallback)(() => {
|
|
2236
|
+
setDraftRows(/* @__PURE__ */ new Map());
|
|
2237
|
+
setRowErrors(/* @__PURE__ */ new Map());
|
|
2238
|
+
}, []);
|
|
2239
|
+
const updateDraft = (0, react$1.useCallback)((key, fieldName, value) => {
|
|
2240
|
+
setDraftRows((prev) => {
|
|
2241
|
+
const current = prev.get(key);
|
|
2242
|
+
if (!current) return prev;
|
|
2243
|
+
const next = new Map(prev);
|
|
2244
|
+
next.set(key, {
|
|
2245
|
+
...current,
|
|
2246
|
+
[fieldName]: value
|
|
2247
|
+
});
|
|
2248
|
+
return next;
|
|
2249
|
+
});
|
|
2250
|
+
}, []);
|
|
2251
|
+
const doSaveRow = (0, react$1.useCallback)(async (key) => {
|
|
2252
|
+
const { url: u, adapter: a, httpClient: http, fields: fs, onSaveError: onErr } = optsRef.current;
|
|
2253
|
+
const draft = draftRowsRef.current.get(key);
|
|
2254
|
+
if (!draft) return false;
|
|
2255
|
+
const errors = {};
|
|
2256
|
+
fs.forEach((field) => {
|
|
2257
|
+
if (!canEditFieldInline(field)) return;
|
|
2258
|
+
if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
|
|
2259
|
+
});
|
|
2260
|
+
if (Object.keys(errors).length > 0) {
|
|
2261
|
+
setRowErrors((prev) => new Map(prev).set(key, errors));
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
setSavingRows((prev) => new Set([...prev, key]));
|
|
2265
|
+
const serialized = serializeFormFields(draft, fs.filter(canEditFieldInline), {
|
|
2266
|
+
uploadedFiles: [],
|
|
2267
|
+
getFieldValue: (name) => draft[name]
|
|
2268
|
+
});
|
|
2269
|
+
try {
|
|
2270
|
+
await http.patch(a.buildItemUrl(u, key), serialized);
|
|
2271
|
+
setDraftRows((prev) => {
|
|
2272
|
+
const next = new Map(prev);
|
|
2273
|
+
next.delete(key);
|
|
2274
|
+
return next;
|
|
2275
|
+
});
|
|
2276
|
+
setRowErrors((prev) => {
|
|
2277
|
+
const next = new Map(prev);
|
|
2278
|
+
next.delete(key);
|
|
2279
|
+
return next;
|
|
2280
|
+
});
|
|
2281
|
+
return true;
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
onErr?.(key, err);
|
|
2284
|
+
if (typeof err === "object" && err !== null && "status" in err && err.status === 422 && "data" in err) {
|
|
2285
|
+
const data = err.data;
|
|
2286
|
+
if (typeof data === "object" && data !== null && "violations" in data) {
|
|
2287
|
+
const violations = data.violations ?? [];
|
|
2288
|
+
const fieldErrors = {};
|
|
2289
|
+
violations.forEach((v) => {
|
|
2290
|
+
fieldErrors[v.propertyPath] = v.message;
|
|
2291
|
+
});
|
|
2292
|
+
setRowErrors((prev) => new Map(prev).set(key, fieldErrors));
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return false;
|
|
2296
|
+
} finally {
|
|
2297
|
+
setSavingRows((prev) => {
|
|
2298
|
+
const next = new Set(prev);
|
|
2299
|
+
next.delete(key);
|
|
2300
|
+
return next;
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}, []);
|
|
2304
|
+
return {
|
|
2305
|
+
draftRows,
|
|
2306
|
+
savingRows,
|
|
2307
|
+
rowErrors,
|
|
2308
|
+
isEditing,
|
|
2309
|
+
startEdit,
|
|
2310
|
+
cancelEdit,
|
|
2311
|
+
discardAll,
|
|
2312
|
+
updateDraft,
|
|
2313
|
+
saveRow: (0, react$1.useCallback)(async (key) => {
|
|
2314
|
+
if (await doSaveRow(key)) optsRef.current.onSaveSuccess?.();
|
|
2315
|
+
}, [doSaveRow]),
|
|
2316
|
+
saveAll: (0, react$1.useCallback)(async () => {
|
|
2317
|
+
const keys = Array.from(draftRowsRef.current.keys());
|
|
2318
|
+
if ((await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value)) optsRef.current.onSaveSuccess?.();
|
|
2319
|
+
}, [doSaveRow])
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
//#endregion
|
|
2323
|
+
//#region packages/crud/datagrid/InlineEditCell.tsx
|
|
2324
|
+
/**
|
|
2325
|
+
* Renders a single cell as an editable control during inline row editing.
|
|
2326
|
+
* Uses the same FieldTypeModule.ControlRender path as the full form, but
|
|
2327
|
+
* with a compact common-props class so controls fit inside a table cell.
|
|
2328
|
+
*/
|
|
2329
|
+
function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t }) {
|
|
2330
|
+
const typeModule = getFieldTypeModule(field.type);
|
|
2331
|
+
const fieldError = errors?.[field.name];
|
|
2332
|
+
const errorClass = fieldError ? " is-error" : "";
|
|
2333
|
+
const commonProps = {
|
|
2334
|
+
className: `nb-inline-control${errorClass}`,
|
|
2335
|
+
disabled,
|
|
2336
|
+
id: `iec-${String(rowKey)}-${field.name}`,
|
|
2337
|
+
name: field.name,
|
|
2338
|
+
onClick: void 0,
|
|
2339
|
+
readOnly: false,
|
|
2340
|
+
required: field.required
|
|
2341
|
+
};
|
|
2342
|
+
const ctx = {
|
|
2343
|
+
httpClient,
|
|
2344
|
+
t,
|
|
2345
|
+
remoteOptions: allRemoteOptions,
|
|
2346
|
+
getPrependData: () => void 0,
|
|
2347
|
+
getFieldValue: (name) => draft[name],
|
|
2348
|
+
getExistingMedia: () => null,
|
|
2349
|
+
clearExistingMedia: () => {},
|
|
2350
|
+
upsertUploadedFile: () => {}
|
|
2351
|
+
};
|
|
2352
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2353
|
+
className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
|
|
2354
|
+
children: typeModule.ControlRender({
|
|
2355
|
+
field,
|
|
2356
|
+
value: draft[field.name],
|
|
2357
|
+
error: fieldError,
|
|
2358
|
+
errorClass,
|
|
2359
|
+
disabled,
|
|
2360
|
+
readOnly: false,
|
|
2361
|
+
commonProps,
|
|
2362
|
+
setFieldValue: onChange,
|
|
2363
|
+
ctx
|
|
2364
|
+
})
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
//#endregion
|
|
1865
2368
|
//#region packages/crud/summary/SummaryUtils.ts
|
|
1866
2369
|
function toFiniteNumber(value) {
|
|
1867
2370
|
if (value === null || value === void 0 || value === "") return null;
|
|
@@ -1931,19 +2434,20 @@ function resolveSummaryText(rows, item) {
|
|
|
1931
2434
|
const DETAIL_COL_WIDTH = 36;
|
|
1932
2435
|
const CHECKBOX_COL_WIDTH = 36;
|
|
1933
2436
|
const ACTIONS_COL_WIDTH = 44;
|
|
2437
|
+
const INLINE_ACTIONS_COL_WIDTH = 72;
|
|
1934
2438
|
const DEFAULT_COL_WIDTH = 120;
|
|
1935
2439
|
const MIN_COL_WIDTH = 48;
|
|
1936
2440
|
function getColumnWidth(field, colWidths) {
|
|
1937
2441
|
return colWidths[field.name] ?? field.width ?? field.minWidth ?? DEFAULT_COL_WIDTH;
|
|
1938
2442
|
}
|
|
1939
|
-
function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth }) {
|
|
2443
|
+
function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth, actionsColWidth = ACTIONS_COL_WIDTH }) {
|
|
1940
2444
|
let total = 0;
|
|
1941
2445
|
if (hasDetail) total += DETAIL_COL_WIDTH;
|
|
1942
2446
|
if (hasCheckbox) total += CHECKBOX_COL_WIDTH;
|
|
1943
2447
|
visibleFields.forEach((field) => {
|
|
1944
2448
|
total += getColumnWidth(field, colWidths);
|
|
1945
2449
|
});
|
|
1946
|
-
if (hasRowActions) total +=
|
|
2450
|
+
if (hasRowActions) total += actionsColWidth;
|
|
1947
2451
|
return Math.max(containerWidth, total);
|
|
1948
2452
|
}
|
|
1949
2453
|
function GridColumnGroup({ fields, colWidths, hasCheckbox, hasDetail, hasRowActions }) {
|
|
@@ -2326,9 +2830,11 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2326
2830
|
]);
|
|
2327
2831
|
const fieldsRef = (0, react.useRef)(options.fields);
|
|
2328
2832
|
fieldsRef.current = options.fields;
|
|
2833
|
+
const loadSeqRef = (0, react.useRef)(0);
|
|
2329
2834
|
const loadRows = (0, react.useCallback)(async () => {
|
|
2330
2835
|
if (options.manualLoad) return rowsRef.current;
|
|
2331
2836
|
setIsGridLoading(true);
|
|
2837
|
+
const seq = ++loadSeqRef.current;
|
|
2332
2838
|
const loadOptions = {
|
|
2333
2839
|
filter: buildFilterExpression(filters, filterOperators, fieldsRef.current),
|
|
2334
2840
|
sort
|
|
@@ -2338,6 +2844,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2338
2844
|
loadOptions.take = pageSize;
|
|
2339
2845
|
}
|
|
2340
2846
|
const result = await source.load(loadOptions);
|
|
2847
|
+
if (seq !== loadSeqRef.current) return rowsRef.current;
|
|
2341
2848
|
rowsRef.current = result.data;
|
|
2342
2849
|
setRows(result.data);
|
|
2343
2850
|
setTotalCount(result.totalCount);
|
|
@@ -2391,6 +2898,17 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2391
2898
|
resourceStoreFactory,
|
|
2392
2899
|
visibleFields
|
|
2393
2900
|
]);
|
|
2901
|
+
const canInlineEditMode = options.editMode === "row" || options.editMode === "batch";
|
|
2902
|
+
const inlineEdit = useInlineEdit({
|
|
2903
|
+
mode: options.editMode === "batch" ? "batch" : "row",
|
|
2904
|
+
url: options.url,
|
|
2905
|
+
idField,
|
|
2906
|
+
adapter: options.adapter,
|
|
2907
|
+
httpClient,
|
|
2908
|
+
fields: options.fields,
|
|
2909
|
+
onSaveSuccess: () => void loadRows(),
|
|
2910
|
+
onSaveError: () => {}
|
|
2911
|
+
});
|
|
2394
2912
|
const handleStateRef = (0, react.useRef)({
|
|
2395
2913
|
selectedKeys,
|
|
2396
2914
|
filters,
|
|
@@ -2457,7 +2975,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2457
2975
|
const rowEditable = (row) => options.canEditRow?.(row) !== false;
|
|
2458
2976
|
const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
|
|
2459
2977
|
const buildRowActions = (row) => [
|
|
2460
|
-
...options.allowEdit && rowEditable(row) &&
|
|
2978
|
+
...options.allowEdit && rowEditable(row) && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
|
|
2979
|
+
text: t("grid.inlineEditRow"),
|
|
2980
|
+
icon: "ph-pencil-simple",
|
|
2981
|
+
disabled: options.editDisabled,
|
|
2982
|
+
onClick: () => inlineEdit.startEdit(row)
|
|
2983
|
+
}] : [],
|
|
2984
|
+
...options.allowEdit && rowEditable(row) && !canInlineEditMode && (options.onEdit || options.events?.EDIT) ? [{
|
|
2461
2985
|
text: t("grid.buttonEdit"),
|
|
2462
2986
|
icon: "ph-pencil-simple",
|
|
2463
2987
|
disabled: options.editDisabled,
|
|
@@ -2481,6 +3005,10 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2481
3005
|
}] : []
|
|
2482
3006
|
];
|
|
2483
3007
|
const openRow = (row) => {
|
|
3008
|
+
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3009
|
+
inlineEdit.startEdit(row);
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
2484
3012
|
if (options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT)) {
|
|
2485
3013
|
if (options.onEdit) options.onEdit(row);
|
|
2486
3014
|
else emit(options.events.EDIT, { row });
|
|
@@ -2612,22 +3140,24 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2612
3140
|
};
|
|
2613
3141
|
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
2614
3142
|
const hasCheckbox = options.selectionMode === "multiple";
|
|
2615
|
-
const hasBuiltInRowActions = Boolean(options.allowEdit && (options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
|
|
3143
|
+
const hasBuiltInRowActions = Boolean(options.allowEdit && (canInlineEditMode || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
|
|
2616
3144
|
const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
|
|
2617
3145
|
const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
|
|
2618
3146
|
const hasDetail = Boolean(options.detailFields);
|
|
3147
|
+
const actionsColWidth = canInlineEditMode ? INLINE_ACTIONS_COL_WIDTH : ACTIONS_COL_WIDTH;
|
|
2619
3148
|
const layoutWidth = computeLayoutWidth({
|
|
2620
3149
|
visibleFields,
|
|
2621
3150
|
colWidths,
|
|
2622
3151
|
hasCheckbox,
|
|
2623
3152
|
hasDetail,
|
|
2624
3153
|
hasRowActions,
|
|
2625
|
-
containerWidth
|
|
3154
|
+
containerWidth,
|
|
3155
|
+
actionsColWidth
|
|
2626
3156
|
});
|
|
2627
3157
|
const resolvedColWidths = (0, react.useMemo)(() => {
|
|
2628
3158
|
if (visibleFields.length === 0) return colWidths;
|
|
2629
3159
|
const dataTotal = visibleFields.reduce((sum, f) => sum + getColumnWidth(f, colWidths), 0);
|
|
2630
|
-
const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ?
|
|
3160
|
+
const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
|
|
2631
3161
|
const extra = Math.max(0, containerWidth - dataTotal - fixedTotal);
|
|
2632
3162
|
if (extra === 0 || dataTotal === 0) return colWidths;
|
|
2633
3163
|
let distributed = 0;
|
|
@@ -2731,6 +3261,31 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2731
3261
|
})
|
|
2732
3262
|
]
|
|
2733
3263
|
}),
|
|
3264
|
+
options.editMode === "batch" && inlineEdit.draftRows.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3265
|
+
className: "nb-datagrid__batch-bar",
|
|
3266
|
+
role: "status",
|
|
3267
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3268
|
+
className: "nb-datagrid__batch-bar-label",
|
|
3269
|
+
children: t("grid.inlineUnsavedRows", { count: inlineEdit.draftRows.size })
|
|
3270
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3271
|
+
className: "nb-datagrid__batch-bar-actions",
|
|
3272
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3273
|
+
type: "button",
|
|
3274
|
+
className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--discard",
|
|
3275
|
+
onClick: () => inlineEdit.discardAll(),
|
|
3276
|
+
children: t("grid.inlineDiscardAll")
|
|
3277
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3278
|
+
type: "button",
|
|
3279
|
+
className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--save",
|
|
3280
|
+
onClick: () => void inlineEdit.saveAll(),
|
|
3281
|
+
children: t("grid.inlineSaveAll", { count: inlineEdit.draftRows.size })
|
|
3282
|
+
})]
|
|
3283
|
+
})]
|
|
3284
|
+
}),
|
|
3285
|
+
options.aboveGrid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3286
|
+
className: "nb-datagrid__above-grid",
|
|
3287
|
+
children: options.aboveGrid
|
|
3288
|
+
}) : null,
|
|
2734
3289
|
isMobile && quickSearchField && (options.filterRow ?? true) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2735
3290
|
className: "nb-datagrid__quick-search",
|
|
2736
3291
|
children: [
|
|
@@ -3020,16 +3575,33 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3020
3575
|
}) }) : rows.map((row, rowIndex) => {
|
|
3021
3576
|
const key = row[idField] ?? rowIndex;
|
|
3022
3577
|
const selected = selectedKeys.includes(key);
|
|
3578
|
+
const editing = inlineEdit.draftRows.has(key);
|
|
3579
|
+
const saving = inlineEdit.savingRows.has(key);
|
|
3580
|
+
const rowDraft = inlineEdit.draftRows.get(key) ?? row;
|
|
3581
|
+
const rowFieldErrors = inlineEdit.rowErrors.get(key);
|
|
3023
3582
|
const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
|
|
3024
3583
|
const detailUrl = options.detailUrl?.replace("{id}", String(key));
|
|
3025
3584
|
const expanded = expandedKeys.has(key);
|
|
3026
3585
|
const rowActions = buildRowActions(row);
|
|
3027
3586
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
3028
|
-
className:
|
|
3587
|
+
className: [
|
|
3588
|
+
"nb-datagrid__row",
|
|
3589
|
+
expanded ? "nb-datagrid__row--expanded" : "",
|
|
3590
|
+
selected ? "nb-datagrid__row--selected" : "",
|
|
3591
|
+
editing ? "nb-datagrid__row--editing" : "",
|
|
3592
|
+
saving ? "nb-datagrid__row--saving" : ""
|
|
3593
|
+
].filter(Boolean).join(" "),
|
|
3029
3594
|
tabIndex: 0,
|
|
3030
3595
|
"aria-selected": selected,
|
|
3031
|
-
onClick: () =>
|
|
3596
|
+
onClick: () => {
|
|
3597
|
+
if (!editing) selectRow(row);
|
|
3598
|
+
},
|
|
3032
3599
|
onDoubleClick: () => {
|
|
3600
|
+
if (editing) return;
|
|
3601
|
+
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3602
|
+
inlineEdit.startEdit(row);
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3033
3605
|
if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
|
|
3034
3606
|
if (options.onEdit) options.onEdit(row);
|
|
3035
3607
|
else emit(options.events.EDIT, { row });
|
|
@@ -3038,10 +3610,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3038
3610
|
if (options.allowView && options.onView) options.onView(row);
|
|
3039
3611
|
},
|
|
3040
3612
|
onKeyDown: (event) => {
|
|
3041
|
-
if (event.key === "
|
|
3613
|
+
if (editing && event.key === "Escape") inlineEdit.cancelEdit(key);
|
|
3614
|
+
else if (editing && event.key === "Enter") inlineEdit.saveRow(key);
|
|
3615
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
|
|
3616
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
|
|
3042
3617
|
else emit(options.events.EDIT, { row });
|
|
3043
|
-
else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
3044
|
-
else if (event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
|
|
3618
|
+
else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
3619
|
+
else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
|
|
3045
3620
|
else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
3046
3621
|
event.preventDefault();
|
|
3047
3622
|
const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
|
|
@@ -3086,9 +3661,29 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3086
3661
|
})
|
|
3087
3662
|
}),
|
|
3088
3663
|
visibleFields.map((field, columnIndex) => {
|
|
3664
|
+
const width = getColumnWidth(field, resolvedColWidths);
|
|
3665
|
+
if (editing && canEditFieldInline(field)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3666
|
+
style: {
|
|
3667
|
+
width,
|
|
3668
|
+
textAlign: field.align
|
|
3669
|
+
},
|
|
3670
|
+
className: "nb-datagrid__edit-cell",
|
|
3671
|
+
onClick: (e) => e.stopPropagation(),
|
|
3672
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineEditCell, {
|
|
3673
|
+
field,
|
|
3674
|
+
rowKey: key,
|
|
3675
|
+
draft: rowDraft,
|
|
3676
|
+
onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
|
|
3677
|
+
errors: rowFieldErrors,
|
|
3678
|
+
disabled: saving,
|
|
3679
|
+
allRemoteOptions: filterRemoteOptions,
|
|
3680
|
+
httpClient,
|
|
3681
|
+
t
|
|
3682
|
+
})
|
|
3683
|
+
}, field.name);
|
|
3089
3684
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3090
3685
|
style: {
|
|
3091
|
-
width
|
|
3686
|
+
width,
|
|
3092
3687
|
textAlign: field.align
|
|
3093
3688
|
},
|
|
3094
3689
|
title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
|
|
@@ -3098,7 +3693,32 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3098
3693
|
hasRowActions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3099
3694
|
className: "nb-datagrid__actions-cell",
|
|
3100
3695
|
onClick: (e) => e.stopPropagation(),
|
|
3101
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
3696
|
+
children: editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3697
|
+
className: "nb-datagrid__inline-actions",
|
|
3698
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3699
|
+
type: "button",
|
|
3700
|
+
className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--save",
|
|
3701
|
+
disabled: saving,
|
|
3702
|
+
"aria-label": t("grid.inlineSaveRow"),
|
|
3703
|
+
title: t("grid.inlineSaveRow"),
|
|
3704
|
+
onClick: () => void inlineEdit.saveRow(key),
|
|
3705
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
3706
|
+
className: "ph ph-check",
|
|
3707
|
+
"aria-hidden": "true"
|
|
3708
|
+
})
|
|
3709
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3710
|
+
type: "button",
|
|
3711
|
+
className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--cancel",
|
|
3712
|
+
disabled: saving,
|
|
3713
|
+
"aria-label": t("grid.inlineCancelRow"),
|
|
3714
|
+
title: t("grid.inlineCancelRow"),
|
|
3715
|
+
onClick: () => inlineEdit.cancelEdit(key),
|
|
3716
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
3717
|
+
className: "ph ph-x",
|
|
3718
|
+
"aria-hidden": "true"
|
|
3719
|
+
})
|
|
3720
|
+
})]
|
|
3721
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3102
3722
|
className: "nb-datagrid__row-actions",
|
|
3103
3723
|
children: rowActions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3104
3724
|
icon: "ph ph-dots-three-vertical",
|
|
@@ -3448,97 +4068,6 @@ const FORM_EVENTS = {
|
|
|
3448
4068
|
};
|
|
3449
4069
|
const FORM_ERRORS_EVENT = "form-errors";
|
|
3450
4070
|
//#endregion
|
|
3451
|
-
//#region packages/crud/adapter/HydraAdapter.ts
|
|
3452
|
-
function trimTrailingSlash(value) {
|
|
3453
|
-
return value.replace(/\/+$/, "");
|
|
3454
|
-
}
|
|
3455
|
-
function buildIRI(url, value) {
|
|
3456
|
-
if (value.startsWith("/")) return value;
|
|
3457
|
-
return `${url}/${value}`;
|
|
3458
|
-
}
|
|
3459
|
-
/**
|
|
3460
|
-
* Default backend adapter for API Platform / JSON-LD + Hydra backends.
|
|
3461
|
-
*
|
|
3462
|
-
* Conventions assumed:
|
|
3463
|
-
* - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
|
|
3464
|
-
* - Entity fields are serialized as IRI strings in POST/PATCH bodies.
|
|
3465
|
-
* - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
|
|
3466
|
-
* - `_iri` is a synthetic alias for `@id` used internally by the engine.
|
|
3467
|
-
*/
|
|
3468
|
-
const HydraAdapter = {
|
|
3469
|
-
getRowId(record, idField) {
|
|
3470
|
-
const direct = record[idField];
|
|
3471
|
-
if (direct !== void 0 && direct !== null) return String(direct);
|
|
3472
|
-
const iri = record["@id"] ?? record["_iri"];
|
|
3473
|
-
if (iri !== void 0 && iri !== null) return String(iri);
|
|
3474
|
-
return String(record["id"] ?? "");
|
|
3475
|
-
},
|
|
3476
|
-
buildItemUrl(baseUrl, id) {
|
|
3477
|
-
const str = String(id);
|
|
3478
|
-
if (str.startsWith("/")) return str;
|
|
3479
|
-
return `${trimTrailingSlash(baseUrl)}/${str}`;
|
|
3480
|
-
},
|
|
3481
|
-
serializeEntityRef(field, rawValue) {
|
|
3482
|
-
if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
|
|
3483
|
-
if (typeof rawValue === "object") {
|
|
3484
|
-
const entity = rawValue;
|
|
3485
|
-
const atId = entity["@id"];
|
|
3486
|
-
if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
|
|
3487
|
-
const idValue = entity["id"];
|
|
3488
|
-
const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
|
|
3489
|
-
return buildIRI(field.url ?? "", resolvedId);
|
|
3490
|
-
}
|
|
3491
|
-
return buildIRI(field.url ?? "", String(rawValue));
|
|
3492
|
-
},
|
|
3493
|
-
normalizeEntityValue(rawValue, field) {
|
|
3494
|
-
if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
|
|
3495
|
-
if (typeof rawValue === "object" && rawValue !== null) {
|
|
3496
|
-
const entity = rawValue;
|
|
3497
|
-
const atId = entity["@id"];
|
|
3498
|
-
if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
|
|
3499
|
-
const directValue = entity[field.valueField];
|
|
3500
|
-
if (directValue !== void 0 && directValue !== null) return directValue;
|
|
3501
|
-
}
|
|
3502
|
-
return rawValue;
|
|
3503
|
-
},
|
|
3504
|
-
getEntityOptionKey(item, field) {
|
|
3505
|
-
if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
|
|
3506
|
-
return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
|
|
3507
|
-
},
|
|
3508
|
-
parseListResponse(response) {
|
|
3509
|
-
const r = response;
|
|
3510
|
-
const member = r["hydra:member"];
|
|
3511
|
-
if (Array.isArray(member)) return {
|
|
3512
|
-
items: member,
|
|
3513
|
-
total: Number(r["hydra:totalItems"] ?? member.length)
|
|
3514
|
-
};
|
|
3515
|
-
if (Array.isArray(response)) return {
|
|
3516
|
-
items: response,
|
|
3517
|
-
total: response.length
|
|
3518
|
-
};
|
|
3519
|
-
return {
|
|
3520
|
-
items: [],
|
|
3521
|
-
total: 0
|
|
3522
|
-
};
|
|
3523
|
-
},
|
|
3524
|
-
synthesizeEntityKey(field, entityValue) {
|
|
3525
|
-
if (!field.url) return void 0;
|
|
3526
|
-
const base = trimTrailingSlash(field.url);
|
|
3527
|
-
const directId = entityValue["id"];
|
|
3528
|
-
if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
|
|
3529
|
-
const directValue = entityValue[field.valueField];
|
|
3530
|
-
if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
|
|
3531
|
-
for (const key of [
|
|
3532
|
-
"code",
|
|
3533
|
-
"uuid",
|
|
3534
|
-
"slug"
|
|
3535
|
-
]) {
|
|
3536
|
-
const candidate = entityValue[key];
|
|
3537
|
-
if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
};
|
|
3541
|
-
//#endregion
|
|
3542
4071
|
//#region packages/crud/form/FormDataTransform.ts
|
|
3543
4072
|
function upsertPrependData(store, field, item) {
|
|
3544
4073
|
const existing = store.get(field.name);
|
|
@@ -3840,59 +4369,6 @@ function buildFieldColSpanContext(options) {
|
|
|
3840
4369
|
};
|
|
3841
4370
|
}
|
|
3842
4371
|
//#endregion
|
|
3843
|
-
//#region packages/crud/form/serializeFormData.ts
|
|
3844
|
-
function applySerializedValue(formData, field, result) {
|
|
3845
|
-
if (result.kind === "set") formData[field.name] = result.value;
|
|
3846
|
-
else if (result.kind === "omit") delete formData[field.name];
|
|
3847
|
-
}
|
|
3848
|
-
/**
|
|
3849
|
-
* Pure serialization of form data before HTTP submission.
|
|
3850
|
-
*
|
|
3851
|
-
* Applies uploaded file references and computed fields, then delegates the
|
|
3852
|
-
* per-type wire format (entity refs, business dates, numeric coercion, file
|
|
3853
|
-
* handling, NONE stripping) to each field's Field-Type module.
|
|
3854
|
-
*
|
|
3855
|
-
* This function is extracted from useFormSubmit for testability.
|
|
3856
|
-
*/
|
|
3857
|
-
function serializeFormFields(rawData, fields, ctx) {
|
|
3858
|
-
const formData = { ...rawData };
|
|
3859
|
-
ctx.uploadedFiles.forEach((file) => {
|
|
3860
|
-
formData[file.name] = file.iri;
|
|
3861
|
-
});
|
|
3862
|
-
fields.forEach((field) => {
|
|
3863
|
-
if (!field.computed) return;
|
|
3864
|
-
const computedValue = field.computed(formData);
|
|
3865
|
-
if (computedValue !== void 0) formData[field.name] = computedValue;
|
|
3866
|
-
});
|
|
3867
|
-
const moduleCtx = {
|
|
3868
|
-
adapter: ctx.adapter ?? HydraAdapter,
|
|
3869
|
-
format: ctx.format,
|
|
3870
|
-
getFieldValue: ctx.getFieldValue
|
|
3871
|
-
};
|
|
3872
|
-
fields.forEach((field) => {
|
|
3873
|
-
applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
|
|
3874
|
-
});
|
|
3875
|
-
return formData;
|
|
3876
|
-
}
|
|
3877
|
-
/**
|
|
3878
|
-
* Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
|
|
3879
|
-
* Pure function — operates on an array of row records.
|
|
3880
|
-
*/
|
|
3881
|
-
function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
|
|
3882
|
-
const details = JSON.parse(JSON.stringify(rows));
|
|
3883
|
-
detailFields.forEach((field) => {
|
|
3884
|
-
const typeModule = getFieldTypeModule(field.type);
|
|
3885
|
-
details.forEach((detail) => {
|
|
3886
|
-
applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
|
|
3887
|
-
});
|
|
3888
|
-
});
|
|
3889
|
-
details.forEach((detail) => {
|
|
3890
|
-
if (!isEditMode) delete detail[detailIdField];
|
|
3891
|
-
else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
|
|
3892
|
-
});
|
|
3893
|
-
return details;
|
|
3894
|
-
}
|
|
3895
|
-
//#endregion
|
|
3896
4372
|
//#region packages/crud/form/useFormSubmit.ts
|
|
3897
4373
|
/**
|
|
3898
4374
|
* Returns helpers for form serialization and HTTP submit/delete operations.
|
|
@@ -4075,30 +4551,30 @@ function mapApiViolations(violations, detailPropertyName = "items", defaultMessa
|
|
|
4075
4551
|
* the state seam, independently testable without the 900-line closure.
|
|
4076
4552
|
*/
|
|
4077
4553
|
function useFormState({ fields, onFieldDataChanged }) {
|
|
4078
|
-
const isEdit = (0, react.useRef)(false);
|
|
4079
|
-
const uploadedFiles = (0, react.useRef)([]);
|
|
4080
|
-
const existingMediaByField = (0, react.useRef)({});
|
|
4081
|
-
const upsertUploadedFile = (0, react.useCallback)((entry) => {
|
|
4554
|
+
const isEdit = (0, react$1.useRef)(false);
|
|
4555
|
+
const uploadedFiles = (0, react$1.useRef)([]);
|
|
4556
|
+
const existingMediaByField = (0, react$1.useRef)({});
|
|
4557
|
+
const upsertUploadedFile = (0, react$1.useCallback)((entry) => {
|
|
4082
4558
|
uploadedFiles.current = [...uploadedFiles.current.filter((file) => file.name !== entry.name), entry];
|
|
4083
4559
|
}, []);
|
|
4084
|
-
const [formData, setFormData] = (0, react.useState)(() => buildEmptyRow(fields));
|
|
4085
|
-
const formDataRef = (0, react.useRef)(formData);
|
|
4086
|
-
const [detailRows, setDetailRows] = (0, react.useState)([]);
|
|
4087
|
-
const detailRowsRef = (0, react.useRef)(detailRows);
|
|
4088
|
-
const [fieldState, setFieldState] = (0, react.useState)({});
|
|
4089
|
-
const [errors, setErrors] = (0, react.useState)({});
|
|
4090
|
-
const [detailErrors, setDetailErrors] = (0, react.useState)({});
|
|
4091
|
-
const prependDataRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
4092
|
-
const setNextFormData = (0, react.useCallback)((nextData) => {
|
|
4560
|
+
const [formData, setFormData] = (0, react$1.useState)(() => buildEmptyRow(fields));
|
|
4561
|
+
const formDataRef = (0, react$1.useRef)(formData);
|
|
4562
|
+
const [detailRows, setDetailRows] = (0, react$1.useState)([]);
|
|
4563
|
+
const detailRowsRef = (0, react$1.useRef)(detailRows);
|
|
4564
|
+
const [fieldState, setFieldState] = (0, react$1.useState)({});
|
|
4565
|
+
const [errors, setErrors] = (0, react$1.useState)({});
|
|
4566
|
+
const [detailErrors, setDetailErrors] = (0, react$1.useState)({});
|
|
4567
|
+
const prependDataRef = (0, react$1.useRef)(/* @__PURE__ */ new Map());
|
|
4568
|
+
const setNextFormData = (0, react$1.useCallback)((nextData) => {
|
|
4093
4569
|
formDataRef.current = nextData;
|
|
4094
4570
|
setFormData(nextData);
|
|
4095
4571
|
onFieldDataChanged?.(nextData);
|
|
4096
4572
|
}, [onFieldDataChanged]);
|
|
4097
|
-
const setNextDetailRows = (0, react.useCallback)((nextRows) => {
|
|
4573
|
+
const setNextDetailRows = (0, react$1.useCallback)((nextRows) => {
|
|
4098
4574
|
detailRowsRef.current = nextRows;
|
|
4099
4575
|
setDetailRows(nextRows);
|
|
4100
4576
|
}, []);
|
|
4101
|
-
const setFieldValue = (0, react.useCallback)((name, value) => {
|
|
4577
|
+
const setFieldValue = (0, react$1.useCallback)((name, value) => {
|
|
4102
4578
|
const field = fields.find((candidate) => candidate.name === name);
|
|
4103
4579
|
const nextData = {
|
|
4104
4580
|
...formDataRef.current,
|
|
@@ -4113,22 +4589,22 @@ function useFormState({ fields, onFieldDataChanged }) {
|
|
|
4113
4589
|
onFieldDataChanged?.(nextData);
|
|
4114
4590
|
(field?.onChange)?.(value);
|
|
4115
4591
|
}, [fields, onFieldDataChanged]);
|
|
4116
|
-
const setEditMode = (0, react.useCallback)((value) => {
|
|
4592
|
+
const setEditMode = (0, react$1.useCallback)((value) => {
|
|
4117
4593
|
isEdit.current = value;
|
|
4118
4594
|
}, []);
|
|
4119
|
-
const resetUploadSession = (0, react.useCallback)(() => {
|
|
4595
|
+
const resetUploadSession = (0, react$1.useCallback)(() => {
|
|
4120
4596
|
uploadedFiles.current = [];
|
|
4121
4597
|
}, []);
|
|
4122
|
-
const setExistingMedia = (0, react.useCallback)((media) => {
|
|
4598
|
+
const setExistingMedia = (0, react$1.useCallback)((media) => {
|
|
4123
4599
|
existingMediaByField.current = media;
|
|
4124
4600
|
}, []);
|
|
4125
|
-
const clearExistingMedia = (0, react.useCallback)((name) => {
|
|
4601
|
+
const clearExistingMedia = (0, react$1.useCallback)((name) => {
|
|
4126
4602
|
delete existingMediaByField.current[name];
|
|
4127
4603
|
}, []);
|
|
4128
|
-
const resetPrependData = (0, react.useCallback)(() => {
|
|
4604
|
+
const resetPrependData = (0, react$1.useCallback)(() => {
|
|
4129
4605
|
prependDataRef.current = /* @__PURE__ */ new Map();
|
|
4130
4606
|
}, []);
|
|
4131
|
-
const clearDetailCellError = (0, react.useCallback)((rowIndex, fieldName) => {
|
|
4607
|
+
const clearDetailCellError = (0, react$1.useCallback)((rowIndex, fieldName) => {
|
|
4132
4608
|
setDetailErrors((current) => {
|
|
4133
4609
|
const rowErrors = current[rowIndex];
|
|
4134
4610
|
if (!rowErrors?.[fieldName]) return current;
|
|
@@ -4140,10 +4616,10 @@ function useFormState({ fields, onFieldDataChanged }) {
|
|
|
4140
4616
|
return next;
|
|
4141
4617
|
});
|
|
4142
4618
|
}, []);
|
|
4143
|
-
(0, react.useEffect)(() => {
|
|
4619
|
+
(0, react$1.useEffect)(() => {
|
|
4144
4620
|
formDataRef.current = formData;
|
|
4145
4621
|
}, [formData]);
|
|
4146
|
-
(0, react.useEffect)(() => {
|
|
4622
|
+
(0, react$1.useEffect)(() => {
|
|
4147
4623
|
detailRowsRef.current = detailRows;
|
|
4148
4624
|
}, [detailRows]);
|
|
4149
4625
|
return {
|
|
@@ -5284,7 +5760,7 @@ function fromOperations(supportedOperations) {
|
|
|
5284
5760
|
*/
|
|
5285
5761
|
function usePermissions(resource, supportedOperations = []) {
|
|
5286
5762
|
const opsKey = supportedOperations.slice().sort().join(",");
|
|
5287
|
-
return (0, react.useMemo)(() => {
|
|
5763
|
+
return (0, react$1.useMemo)(() => {
|
|
5288
5764
|
const p = resource.permissions;
|
|
5289
5765
|
const inferred = fromOperations(supportedOperations);
|
|
5290
5766
|
function resolveWithInferred(permValue, inferredValue, platformDefault) {
|
|
@@ -5300,20 +5776,24 @@ function usePermissions(resource, supportedOperations = []) {
|
|
|
5300
5776
|
canExport: resolve(p?.canExport, false),
|
|
5301
5777
|
canBulkDelete: resolve(p?.canBulkDelete, false)
|
|
5302
5778
|
};
|
|
5303
|
-
}, [
|
|
5779
|
+
}, [
|
|
5780
|
+
resource.id,
|
|
5781
|
+
opsKey,
|
|
5782
|
+
resource.permissions
|
|
5783
|
+
]);
|
|
5304
5784
|
}
|
|
5305
5785
|
//#endregion
|
|
5306
5786
|
//#region packages/crud/crud/useSelectionState.ts
|
|
5307
5787
|
function useSelectionState(identityField) {
|
|
5308
|
-
const [selectedIds, setSelectedIds] = (0, react.useState)([]);
|
|
5309
|
-
const onSelectionChanged = (0, react.useCallback)((selectedRows) => {
|
|
5788
|
+
const [selectedIds, setSelectedIds] = (0, react$1.useState)([]);
|
|
5789
|
+
const onSelectionChanged = (0, react$1.useCallback)((selectedRows) => {
|
|
5310
5790
|
setSelectedIds(selectedRows.map((row) => {
|
|
5311
5791
|
const val = row[identityField];
|
|
5312
5792
|
if (typeof val === "string" || typeof val === "number") return val;
|
|
5313
5793
|
return String(val);
|
|
5314
5794
|
}));
|
|
5315
5795
|
}, [identityField]);
|
|
5316
|
-
const clearSelection = (0, react.useCallback)(() => {
|
|
5796
|
+
const clearSelection = (0, react$1.useCallback)(() => {
|
|
5317
5797
|
setSelectedIds([]);
|
|
5318
5798
|
}, []);
|
|
5319
5799
|
return {
|
|
@@ -5344,7 +5824,7 @@ function resolveVisibleColumns(resource, activeKey) {
|
|
|
5344
5824
|
return resource.columnPresets.find((p) => p.key === activeKey)?.columns ?? null;
|
|
5345
5825
|
}
|
|
5346
5826
|
function useColumnPreset(resource) {
|
|
5347
|
-
const [activePreset, setActivePresetState] = (0, react.useState)(() => {
|
|
5827
|
+
const [activePreset, setActivePresetState] = (0, react$1.useState)(() => {
|
|
5348
5828
|
if (!resource.columnPresets?.length) return null;
|
|
5349
5829
|
const stored = readFromStorage(resource.id);
|
|
5350
5830
|
if (stored && resource.columnPresets.some((p) => p.key === stored)) return stored;
|
|
@@ -5352,7 +5832,7 @@ function useColumnPreset(resource) {
|
|
|
5352
5832
|
});
|
|
5353
5833
|
return {
|
|
5354
5834
|
activePreset,
|
|
5355
|
-
setPreset: (0, react.useCallback)((key) => {
|
|
5835
|
+
setPreset: (0, react$1.useCallback)((key) => {
|
|
5356
5836
|
writeToStorage(resource.id, key);
|
|
5357
5837
|
setActivePresetState(key);
|
|
5358
5838
|
}, [resource.id]),
|
|
@@ -5673,19 +6153,19 @@ function buildFields(items) {
|
|
|
5673
6153
|
//#endregion
|
|
5674
6154
|
//#region packages/crud/crud/useCrudPage.ts
|
|
5675
6155
|
function useCrudPage(resource, externalFormRef) {
|
|
5676
|
-
const resolvedResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
|
|
6156
|
+
const resolvedResource = (0, react$1.useMemo)(() => resolveCrudResource(resource), [resource]);
|
|
5677
6157
|
const events = resolvedResource.events;
|
|
5678
|
-
const _internalFormRef = (0, react.useRef)(null);
|
|
6158
|
+
const _internalFormRef = (0, react$1.useRef)(null);
|
|
5679
6159
|
const formRef = externalFormRef ?? _internalFormRef;
|
|
5680
|
-
const fields = (0, react.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
|
|
6160
|
+
const fields = (0, react$1.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
|
|
5681
6161
|
return {
|
|
5682
6162
|
events,
|
|
5683
6163
|
resource: resolvedResource,
|
|
5684
6164
|
fields,
|
|
5685
|
-
formFields: (0, react.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
|
|
6165
|
+
formFields: (0, react$1.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
|
|
5686
6166
|
formRef,
|
|
5687
6167
|
permissions: usePermissions(resolvedResource, resolvedResource._supportedOperations ?? []),
|
|
5688
|
-
selectionState: useSelectionState((0, react.useMemo)(() => {
|
|
6168
|
+
selectionState: useSelectionState((0, react$1.useMemo)(() => {
|
|
5689
6169
|
return fields.find((f) => f.isIdentity)?.name ?? "id";
|
|
5690
6170
|
}, [fields])),
|
|
5691
6171
|
presetState: useColumnPreset(resolvedResource)
|
|
@@ -6044,7 +6524,7 @@ function useDialogStoreContext() {
|
|
|
6044
6524
|
function useCrudDialogStore(resourceId) {
|
|
6045
6525
|
const { state, dispatch } = useDialogStoreContext();
|
|
6046
6526
|
const dialogState = state[resourceId] ?? initialDialogState();
|
|
6047
|
-
const openDialog = (0, react.useCallback)((mode, rowData = null) => {
|
|
6527
|
+
const openDialog = (0, react$1.useCallback)((mode, rowData = null) => {
|
|
6048
6528
|
dispatch({
|
|
6049
6529
|
type: "OPEN",
|
|
6050
6530
|
resourceId,
|
|
@@ -6052,7 +6532,7 @@ function useCrudDialogStore(resourceId) {
|
|
|
6052
6532
|
rowData
|
|
6053
6533
|
});
|
|
6054
6534
|
}, [dispatch, resourceId]);
|
|
6055
|
-
const closeDialog = (0, react.useCallback)(() => {
|
|
6535
|
+
const closeDialog = (0, react$1.useCallback)(() => {
|
|
6056
6536
|
dispatch({
|
|
6057
6537
|
type: "CLOSE",
|
|
6058
6538
|
resourceId
|
|
@@ -6149,7 +6629,7 @@ function buildRoutingFilterRules(fields, initialFilters) {
|
|
|
6149
6629
|
* Public export — wraps CrudPageInner in its own DialogStoreProvider so that
|
|
6150
6630
|
* existing pages do not need to add a provider themselves.
|
|
6151
6631
|
*/
|
|
6152
|
-
const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, onOperationChange }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogStoreProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudPageInner, {
|
|
6632
|
+
const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid, onOperationChange }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogStoreProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudPageInner, {
|
|
6153
6633
|
resource,
|
|
6154
6634
|
onFormDataChange,
|
|
6155
6635
|
initialRecordId,
|
|
@@ -6162,9 +6642,10 @@ const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, c
|
|
|
6162
6642
|
editDisabled,
|
|
6163
6643
|
deleteDisabled,
|
|
6164
6644
|
gridRef,
|
|
6645
|
+
aboveGrid,
|
|
6165
6646
|
onOperationChange
|
|
6166
6647
|
}) });
|
|
6167
|
-
const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, initialIsNew = false, computedValues = EMPTY_COMPUTED_VALUES, initialFilters = EMPTY_INITIAL_FILTERS, onFiltersChange, formRef: externalFormRef, onSelectionChanged: externalOnSelectionChanged, editDisabled, deleteDisabled, gridRef: externalGridRef, onOperationChange }) => {
|
|
6648
|
+
const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, initialIsNew = false, computedValues = EMPTY_COMPUTED_VALUES, initialFilters = EMPTY_INITIAL_FILTERS, onFiltersChange, formRef: externalFormRef, onSelectionChanged: externalOnSelectionChanged, editDisabled, deleteDisabled, gridRef: externalGridRef, aboveGrid: aboveGridOverride, onOperationChange }) => {
|
|
6168
6649
|
const { t } = (0, _nubitio_core.useCoreTranslation)();
|
|
6169
6650
|
const { events, resource: resolvedResource, fields, formFields, formRef, permissions, selectionState, presetState } = useCrudPage((0, react.useMemo)(() => resolveCrudResource(resource), [resource]), externalFormRef);
|
|
6170
6651
|
const datagridFields = (0, react.useMemo)(() => fields.filter((field) => field.isIdentity || field.visible !== false), [fields]);
|
|
@@ -6427,6 +6908,16 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
|
|
|
6427
6908
|
resolvedResource.rowActions,
|
|
6428
6909
|
t
|
|
6429
6910
|
]);
|
|
6911
|
+
const aboveGridSlot = aboveGridOverride ?? resolvedResource.aboveGrid;
|
|
6912
|
+
const aboveGridContent = (() => {
|
|
6913
|
+
if (!aboveGridSlot) return;
|
|
6914
|
+
if (typeof aboveGridSlot === "function") return aboveGridSlot({
|
|
6915
|
+
resource: resolvedResource,
|
|
6916
|
+
gridRef,
|
|
6917
|
+
refresh: () => gridRef.current?.refresh()
|
|
6918
|
+
});
|
|
6919
|
+
return aboveGridSlot;
|
|
6920
|
+
})();
|
|
6430
6921
|
/** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
|
|
6431
6922
|
const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColumnPresetSelector, {
|
|
6432
6923
|
resourceId: resolvedResource.id,
|
|
@@ -6489,6 +6980,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
|
|
|
6489
6980
|
editMode: resolvedResource.editMode,
|
|
6490
6981
|
visibleColumns: presetState.visibleColumns,
|
|
6491
6982
|
beforeToolbar: renderPresetSelector,
|
|
6983
|
+
aboveGrid: aboveGridContent,
|
|
6492
6984
|
detailUrl: gridDetail?.url,
|
|
6493
6985
|
detailFields: gridDetail?.fields,
|
|
6494
6986
|
onFilterChange: onFiltersChange,
|
|
@@ -6639,7 +7131,7 @@ function useRouting(routing) {
|
|
|
6639
7131
|
return {
|
|
6640
7132
|
initialRecordId,
|
|
6641
7133
|
initialIsNew,
|
|
6642
|
-
initialFilters: (0, react.useMemo)(() => {
|
|
7134
|
+
initialFilters: (0, react$1.useMemo)(() => {
|
|
6643
7135
|
if (!syncFiltersToUrl) return NO_FILTERS;
|
|
6644
7136
|
const filters = {};
|
|
6645
7137
|
searchParams.forEach((value, key) => {
|
|
@@ -6647,7 +7139,7 @@ function useRouting(routing) {
|
|
|
6647
7139
|
});
|
|
6648
7140
|
return filters;
|
|
6649
7141
|
}, [syncFiltersToUrl, searchParams]),
|
|
6650
|
-
syncFilters: (0, react.useCallback)((filters) => {
|
|
7142
|
+
syncFilters: (0, react$1.useCallback)((filters) => {
|
|
6651
7143
|
if (!routing?.syncFiltersToUrl) return;
|
|
6652
7144
|
const next = new URLSearchParams();
|
|
6653
7145
|
Object.entries(filters).forEach(([key, value]) => {
|
|
@@ -6758,23 +7250,23 @@ function crudReducer(state, action) {
|
|
|
6758
7250
|
*/
|
|
6759
7251
|
function useSmartCrudOperation(events, routingState) {
|
|
6760
7252
|
const [on] = (0, _nubitio_core.useEvents)();
|
|
6761
|
-
const [{ activeOperation, formData }, dispatchState] = (0, react.useReducer)(crudReducer, routingState, stateFromRouting);
|
|
6762
|
-
const handleFormDataChange = (0, react.useCallback)((data) => {
|
|
7253
|
+
const [{ activeOperation, formData }, dispatchState] = (0, react$1.useReducer)(crudReducer, routingState, stateFromRouting);
|
|
7254
|
+
const handleFormDataChange = (0, react$1.useCallback)((data) => {
|
|
6763
7255
|
dispatchState({
|
|
6764
7256
|
type: "set-form-data",
|
|
6765
7257
|
data
|
|
6766
7258
|
});
|
|
6767
7259
|
}, []);
|
|
6768
|
-
const startCreate = (0, react.useCallback)(() => {
|
|
7260
|
+
const startCreate = (0, react$1.useCallback)(() => {
|
|
6769
7261
|
dispatchState({ type: "create" });
|
|
6770
7262
|
}, []);
|
|
6771
|
-
const startEdit = (0, react.useCallback)(() => {
|
|
7263
|
+
const startEdit = (0, react$1.useCallback)(() => {
|
|
6772
7264
|
dispatchState({ type: "edit" });
|
|
6773
7265
|
}, []);
|
|
6774
|
-
const resetOperation = (0, react.useCallback)(() => {
|
|
7266
|
+
const resetOperation = (0, react$1.useCallback)(() => {
|
|
6775
7267
|
dispatchState({ type: "reset" });
|
|
6776
7268
|
}, []);
|
|
6777
|
-
(0, react.useEffect)(() => {
|
|
7269
|
+
(0, react$1.useEffect)(() => {
|
|
6778
7270
|
dispatchState({
|
|
6779
7271
|
type: "sync-routing",
|
|
6780
7272
|
routingState: {
|
|
@@ -6783,7 +7275,7 @@ function useSmartCrudOperation(events, routingState) {
|
|
|
6783
7275
|
}
|
|
6784
7276
|
});
|
|
6785
7277
|
}, [routingState.initialIsNew, routingState.initialRecordId]);
|
|
6786
|
-
(0, react.useEffect)(() => {
|
|
7278
|
+
(0, react$1.useEffect)(() => {
|
|
6787
7279
|
const subscriptions = [];
|
|
6788
7280
|
if (events?.ADD) subscriptions.push(on(events.ADD, () => {
|
|
6789
7281
|
dispatchState({ type: "create" });
|
|
@@ -6829,7 +7321,7 @@ function useSmartCrudOperation(events, routingState) {
|
|
|
6829
7321
|
* so grids and forms can resolve row keys consistently.
|
|
6830
7322
|
*/
|
|
6831
7323
|
function useFieldPermissions(fields, userRoles) {
|
|
6832
|
-
return (0, react.useMemo)(() => {
|
|
7324
|
+
return (0, react$1.useMemo)(() => {
|
|
6833
7325
|
return fields.reduce((acc, field) => {
|
|
6834
7326
|
if (field.isIdentity) {
|
|
6835
7327
|
acc.push(field);
|
|
@@ -6899,7 +7391,7 @@ function evaluateConditionalRuleState(field, formData) {
|
|
|
6899
7391
|
* are returned. Empty objects `{}` are treated as valid create-form state.
|
|
6900
7392
|
*/
|
|
6901
7393
|
function useConditionalRules(fields, formData) {
|
|
6902
|
-
return (0, react.useMemo)(() => {
|
|
7394
|
+
return (0, react$1.useMemo)(() => {
|
|
6903
7395
|
return fields.map((field) => evaluateConditionalRuleState(field, formData));
|
|
6904
7396
|
}, [fields, formData]);
|
|
6905
7397
|
}
|
|
@@ -6934,9 +7426,9 @@ function useDependsOn(fields, formData) {
|
|
|
6934
7426
|
url: field.url ?? null,
|
|
6935
7427
|
values: (field.dependsOn ?? []).map((dep) => formData[dep])
|
|
6936
7428
|
}));
|
|
6937
|
-
const isMounted = (0, react.useRef)(false);
|
|
6938
|
-
const previousEntriesRef = (0, react.useRef)(null);
|
|
6939
|
-
(0, react.useEffect)(() => {
|
|
7429
|
+
const isMounted = (0, react$1.useRef)(false);
|
|
7430
|
+
const previousEntriesRef = (0, react$1.useRef)(null);
|
|
7431
|
+
(0, react$1.useEffect)(() => {
|
|
6940
7432
|
if (!isMounted.current) {
|
|
6941
7433
|
isMounted.current = true;
|
|
6942
7434
|
previousEntriesRef.current = currentEntries;
|
|
@@ -7118,7 +7610,7 @@ function applyFieldOperationSemantics(field, operation, behavior, owner = `Field
|
|
|
7118
7610
|
* from schema loading and routing concerns.
|
|
7119
7611
|
*/
|
|
7120
7612
|
function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
7121
|
-
const permissionFilteredFields = useFieldPermissions((0, react.useMemo)(() => {
|
|
7613
|
+
const permissionFilteredFields = useFieldPermissions((0, react$1.useMemo)(() => {
|
|
7122
7614
|
if (!activeOperation) return fields;
|
|
7123
7615
|
return fields.map((field) => {
|
|
7124
7616
|
const runtimeField = field;
|
|
@@ -7130,7 +7622,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
|
7130
7622
|
useDependsOn(permissionFilteredFields, formData);
|
|
7131
7623
|
return {
|
|
7132
7624
|
gridFields: permissionFilteredFields,
|
|
7133
|
-
processedFields: (0, react.useMemo)(() => {
|
|
7625
|
+
processedFields: (0, react$1.useMemo)(() => {
|
|
7134
7626
|
const stateByName = new Map(fieldStates.map((s) => [s.name, s]));
|
|
7135
7627
|
let anyChanged = false;
|
|
7136
7628
|
const merged = permissionFilteredFields.map((field) => {
|
|
@@ -7150,7 +7642,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
|
7150
7642
|
});
|
|
7151
7643
|
return anyChanged ? merged : permissionFilteredFields;
|
|
7152
7644
|
}, [permissionFilteredFields, fieldStates]),
|
|
7153
|
-
computedValues: (0, react.useMemo)(() => fieldStates.reduce((acc, state) => {
|
|
7645
|
+
computedValues: (0, react$1.useMemo)(() => fieldStates.reduce((acc, state) => {
|
|
7154
7646
|
if (state.computedValue !== void 0) acc[state.name] = state.computedValue;
|
|
7155
7647
|
return acc;
|
|
7156
7648
|
}, {}), [fieldStates])
|
|
@@ -7610,7 +8102,7 @@ function formatRuntimeErrorMessage(error) {
|
|
|
7610
8102
|
*
|
|
7611
8103
|
* URL deep-linking is wired via `initialRecordId` / `initialIsNew` props on CrudPage.
|
|
7612
8104
|
*/
|
|
7613
|
-
function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef }) {
|
|
8105
|
+
function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid }) {
|
|
7614
8106
|
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
7615
8107
|
const internalGridRef = (0, react.useRef)(null);
|
|
7616
8108
|
const effectiveGridRef = gridRef ?? internalGridRef;
|
|
@@ -7695,6 +8187,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
|
|
|
7695
8187
|
editDisabled,
|
|
7696
8188
|
deleteDisabled,
|
|
7697
8189
|
gridRef: effectiveGridRef,
|
|
8190
|
+
aboveGrid: aboveGrid ?? resource.aboveGrid,
|
|
7698
8191
|
onOperationChange: (operation) => {
|
|
7699
8192
|
if (operation === "create") {
|
|
7700
8193
|
startCreate();
|