@nubitio/crud 0.5.24 → 0.5.27
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 +925 -230
- package/dist/index.d.cts +27 -1
- package/dist/index.d.mts +27 -1
- package/dist/index.mjs +866 -174
- package/dist/style.css +258 -2
- 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,459 @@ 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 valuesEqual$1(a, b) {
|
|
2187
|
+
if (a === b) return true;
|
|
2188
|
+
if (a == null && b == null) return true;
|
|
2189
|
+
if (a == null || b == null) return false;
|
|
2190
|
+
const numA = Number(a);
|
|
2191
|
+
const numB = Number(b);
|
|
2192
|
+
if (!Number.isNaN(numA) && !Number.isNaN(numB)) return numA === numB;
|
|
2193
|
+
return String(a) === String(b);
|
|
2194
|
+
}
|
|
2195
|
+
function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError, onBatchSave }) {
|
|
2196
|
+
const [draftRows, setDraftRows] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
2197
|
+
const [savingRows, setSavingRows] = (0, react$1.useState)(/* @__PURE__ */ new Set());
|
|
2198
|
+
const [rowErrors, setRowErrors] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
2199
|
+
const [activeCell, setActiveCell] = (0, react$1.useState)(null);
|
|
2200
|
+
const draftRowsRef = (0, react$1.useRef)(draftRows);
|
|
2201
|
+
draftRowsRef.current = draftRows;
|
|
2202
|
+
const optsRef = (0, react$1.useRef)({
|
|
2203
|
+
url,
|
|
2204
|
+
idField,
|
|
2205
|
+
adapter,
|
|
2206
|
+
httpClient,
|
|
2207
|
+
fields,
|
|
2208
|
+
onSaveSuccess,
|
|
2209
|
+
onSaveError,
|
|
2210
|
+
onBatchSave
|
|
2211
|
+
});
|
|
2212
|
+
optsRef.current = {
|
|
2213
|
+
url,
|
|
2214
|
+
idField,
|
|
2215
|
+
adapter,
|
|
2216
|
+
httpClient,
|
|
2217
|
+
fields,
|
|
2218
|
+
onSaveSuccess,
|
|
2219
|
+
onSaveError,
|
|
2220
|
+
onBatchSave
|
|
2221
|
+
};
|
|
2222
|
+
const isEditing = (0, react$1.useCallback)((key) => draftRows.has(key), [draftRows]);
|
|
2223
|
+
const isCellActive = (0, react$1.useCallback)((key, fieldName) => activeCell != null && activeCell.key === key && activeCell.fieldName === fieldName, [activeCell]);
|
|
2224
|
+
const isCellDirty = (0, react$1.useCallback)((key, fieldName, original) => {
|
|
2225
|
+
const draft = draftRows.get(key);
|
|
2226
|
+
if (!draft) return false;
|
|
2227
|
+
return !valuesEqual$1(draft[fieldName], original[fieldName]);
|
|
2228
|
+
}, [draftRows]);
|
|
2229
|
+
const hasDraftChanges = (0, react$1.useCallback)((key, original) => {
|
|
2230
|
+
const draft = draftRows.get(key);
|
|
2231
|
+
if (!draft) return false;
|
|
2232
|
+
return optsRef.current.fields.some((field) => canEditFieldInline(field) && !valuesEqual$1(draft[field.name], original[field.name]));
|
|
2233
|
+
}, [draftRows]);
|
|
2234
|
+
const startEdit = (0, react$1.useCallback)((row) => {
|
|
2235
|
+
const key = row[optsRef.current.idField];
|
|
2236
|
+
setDraftRows((prev) => {
|
|
2237
|
+
const base = mode === "row" ? [] : Array.from(prev.entries());
|
|
2238
|
+
return new Map([...base, [key, { ...row }]]);
|
|
2239
|
+
});
|
|
2240
|
+
setRowErrors((prev) => {
|
|
2241
|
+
const next = new Map(prev);
|
|
2242
|
+
next.delete(key);
|
|
2243
|
+
return next;
|
|
2244
|
+
});
|
|
2245
|
+
setActiveCell(null);
|
|
2246
|
+
}, [mode]);
|
|
2247
|
+
const startCellEdit = (0, react$1.useCallback)((row, fieldName) => {
|
|
2248
|
+
const key = row[optsRef.current.idField];
|
|
2249
|
+
setDraftRows((prev) => {
|
|
2250
|
+
const existing = prev.get(key);
|
|
2251
|
+
const withoutKey = (mode === "row" ? [] : Array.from(prev.entries())).filter(([entryKey]) => entryKey !== key);
|
|
2252
|
+
return new Map([...withoutKey, [key, existing ? { ...existing } : { ...row }]]);
|
|
2253
|
+
});
|
|
2254
|
+
setRowErrors((prev) => {
|
|
2255
|
+
const next = new Map(prev);
|
|
2256
|
+
next.delete(key);
|
|
2257
|
+
return next;
|
|
2258
|
+
});
|
|
2259
|
+
setActiveCell({
|
|
2260
|
+
key,
|
|
2261
|
+
fieldName
|
|
2262
|
+
});
|
|
2263
|
+
}, [mode]);
|
|
2264
|
+
const stopCellEdit = (0, react$1.useCallback)(() => {
|
|
2265
|
+
setActiveCell(null);
|
|
2266
|
+
}, []);
|
|
2267
|
+
const cancelEdit = (0, react$1.useCallback)((key) => {
|
|
2268
|
+
setDraftRows((prev) => {
|
|
2269
|
+
const next = new Map(prev);
|
|
2270
|
+
next.delete(key);
|
|
2271
|
+
return next;
|
|
2272
|
+
});
|
|
2273
|
+
setRowErrors((prev) => {
|
|
2274
|
+
const next = new Map(prev);
|
|
2275
|
+
next.delete(key);
|
|
2276
|
+
return next;
|
|
2277
|
+
});
|
|
2278
|
+
setActiveCell((current) => current?.key === key ? null : current);
|
|
2279
|
+
}, []);
|
|
2280
|
+
const discardAll = (0, react$1.useCallback)(() => {
|
|
2281
|
+
setDraftRows(/* @__PURE__ */ new Map());
|
|
2282
|
+
setRowErrors(/* @__PURE__ */ new Map());
|
|
2283
|
+
setActiveCell(null);
|
|
2284
|
+
}, []);
|
|
2285
|
+
const updateDraft = (0, react$1.useCallback)((key, fieldName, value) => {
|
|
2286
|
+
setDraftRows((prev) => {
|
|
2287
|
+
const current = prev.get(key);
|
|
2288
|
+
if (!current) return prev;
|
|
2289
|
+
const next = new Map(prev);
|
|
2290
|
+
next.set(key, {
|
|
2291
|
+
...current,
|
|
2292
|
+
[fieldName]: value
|
|
2293
|
+
});
|
|
2294
|
+
return next;
|
|
2295
|
+
});
|
|
2296
|
+
}, []);
|
|
2297
|
+
const doSaveRow = (0, react$1.useCallback)(async (key) => {
|
|
2298
|
+
const { url: u, adapter: a, httpClient: http, fields: fs, onSaveError: onErr } = optsRef.current;
|
|
2299
|
+
const draft = draftRowsRef.current.get(key);
|
|
2300
|
+
if (!draft) return false;
|
|
2301
|
+
const errors = {};
|
|
2302
|
+
fs.forEach((field) => {
|
|
2303
|
+
if (!canEditFieldInline(field)) return;
|
|
2304
|
+
if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
|
|
2305
|
+
});
|
|
2306
|
+
if (Object.keys(errors).length > 0) {
|
|
2307
|
+
setRowErrors((prev) => new Map(prev).set(key, errors));
|
|
2308
|
+
return false;
|
|
2309
|
+
}
|
|
2310
|
+
setSavingRows((prev) => new Set([...prev, key]));
|
|
2311
|
+
const serialized = serializeFormFields(draft, fs.filter(canEditFieldInline), {
|
|
2312
|
+
uploadedFiles: [],
|
|
2313
|
+
getFieldValue: (name) => draft[name]
|
|
2314
|
+
});
|
|
2315
|
+
try {
|
|
2316
|
+
await http.patch(a.buildItemUrl(u, key), serialized);
|
|
2317
|
+
setDraftRows((prev) => {
|
|
2318
|
+
const next = new Map(prev);
|
|
2319
|
+
next.delete(key);
|
|
2320
|
+
return next;
|
|
2321
|
+
});
|
|
2322
|
+
setRowErrors((prev) => {
|
|
2323
|
+
const next = new Map(prev);
|
|
2324
|
+
next.delete(key);
|
|
2325
|
+
return next;
|
|
2326
|
+
});
|
|
2327
|
+
return true;
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
onErr?.(key, err);
|
|
2330
|
+
if (typeof err === "object" && err !== null && "status" in err && err.status === 422 && "data" in err) {
|
|
2331
|
+
const data = err.data;
|
|
2332
|
+
if (typeof data === "object" && data !== null && "violations" in data) {
|
|
2333
|
+
const violations = data.violations ?? [];
|
|
2334
|
+
const fieldErrors = {};
|
|
2335
|
+
violations.forEach((v) => {
|
|
2336
|
+
fieldErrors[v.propertyPath] = v.message;
|
|
2337
|
+
});
|
|
2338
|
+
setRowErrors((prev) => new Map(prev).set(key, fieldErrors));
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
return false;
|
|
2342
|
+
} finally {
|
|
2343
|
+
setSavingRows((prev) => {
|
|
2344
|
+
const next = new Set(prev);
|
|
2345
|
+
next.delete(key);
|
|
2346
|
+
return next;
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
}, []);
|
|
2350
|
+
const validateDraft = (0, react$1.useCallback)((key, draft, fs) => {
|
|
2351
|
+
const errors = {};
|
|
2352
|
+
fs.forEach((field) => {
|
|
2353
|
+
if (!canEditFieldInline(field)) return;
|
|
2354
|
+
if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
|
|
2355
|
+
});
|
|
2356
|
+
if (Object.keys(errors).length === 0) return true;
|
|
2357
|
+
setRowErrors((prev) => new Map(prev).set(key, errors));
|
|
2358
|
+
return false;
|
|
2359
|
+
}, []);
|
|
2360
|
+
const doSaveBatch = (0, react$1.useCallback)(async () => {
|
|
2361
|
+
const { fields: fs, onBatchSave: saveBatch, onSaveError: onErr } = optsRef.current;
|
|
2362
|
+
if (!saveBatch) return false;
|
|
2363
|
+
const entries = Array.from(draftRowsRef.current.entries());
|
|
2364
|
+
if (entries.length === 0) return false;
|
|
2365
|
+
if (!entries.every(([key, draft]) => validateDraft(key, draft, fs))) return false;
|
|
2366
|
+
const keys = entries.map(([key]) => key);
|
|
2367
|
+
setSavingRows((prev) => new Set([...prev, ...keys]));
|
|
2368
|
+
try {
|
|
2369
|
+
await saveBatch(entries.map(([, draft]) => ({ ...draft })));
|
|
2370
|
+
setDraftRows(/* @__PURE__ */ new Map());
|
|
2371
|
+
setRowErrors(/* @__PURE__ */ new Map());
|
|
2372
|
+
return true;
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
keys.forEach((key) => onErr?.(key, err));
|
|
2375
|
+
return false;
|
|
2376
|
+
} finally {
|
|
2377
|
+
setSavingRows((prev) => {
|
|
2378
|
+
const next = new Set(prev);
|
|
2379
|
+
keys.forEach((key) => next.delete(key));
|
|
2380
|
+
return next;
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
}, [validateDraft]);
|
|
2384
|
+
return {
|
|
2385
|
+
draftRows,
|
|
2386
|
+
savingRows,
|
|
2387
|
+
rowErrors,
|
|
2388
|
+
activeCell,
|
|
2389
|
+
isEditing,
|
|
2390
|
+
isCellActive,
|
|
2391
|
+
isCellDirty,
|
|
2392
|
+
hasDraftChanges,
|
|
2393
|
+
startEdit,
|
|
2394
|
+
startCellEdit,
|
|
2395
|
+
stopCellEdit,
|
|
2396
|
+
cancelEdit,
|
|
2397
|
+
discardAll,
|
|
2398
|
+
updateDraft,
|
|
2399
|
+
saveRow: (0, react$1.useCallback)(async (key) => {
|
|
2400
|
+
if (optsRef.current.onBatchSave) {
|
|
2401
|
+
const ok = await doSaveBatch();
|
|
2402
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2403
|
+
return ok;
|
|
2404
|
+
}
|
|
2405
|
+
const ok = await doSaveRow(key);
|
|
2406
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2407
|
+
return ok;
|
|
2408
|
+
}, [doSaveBatch, doSaveRow]),
|
|
2409
|
+
saveAll: (0, react$1.useCallback)(async () => {
|
|
2410
|
+
if (optsRef.current.onBatchSave) {
|
|
2411
|
+
const ok = await doSaveBatch();
|
|
2412
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2413
|
+
return ok;
|
|
2414
|
+
}
|
|
2415
|
+
const keys = Array.from(draftRowsRef.current.keys());
|
|
2416
|
+
const anySuccess = (await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value);
|
|
2417
|
+
if (anySuccess) optsRef.current.onSaveSuccess?.();
|
|
2418
|
+
return anySuccess;
|
|
2419
|
+
}, [doSaveBatch, doSaveRow])
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
function focusInlineControl(container) {
|
|
2423
|
+
if (!container) return;
|
|
2424
|
+
const focusable = container.querySelector("input:not([type=\"hidden\"]):not([disabled]):not([readonly]), select:not([disabled]), textarea:not([disabled]), .nb-date-picker__input:not([readonly]), .nb-dropdown__trigger:not([disabled])");
|
|
2425
|
+
focusable?.focus();
|
|
2426
|
+
if (focusable instanceof HTMLInputElement && focusable.type !== "checkbox") focusable.select();
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Renders a single cell as an editable control during inline editing.
|
|
2430
|
+
* Uses the same FieldTypeModule.ControlRender path as the full form, with
|
|
2431
|
+
* compact styling so controls fit naturally inside a table cell.
|
|
2432
|
+
*/
|
|
2433
|
+
function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t, autoFocus = true }) {
|
|
2434
|
+
const containerRef = (0, react.useRef)(null);
|
|
2435
|
+
const typeModule = getFieldTypeModule(field.type);
|
|
2436
|
+
const fieldError = errors?.[field.name];
|
|
2437
|
+
const errorClass = fieldError ? " is-error" : "";
|
|
2438
|
+
(0, react.useEffect)(() => {
|
|
2439
|
+
if (!autoFocus) return;
|
|
2440
|
+
focusInlineControl(containerRef.current);
|
|
2441
|
+
}, [
|
|
2442
|
+
autoFocus,
|
|
2443
|
+
field.name,
|
|
2444
|
+
rowKey
|
|
2445
|
+
]);
|
|
2446
|
+
const commonProps = {
|
|
2447
|
+
className: `nb-inline-control${errorClass}`,
|
|
2448
|
+
disabled,
|
|
2449
|
+
id: `iec-${String(rowKey)}-${field.name}`,
|
|
2450
|
+
name: field.name,
|
|
2451
|
+
onClick: void 0,
|
|
2452
|
+
readOnly: false,
|
|
2453
|
+
required: field.required
|
|
2454
|
+
};
|
|
2455
|
+
const ctx = {
|
|
2456
|
+
httpClient,
|
|
2457
|
+
t,
|
|
2458
|
+
remoteOptions: allRemoteOptions,
|
|
2459
|
+
getPrependData: () => void 0,
|
|
2460
|
+
getFieldValue: (name) => draft[name],
|
|
2461
|
+
getExistingMedia: () => null,
|
|
2462
|
+
clearExistingMedia: () => {},
|
|
2463
|
+
upsertUploadedFile: () => {}
|
|
2464
|
+
};
|
|
2465
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2466
|
+
ref: containerRef,
|
|
2467
|
+
className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
|
|
2468
|
+
onKeyDown: (event) => {
|
|
2469
|
+
if (event.key === "Escape") event.stopPropagation();
|
|
2470
|
+
},
|
|
2471
|
+
onMouseDown: (event) => {
|
|
2472
|
+
if (event.target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
|
|
2473
|
+
event.stopPropagation();
|
|
2474
|
+
},
|
|
2475
|
+
onClick: (event) => event.stopPropagation(),
|
|
2476
|
+
children: typeModule.ControlRender({
|
|
2477
|
+
field,
|
|
2478
|
+
value: draft[field.name],
|
|
2479
|
+
error: fieldError,
|
|
2480
|
+
errorClass,
|
|
2481
|
+
disabled,
|
|
2482
|
+
readOnly: false,
|
|
2483
|
+
commonProps,
|
|
2484
|
+
setFieldValue: onChange,
|
|
2485
|
+
ctx
|
|
2486
|
+
})
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
//#endregion
|
|
1865
2490
|
//#region packages/crud/summary/SummaryUtils.ts
|
|
1866
2491
|
function toFiniteNumber(value) {
|
|
1867
2492
|
if (value === null || value === void 0 || value === "") return null;
|
|
@@ -1931,19 +2556,20 @@ function resolveSummaryText(rows, item) {
|
|
|
1931
2556
|
const DETAIL_COL_WIDTH = 36;
|
|
1932
2557
|
const CHECKBOX_COL_WIDTH = 36;
|
|
1933
2558
|
const ACTIONS_COL_WIDTH = 44;
|
|
2559
|
+
const INLINE_ACTIONS_COL_WIDTH = 72;
|
|
1934
2560
|
const DEFAULT_COL_WIDTH = 120;
|
|
1935
2561
|
const MIN_COL_WIDTH = 48;
|
|
1936
2562
|
function getColumnWidth(field, colWidths) {
|
|
1937
2563
|
return colWidths[field.name] ?? field.width ?? field.minWidth ?? DEFAULT_COL_WIDTH;
|
|
1938
2564
|
}
|
|
1939
|
-
function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth }) {
|
|
2565
|
+
function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth, actionsColWidth = ACTIONS_COL_WIDTH }) {
|
|
1940
2566
|
let total = 0;
|
|
1941
2567
|
if (hasDetail) total += DETAIL_COL_WIDTH;
|
|
1942
2568
|
if (hasCheckbox) total += CHECKBOX_COL_WIDTH;
|
|
1943
2569
|
visibleFields.forEach((field) => {
|
|
1944
2570
|
total += getColumnWidth(field, colWidths);
|
|
1945
2571
|
});
|
|
1946
|
-
if (hasRowActions) total +=
|
|
2572
|
+
if (hasRowActions) total += actionsColWidth;
|
|
1947
2573
|
return Math.max(containerWidth, total);
|
|
1948
2574
|
}
|
|
1949
2575
|
function GridColumnGroup({ fields, colWidths, hasCheckbox, hasDetail, hasRowActions }) {
|
|
@@ -1975,6 +2601,25 @@ function normalizeIcon$1(icon) {
|
|
|
1975
2601
|
function isDateLikeField(field) {
|
|
1976
2602
|
return field.type === "date" || field.type === "datetime";
|
|
1977
2603
|
}
|
|
2604
|
+
function isCellEditMode(editMode) {
|
|
2605
|
+
return editMode === "cell" || editMode === "batch";
|
|
2606
|
+
}
|
|
2607
|
+
function resolveInlineEditToolbar(editMode, inlineEditToolbar) {
|
|
2608
|
+
if (inlineEditToolbar === false) return null;
|
|
2609
|
+
const defaultShow = isCellEditMode(editMode);
|
|
2610
|
+
if (inlineEditToolbar == null) return defaultShow ? {
|
|
2611
|
+
save: true,
|
|
2612
|
+
revert: true
|
|
2613
|
+
} : null;
|
|
2614
|
+
if (typeof inlineEditToolbar === "boolean") return inlineEditToolbar ? {
|
|
2615
|
+
save: true,
|
|
2616
|
+
revert: true
|
|
2617
|
+
} : null;
|
|
2618
|
+
return {
|
|
2619
|
+
save: inlineEditToolbar.save !== false,
|
|
2620
|
+
revert: inlineEditToolbar.revert !== false
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
1978
2623
|
function getToolbarKey(action, index) {
|
|
1979
2624
|
return action.key ?? action.text ?? String(index);
|
|
1980
2625
|
}
|
|
@@ -2326,9 +2971,20 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2326
2971
|
]);
|
|
2327
2972
|
const fieldsRef = (0, react.useRef)(options.fields);
|
|
2328
2973
|
fieldsRef.current = options.fields;
|
|
2974
|
+
const loadSeqRef = (0, react.useRef)(0);
|
|
2329
2975
|
const loadRows = (0, react.useCallback)(async () => {
|
|
2976
|
+
if (options.data) {
|
|
2977
|
+
rowsRef.current = options.data;
|
|
2978
|
+
setRows(options.data);
|
|
2979
|
+
setTotalCount(options.data.length);
|
|
2980
|
+
setGridSummary(options.gridSummary ?? null);
|
|
2981
|
+
setIsGridLoading(false);
|
|
2982
|
+
onContentReadyRef.current?.();
|
|
2983
|
+
return options.data;
|
|
2984
|
+
}
|
|
2330
2985
|
if (options.manualLoad) return rowsRef.current;
|
|
2331
2986
|
setIsGridLoading(true);
|
|
2987
|
+
const seq = ++loadSeqRef.current;
|
|
2332
2988
|
const loadOptions = {
|
|
2333
2989
|
filter: buildFilterExpression(filters, filterOperators, fieldsRef.current),
|
|
2334
2990
|
sort
|
|
@@ -2338,6 +2994,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2338
2994
|
loadOptions.take = pageSize;
|
|
2339
2995
|
}
|
|
2340
2996
|
const result = await source.load(loadOptions);
|
|
2997
|
+
if (seq !== loadSeqRef.current) return rowsRef.current;
|
|
2341
2998
|
rowsRef.current = result.data;
|
|
2342
2999
|
setRows(result.data);
|
|
2343
3000
|
setTotalCount(result.totalCount);
|
|
@@ -2348,6 +3005,8 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2348
3005
|
}, [
|
|
2349
3006
|
filterOperators,
|
|
2350
3007
|
filters,
|
|
3008
|
+
options.data,
|
|
3009
|
+
options.gridSummary,
|
|
2351
3010
|
options.manualLoad,
|
|
2352
3011
|
options.paging,
|
|
2353
3012
|
page,
|
|
@@ -2391,19 +3050,58 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2391
3050
|
resourceStoreFactory,
|
|
2392
3051
|
visibleFields
|
|
2393
3052
|
]);
|
|
3053
|
+
const canInlineEditMode = options.editMode === "row" || options.editMode === "cell" || options.editMode === "batch";
|
|
3054
|
+
const cellEditMode = isCellEditMode(options.editMode);
|
|
3055
|
+
const rowInlineMode = options.editMode === "row";
|
|
3056
|
+
const inlineEditToolbar = resolveInlineEditToolbar(options.editMode, options.inlineEditToolbar);
|
|
3057
|
+
const showRowInlineActions = options.inlineRowActions ?? rowInlineMode;
|
|
3058
|
+
const inlineEdit = useInlineEdit({
|
|
3059
|
+
mode: cellEditMode ? "batch" : "row",
|
|
3060
|
+
url: options.url,
|
|
3061
|
+
idField,
|
|
3062
|
+
adapter: options.adapter,
|
|
3063
|
+
httpClient,
|
|
3064
|
+
fields: options.fields,
|
|
3065
|
+
onSaveSuccess: () => void loadRows(),
|
|
3066
|
+
onSaveError: () => {},
|
|
3067
|
+
onBatchSave: options.onBatchSave
|
|
3068
|
+
});
|
|
3069
|
+
const dirtyRowCount = (0, react.useMemo)(() => rows.filter((row) => {
|
|
3070
|
+
const key = row[idField] ?? row;
|
|
3071
|
+
return inlineEdit.draftRows.has(key) && inlineEdit.hasDraftChanges(key, row);
|
|
3072
|
+
}).length, [
|
|
3073
|
+
rows,
|
|
3074
|
+
idField,
|
|
3075
|
+
inlineEdit.draftRows,
|
|
3076
|
+
inlineEdit.hasDraftChanges
|
|
3077
|
+
]);
|
|
3078
|
+
const hasPendingInlineEdits = dirtyRowCount > 0;
|
|
3079
|
+
(0, react.useEffect)(() => {
|
|
3080
|
+
if (!inlineEdit.activeCell) return;
|
|
3081
|
+
const handler = (event) => {
|
|
3082
|
+
const target = event.target;
|
|
3083
|
+
if (target.closest(".nb-datagrid__edit-cell")) return;
|
|
3084
|
+
if (target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
|
|
3085
|
+
inlineEdit.stopCellEdit();
|
|
3086
|
+
};
|
|
3087
|
+
document.addEventListener("mousedown", handler);
|
|
3088
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
3089
|
+
}, [inlineEdit.activeCell, inlineEdit.stopCellEdit]);
|
|
2394
3090
|
const handleStateRef = (0, react.useRef)({
|
|
2395
3091
|
selectedKeys,
|
|
2396
3092
|
filters,
|
|
2397
3093
|
filterOperators,
|
|
2398
3094
|
loadRows,
|
|
2399
|
-
idField
|
|
3095
|
+
idField,
|
|
3096
|
+
inlineEdit
|
|
2400
3097
|
});
|
|
2401
3098
|
handleStateRef.current = {
|
|
2402
3099
|
selectedKeys,
|
|
2403
3100
|
filters,
|
|
2404
3101
|
filterOperators,
|
|
2405
3102
|
loadRows,
|
|
2406
|
-
idField
|
|
3103
|
+
idField,
|
|
3104
|
+
inlineEdit
|
|
2407
3105
|
};
|
|
2408
3106
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
2409
3107
|
showLoading: (message) => {
|
|
@@ -2443,7 +3141,9 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2443
3141
|
setFilterInputs(nextFilters);
|
|
2444
3142
|
setFilterOperators(nextOperators);
|
|
2445
3143
|
setPage(0);
|
|
2446
|
-
}
|
|
3144
|
+
},
|
|
3145
|
+
hasEditData: () => handleStateRef.current.inlineEdit.draftRows.size > 0,
|
|
3146
|
+
saveChanges: () => handleStateRef.current.inlineEdit.saveAll()
|
|
2447
3147
|
}), [options.fields, t]);
|
|
2448
3148
|
const selectedRows = rows.filter((row) => selectedKeys.includes(row[idField]));
|
|
2449
3149
|
const selectRow = (row) => {
|
|
@@ -2457,7 +3157,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2457
3157
|
const rowEditable = (row) => options.canEditRow?.(row) !== false;
|
|
2458
3158
|
const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
|
|
2459
3159
|
const buildRowActions = (row) => [
|
|
2460
|
-
...options.allowEdit && rowEditable(row) &&
|
|
3160
|
+
...options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
|
|
3161
|
+
text: t("grid.inlineEditRow"),
|
|
3162
|
+
icon: "ph-pencil-simple",
|
|
3163
|
+
disabled: options.editDisabled,
|
|
3164
|
+
onClick: () => inlineEdit.startEdit(row)
|
|
3165
|
+
}] : [],
|
|
3166
|
+
...options.allowEdit && rowEditable(row) && !canInlineEditMode && (options.onEdit || options.events?.EDIT) ? [{
|
|
2461
3167
|
text: t("grid.buttonEdit"),
|
|
2462
3168
|
icon: "ph-pencil-simple",
|
|
2463
3169
|
disabled: options.editDisabled,
|
|
@@ -2481,6 +3187,10 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2481
3187
|
}] : []
|
|
2482
3188
|
];
|
|
2483
3189
|
const openRow = (row) => {
|
|
3190
|
+
if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
|
|
3191
|
+
inlineEdit.startEdit(row);
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
2484
3194
|
if (options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT)) {
|
|
2485
3195
|
if (options.onEdit) options.onEdit(row);
|
|
2486
3196
|
else emit(options.events.EDIT, { row });
|
|
@@ -2612,28 +3322,41 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2612
3322
|
};
|
|
2613
3323
|
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
2614
3324
|
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));
|
|
3325
|
+
const hasBuiltInRowActions = Boolean(options.allowEdit && (rowInlineMode && showRowInlineActions || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
|
|
2616
3326
|
const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
|
|
2617
3327
|
const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
|
|
2618
3328
|
const hasDetail = Boolean(options.detailFields);
|
|
3329
|
+
const actionsColWidth = canInlineEditMode ? INLINE_ACTIONS_COL_WIDTH : ACTIONS_COL_WIDTH;
|
|
2619
3330
|
const layoutWidth = computeLayoutWidth({
|
|
2620
3331
|
visibleFields,
|
|
2621
3332
|
colWidths,
|
|
2622
3333
|
hasCheckbox,
|
|
2623
3334
|
hasDetail,
|
|
2624
3335
|
hasRowActions,
|
|
2625
|
-
containerWidth
|
|
3336
|
+
containerWidth,
|
|
3337
|
+
actionsColWidth
|
|
2626
3338
|
});
|
|
2627
3339
|
const resolvedColWidths = (0, react.useMemo)(() => {
|
|
2628
|
-
if (visibleFields.length === 0) return colWidths;
|
|
2629
|
-
const
|
|
2630
|
-
const
|
|
2631
|
-
const
|
|
3340
|
+
if (visibleFields.length === 0 || containerWidth <= 0) return colWidths;
|
|
3341
|
+
const bases = visibleFields.map((f) => getColumnWidth(f, colWidths));
|
|
3342
|
+
const dataTotal = bases.reduce((sum, width) => sum + width, 0);
|
|
3343
|
+
const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
|
|
3344
|
+
const available = Math.max(0, containerWidth - fixedTotal);
|
|
3345
|
+
if (dataTotal > available && available > 0) {
|
|
3346
|
+
const scale = available / dataTotal;
|
|
3347
|
+
const result = {};
|
|
3348
|
+
visibleFields.forEach((field, index) => {
|
|
3349
|
+
const floor = field.minWidth ?? MIN_COL_WIDTH;
|
|
3350
|
+
result[field.name] = Math.max(floor, Math.floor(bases[index] * scale));
|
|
3351
|
+
});
|
|
3352
|
+
return result;
|
|
3353
|
+
}
|
|
3354
|
+
const extra = Math.max(0, available - dataTotal);
|
|
2632
3355
|
if (extra === 0 || dataTotal === 0) return colWidths;
|
|
2633
3356
|
let distributed = 0;
|
|
2634
3357
|
const result = {};
|
|
2635
3358
|
visibleFields.forEach((f, i) => {
|
|
2636
|
-
const base =
|
|
3359
|
+
const base = bases[i];
|
|
2637
3360
|
const share = i < visibleFields.length - 1 ? Math.round(extra * (base / dataTotal)) : extra - distributed;
|
|
2638
3361
|
result[f.name] = base + share;
|
|
2639
3362
|
distributed += share;
|
|
@@ -2645,7 +3368,8 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2645
3368
|
hasCheckbox,
|
|
2646
3369
|
hasDetail,
|
|
2647
3370
|
hasRowActions,
|
|
2648
|
-
containerWidth
|
|
3371
|
+
containerWidth,
|
|
3372
|
+
actionsColWidth
|
|
2649
3373
|
]);
|
|
2650
3374
|
const tableLayoutStyle = { "--nb-datagrid-layout-width": `${layoutWidth}px` };
|
|
2651
3375
|
const filterableFields = visibleFields.filter((field) => field.filterable);
|
|
@@ -2724,6 +3448,18 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2724
3448
|
children: activeFilterCount
|
|
2725
3449
|
})]
|
|
2726
3450
|
}),
|
|
3451
|
+
inlineEditToolbar?.revert && hasPendingInlineEdits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3452
|
+
className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--revert",
|
|
3453
|
+
icon: "ph ph-arrow-counter-clockwise",
|
|
3454
|
+
label: t("grid.inlineRevertChanges"),
|
|
3455
|
+
onClick: () => inlineEdit.discardAll()
|
|
3456
|
+
}),
|
|
3457
|
+
inlineEditToolbar?.save && hasPendingInlineEdits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3458
|
+
className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--save",
|
|
3459
|
+
icon: "ph ph-floppy-disk",
|
|
3460
|
+
label: t("grid.inlineSaveChanges"),
|
|
3461
|
+
onClick: () => void inlineEdit.saveAll()
|
|
3462
|
+
}),
|
|
2727
3463
|
toolbar.showRefresh !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
2728
3464
|
icon: "ph ph-arrow-clockwise",
|
|
2729
3465
|
label: t("grid.buttonRefresh"),
|
|
@@ -2731,6 +3467,14 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2731
3467
|
})
|
|
2732
3468
|
]
|
|
2733
3469
|
}),
|
|
3470
|
+
cellEditMode && hasPendingInlineEdits && !inlineEditToolbar && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3471
|
+
className: "nb-datagrid__batch-bar nb-datagrid__batch-bar--compact",
|
|
3472
|
+
role: "status",
|
|
3473
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3474
|
+
className: "nb-datagrid__batch-bar-label",
|
|
3475
|
+
children: t("grid.inlineUnsavedRows", { count: dirtyRowCount })
|
|
3476
|
+
})
|
|
3477
|
+
}),
|
|
2734
3478
|
options.aboveGrid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2735
3479
|
className: "nb-datagrid__above-grid",
|
|
2736
3480
|
children: options.aboveGrid
|
|
@@ -3024,16 +3768,37 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3024
3768
|
}) }) : rows.map((row, rowIndex) => {
|
|
3025
3769
|
const key = row[idField] ?? rowIndex;
|
|
3026
3770
|
const selected = selectedKeys.includes(key);
|
|
3771
|
+
const editing = inlineEdit.draftRows.has(key);
|
|
3772
|
+
const rowHasChanges = editing && inlineEdit.hasDraftChanges(key, row);
|
|
3773
|
+
const saving = inlineEdit.savingRows.has(key);
|
|
3774
|
+
const rowDraft = inlineEdit.draftRows.get(key) ?? row;
|
|
3775
|
+
const rowFieldErrors = inlineEdit.rowErrors.get(key);
|
|
3776
|
+
const rowInRowEdit = rowInlineMode && editing;
|
|
3027
3777
|
const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
|
|
3028
3778
|
const detailUrl = options.detailUrl?.replace("{id}", String(key));
|
|
3029
3779
|
const expanded = expandedKeys.has(key);
|
|
3030
3780
|
const rowActions = buildRowActions(row);
|
|
3031
3781
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
3032
|
-
className:
|
|
3782
|
+
className: [
|
|
3783
|
+
"nb-datagrid__row",
|
|
3784
|
+
expanded ? "nb-datagrid__row--expanded" : "",
|
|
3785
|
+
selected ? "nb-datagrid__row--selected" : "",
|
|
3786
|
+
rowInRowEdit ? "nb-datagrid__row--editing" : "",
|
|
3787
|
+
rowHasChanges ? "nb-datagrid__row--dirty" : "",
|
|
3788
|
+
saving ? "nb-datagrid__row--saving" : ""
|
|
3789
|
+
].filter(Boolean).join(" "),
|
|
3033
3790
|
tabIndex: 0,
|
|
3034
3791
|
"aria-selected": selected,
|
|
3035
|
-
onClick: () =>
|
|
3792
|
+
onClick: () => {
|
|
3793
|
+
if (rowInRowEdit) return;
|
|
3794
|
+
selectRow(row);
|
|
3795
|
+
},
|
|
3036
3796
|
onDoubleClick: () => {
|
|
3797
|
+
if (rowInRowEdit || inlineEdit.activeCell) return;
|
|
3798
|
+
if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
|
|
3799
|
+
inlineEdit.startEdit(row);
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3037
3802
|
if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
|
|
3038
3803
|
if (options.onEdit) options.onEdit(row);
|
|
3039
3804
|
else emit(options.events.EDIT, { row });
|
|
@@ -3042,10 +3807,14 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3042
3807
|
if (options.allowView && options.onView) options.onView(row);
|
|
3043
3808
|
},
|
|
3044
3809
|
onKeyDown: (event) => {
|
|
3045
|
-
if (event.key === "
|
|
3810
|
+
if (inlineEdit.activeCell && event.key === "Escape") inlineEdit.stopCellEdit();
|
|
3811
|
+
else if (rowInRowEdit && event.key === "Escape") inlineEdit.cancelEdit(key);
|
|
3812
|
+
else if (rowInRowEdit && event.key === "Enter" && showRowInlineActions) inlineEdit.saveRow(key);
|
|
3813
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && rowInlineMode && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
|
|
3814
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
|
|
3046
3815
|
else emit(options.events.EDIT, { row });
|
|
3047
|
-
else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
3048
|
-
else if (event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
|
|
3816
|
+
else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
3817
|
+
else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
|
|
3049
3818
|
else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
3050
3819
|
event.preventDefault();
|
|
3051
3820
|
const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
|
|
@@ -3090,19 +3859,87 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3090
3859
|
})
|
|
3091
3860
|
}),
|
|
3092
3861
|
visibleFields.map((field, columnIndex) => {
|
|
3862
|
+
const width = getColumnWidth(field, resolvedColWidths);
|
|
3863
|
+
const editable = options.allowEdit && !options.editDisabled && rowEditable(row) && canEditFieldInline(field);
|
|
3864
|
+
const cellActive = inlineEdit.isCellActive(key, field.name);
|
|
3865
|
+
const cellDirty = editing && inlineEdit.isCellDirty(key, field.name, row);
|
|
3866
|
+
const showCellEditor = editable && (rowInRowEdit && editing || cellEditMode && cellActive);
|
|
3867
|
+
const displayRow = editing ? rowDraft : row;
|
|
3868
|
+
const cellClassName = [
|
|
3869
|
+
showCellEditor ? "nb-datagrid__edit-cell" : "nb-datagrid__data-cell",
|
|
3870
|
+
editable && canInlineEditMode ? "nb-datagrid__cell--editable" : "",
|
|
3871
|
+
cellDirty ? "nb-datagrid__cell--dirty" : "",
|
|
3872
|
+
cellActive ? "nb-datagrid__cell--active" : ""
|
|
3873
|
+
].filter(Boolean).join(" ");
|
|
3874
|
+
const beginInlineEdit = () => {
|
|
3875
|
+
if (!editable) return;
|
|
3876
|
+
selectRow(row);
|
|
3877
|
+
if (cellEditMode) inlineEdit.startCellEdit(row, field.name);
|
|
3878
|
+
else if (rowInlineMode && !editing) inlineEdit.startEdit(row);
|
|
3879
|
+
};
|
|
3880
|
+
if (showCellEditor) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3881
|
+
style: {
|
|
3882
|
+
width,
|
|
3883
|
+
textAlign: field.align
|
|
3884
|
+
},
|
|
3885
|
+
className: cellClassName,
|
|
3886
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineEditCell, {
|
|
3887
|
+
field,
|
|
3888
|
+
rowKey: key,
|
|
3889
|
+
draft: rowDraft,
|
|
3890
|
+
onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
|
|
3891
|
+
errors: rowFieldErrors,
|
|
3892
|
+
disabled: saving,
|
|
3893
|
+
allRemoteOptions: filterRemoteOptions,
|
|
3894
|
+
httpClient,
|
|
3895
|
+
t,
|
|
3896
|
+
autoFocus: true
|
|
3897
|
+
})
|
|
3898
|
+
}, field.name);
|
|
3093
3899
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3094
3900
|
style: {
|
|
3095
|
-
width
|
|
3901
|
+
width,
|
|
3096
3902
|
textAlign: field.align
|
|
3097
3903
|
},
|
|
3098
|
-
|
|
3099
|
-
|
|
3904
|
+
className: cellClassName,
|
|
3905
|
+
title: getCellText(field, displayRow, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
|
|
3906
|
+
onClick: (event) => {
|
|
3907
|
+
if (!editable || !canInlineEditMode) return;
|
|
3908
|
+
event.stopPropagation();
|
|
3909
|
+
beginInlineEdit();
|
|
3910
|
+
},
|
|
3911
|
+
children: renderCell(field, displayRow, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
|
|
3100
3912
|
}, field.name);
|
|
3101
3913
|
}),
|
|
3102
3914
|
hasRowActions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3103
3915
|
className: "nb-datagrid__actions-cell",
|
|
3104
3916
|
onClick: (e) => e.stopPropagation(),
|
|
3105
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
3917
|
+
children: rowInRowEdit && showRowInlineActions ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3918
|
+
className: "nb-datagrid__inline-actions",
|
|
3919
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3920
|
+
type: "button",
|
|
3921
|
+
className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--save",
|
|
3922
|
+
disabled: saving,
|
|
3923
|
+
"aria-label": t("grid.inlineSaveRow"),
|
|
3924
|
+
title: t("grid.inlineSaveRow"),
|
|
3925
|
+
onClick: () => void inlineEdit.saveRow(key),
|
|
3926
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
3927
|
+
className: "ph ph-check",
|
|
3928
|
+
"aria-hidden": "true"
|
|
3929
|
+
})
|
|
3930
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3931
|
+
type: "button",
|
|
3932
|
+
className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--cancel",
|
|
3933
|
+
disabled: saving,
|
|
3934
|
+
"aria-label": t("grid.inlineCancelRow"),
|
|
3935
|
+
title: t("grid.inlineCancelRow"),
|
|
3936
|
+
onClick: () => inlineEdit.cancelEdit(key),
|
|
3937
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
3938
|
+
className: "ph ph-x",
|
|
3939
|
+
"aria-hidden": "true"
|
|
3940
|
+
})
|
|
3941
|
+
})]
|
|
3942
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3106
3943
|
className: "nb-datagrid__row-actions",
|
|
3107
3944
|
children: rowActions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3108
3945
|
icon: "ph ph-dots-three-vertical",
|
|
@@ -3452,97 +4289,6 @@ const FORM_EVENTS = {
|
|
|
3452
4289
|
};
|
|
3453
4290
|
const FORM_ERRORS_EVENT = "form-errors";
|
|
3454
4291
|
//#endregion
|
|
3455
|
-
//#region packages/crud/adapter/HydraAdapter.ts
|
|
3456
|
-
function trimTrailingSlash(value) {
|
|
3457
|
-
return value.replace(/\/+$/, "");
|
|
3458
|
-
}
|
|
3459
|
-
function buildIRI(url, value) {
|
|
3460
|
-
if (value.startsWith("/")) return value;
|
|
3461
|
-
return `${url}/${value}`;
|
|
3462
|
-
}
|
|
3463
|
-
/**
|
|
3464
|
-
* Default backend adapter for API Platform / JSON-LD + Hydra backends.
|
|
3465
|
-
*
|
|
3466
|
-
* Conventions assumed:
|
|
3467
|
-
* - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
|
|
3468
|
-
* - Entity fields are serialized as IRI strings in POST/PATCH bodies.
|
|
3469
|
-
* - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
|
|
3470
|
-
* - `_iri` is a synthetic alias for `@id` used internally by the engine.
|
|
3471
|
-
*/
|
|
3472
|
-
const HydraAdapter = {
|
|
3473
|
-
getRowId(record, idField) {
|
|
3474
|
-
const direct = record[idField];
|
|
3475
|
-
if (direct !== void 0 && direct !== null) return String(direct);
|
|
3476
|
-
const iri = record["@id"] ?? record["_iri"];
|
|
3477
|
-
if (iri !== void 0 && iri !== null) return String(iri);
|
|
3478
|
-
return String(record["id"] ?? "");
|
|
3479
|
-
},
|
|
3480
|
-
buildItemUrl(baseUrl, id) {
|
|
3481
|
-
const str = String(id);
|
|
3482
|
-
if (str.startsWith("/")) return str;
|
|
3483
|
-
return `${trimTrailingSlash(baseUrl)}/${str}`;
|
|
3484
|
-
},
|
|
3485
|
-
serializeEntityRef(field, rawValue) {
|
|
3486
|
-
if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
|
|
3487
|
-
if (typeof rawValue === "object") {
|
|
3488
|
-
const entity = rawValue;
|
|
3489
|
-
const atId = entity["@id"];
|
|
3490
|
-
if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
|
|
3491
|
-
const idValue = entity["id"];
|
|
3492
|
-
const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
|
|
3493
|
-
return buildIRI(field.url ?? "", resolvedId);
|
|
3494
|
-
}
|
|
3495
|
-
return buildIRI(field.url ?? "", String(rawValue));
|
|
3496
|
-
},
|
|
3497
|
-
normalizeEntityValue(rawValue, field) {
|
|
3498
|
-
if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
|
|
3499
|
-
if (typeof rawValue === "object" && rawValue !== null) {
|
|
3500
|
-
const entity = rawValue;
|
|
3501
|
-
const atId = entity["@id"];
|
|
3502
|
-
if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
|
|
3503
|
-
const directValue = entity[field.valueField];
|
|
3504
|
-
if (directValue !== void 0 && directValue !== null) return directValue;
|
|
3505
|
-
}
|
|
3506
|
-
return rawValue;
|
|
3507
|
-
},
|
|
3508
|
-
getEntityOptionKey(item, field) {
|
|
3509
|
-
if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
|
|
3510
|
-
return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
|
|
3511
|
-
},
|
|
3512
|
-
parseListResponse(response) {
|
|
3513
|
-
const r = response;
|
|
3514
|
-
const member = r["hydra:member"];
|
|
3515
|
-
if (Array.isArray(member)) return {
|
|
3516
|
-
items: member,
|
|
3517
|
-
total: Number(r["hydra:totalItems"] ?? member.length)
|
|
3518
|
-
};
|
|
3519
|
-
if (Array.isArray(response)) return {
|
|
3520
|
-
items: response,
|
|
3521
|
-
total: response.length
|
|
3522
|
-
};
|
|
3523
|
-
return {
|
|
3524
|
-
items: [],
|
|
3525
|
-
total: 0
|
|
3526
|
-
};
|
|
3527
|
-
},
|
|
3528
|
-
synthesizeEntityKey(field, entityValue) {
|
|
3529
|
-
if (!field.url) return void 0;
|
|
3530
|
-
const base = trimTrailingSlash(field.url);
|
|
3531
|
-
const directId = entityValue["id"];
|
|
3532
|
-
if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
|
|
3533
|
-
const directValue = entityValue[field.valueField];
|
|
3534
|
-
if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
|
|
3535
|
-
for (const key of [
|
|
3536
|
-
"code",
|
|
3537
|
-
"uuid",
|
|
3538
|
-
"slug"
|
|
3539
|
-
]) {
|
|
3540
|
-
const candidate = entityValue[key];
|
|
3541
|
-
if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
};
|
|
3545
|
-
//#endregion
|
|
3546
4292
|
//#region packages/crud/form/FormDataTransform.ts
|
|
3547
4293
|
function upsertPrependData(store, field, item) {
|
|
3548
4294
|
const existing = store.get(field.name);
|
|
@@ -3844,59 +4590,6 @@ function buildFieldColSpanContext(options) {
|
|
|
3844
4590
|
};
|
|
3845
4591
|
}
|
|
3846
4592
|
//#endregion
|
|
3847
|
-
//#region packages/crud/form/serializeFormData.ts
|
|
3848
|
-
function applySerializedValue(formData, field, result) {
|
|
3849
|
-
if (result.kind === "set") formData[field.name] = result.value;
|
|
3850
|
-
else if (result.kind === "omit") delete formData[field.name];
|
|
3851
|
-
}
|
|
3852
|
-
/**
|
|
3853
|
-
* Pure serialization of form data before HTTP submission.
|
|
3854
|
-
*
|
|
3855
|
-
* Applies uploaded file references and computed fields, then delegates the
|
|
3856
|
-
* per-type wire format (entity refs, business dates, numeric coercion, file
|
|
3857
|
-
* handling, NONE stripping) to each field's Field-Type module.
|
|
3858
|
-
*
|
|
3859
|
-
* This function is extracted from useFormSubmit for testability.
|
|
3860
|
-
*/
|
|
3861
|
-
function serializeFormFields(rawData, fields, ctx) {
|
|
3862
|
-
const formData = { ...rawData };
|
|
3863
|
-
ctx.uploadedFiles.forEach((file) => {
|
|
3864
|
-
formData[file.name] = file.iri;
|
|
3865
|
-
});
|
|
3866
|
-
fields.forEach((field) => {
|
|
3867
|
-
if (!field.computed) return;
|
|
3868
|
-
const computedValue = field.computed(formData);
|
|
3869
|
-
if (computedValue !== void 0) formData[field.name] = computedValue;
|
|
3870
|
-
});
|
|
3871
|
-
const moduleCtx = {
|
|
3872
|
-
adapter: ctx.adapter ?? HydraAdapter,
|
|
3873
|
-
format: ctx.format,
|
|
3874
|
-
getFieldValue: ctx.getFieldValue
|
|
3875
|
-
};
|
|
3876
|
-
fields.forEach((field) => {
|
|
3877
|
-
applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
|
|
3878
|
-
});
|
|
3879
|
-
return formData;
|
|
3880
|
-
}
|
|
3881
|
-
/**
|
|
3882
|
-
* Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
|
|
3883
|
-
* Pure function — operates on an array of row records.
|
|
3884
|
-
*/
|
|
3885
|
-
function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
|
|
3886
|
-
const details = JSON.parse(JSON.stringify(rows));
|
|
3887
|
-
detailFields.forEach((field) => {
|
|
3888
|
-
const typeModule = getFieldTypeModule(field.type);
|
|
3889
|
-
details.forEach((detail) => {
|
|
3890
|
-
applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
|
|
3891
|
-
});
|
|
3892
|
-
});
|
|
3893
|
-
details.forEach((detail) => {
|
|
3894
|
-
if (!isEditMode) delete detail[detailIdField];
|
|
3895
|
-
else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
|
|
3896
|
-
});
|
|
3897
|
-
return details;
|
|
3898
|
-
}
|
|
3899
|
-
//#endregion
|
|
3900
4593
|
//#region packages/crud/form/useFormSubmit.ts
|
|
3901
4594
|
/**
|
|
3902
4595
|
* Returns helpers for form serialization and HTTP submit/delete operations.
|
|
@@ -4079,30 +4772,30 @@ function mapApiViolations(violations, detailPropertyName = "items", defaultMessa
|
|
|
4079
4772
|
* the state seam, independently testable without the 900-line closure.
|
|
4080
4773
|
*/
|
|
4081
4774
|
function useFormState({ fields, onFieldDataChanged }) {
|
|
4082
|
-
const isEdit = (0, react.useRef)(false);
|
|
4083
|
-
const uploadedFiles = (0, react.useRef)([]);
|
|
4084
|
-
const existingMediaByField = (0, react.useRef)({});
|
|
4085
|
-
const upsertUploadedFile = (0, react.useCallback)((entry) => {
|
|
4775
|
+
const isEdit = (0, react$1.useRef)(false);
|
|
4776
|
+
const uploadedFiles = (0, react$1.useRef)([]);
|
|
4777
|
+
const existingMediaByField = (0, react$1.useRef)({});
|
|
4778
|
+
const upsertUploadedFile = (0, react$1.useCallback)((entry) => {
|
|
4086
4779
|
uploadedFiles.current = [...uploadedFiles.current.filter((file) => file.name !== entry.name), entry];
|
|
4087
4780
|
}, []);
|
|
4088
|
-
const [formData, setFormData] = (0, react.useState)(() => buildEmptyRow(fields));
|
|
4089
|
-
const formDataRef = (0, react.useRef)(formData);
|
|
4090
|
-
const [detailRows, setDetailRows] = (0, react.useState)([]);
|
|
4091
|
-
const detailRowsRef = (0, react.useRef)(detailRows);
|
|
4092
|
-
const [fieldState, setFieldState] = (0, react.useState)({});
|
|
4093
|
-
const [errors, setErrors] = (0, react.useState)({});
|
|
4094
|
-
const [detailErrors, setDetailErrors] = (0, react.useState)({});
|
|
4095
|
-
const prependDataRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
4096
|
-
const setNextFormData = (0, react.useCallback)((nextData) => {
|
|
4781
|
+
const [formData, setFormData] = (0, react$1.useState)(() => buildEmptyRow(fields));
|
|
4782
|
+
const formDataRef = (0, react$1.useRef)(formData);
|
|
4783
|
+
const [detailRows, setDetailRows] = (0, react$1.useState)([]);
|
|
4784
|
+
const detailRowsRef = (0, react$1.useRef)(detailRows);
|
|
4785
|
+
const [fieldState, setFieldState] = (0, react$1.useState)({});
|
|
4786
|
+
const [errors, setErrors] = (0, react$1.useState)({});
|
|
4787
|
+
const [detailErrors, setDetailErrors] = (0, react$1.useState)({});
|
|
4788
|
+
const prependDataRef = (0, react$1.useRef)(/* @__PURE__ */ new Map());
|
|
4789
|
+
const setNextFormData = (0, react$1.useCallback)((nextData) => {
|
|
4097
4790
|
formDataRef.current = nextData;
|
|
4098
4791
|
setFormData(nextData);
|
|
4099
4792
|
onFieldDataChanged?.(nextData);
|
|
4100
4793
|
}, [onFieldDataChanged]);
|
|
4101
|
-
const setNextDetailRows = (0, react.useCallback)((nextRows) => {
|
|
4794
|
+
const setNextDetailRows = (0, react$1.useCallback)((nextRows) => {
|
|
4102
4795
|
detailRowsRef.current = nextRows;
|
|
4103
4796
|
setDetailRows(nextRows);
|
|
4104
4797
|
}, []);
|
|
4105
|
-
const setFieldValue = (0, react.useCallback)((name, value) => {
|
|
4798
|
+
const setFieldValue = (0, react$1.useCallback)((name, value) => {
|
|
4106
4799
|
const field = fields.find((candidate) => candidate.name === name);
|
|
4107
4800
|
const nextData = {
|
|
4108
4801
|
...formDataRef.current,
|
|
@@ -4117,22 +4810,22 @@ function useFormState({ fields, onFieldDataChanged }) {
|
|
|
4117
4810
|
onFieldDataChanged?.(nextData);
|
|
4118
4811
|
(field?.onChange)?.(value);
|
|
4119
4812
|
}, [fields, onFieldDataChanged]);
|
|
4120
|
-
const setEditMode = (0, react.useCallback)((value) => {
|
|
4813
|
+
const setEditMode = (0, react$1.useCallback)((value) => {
|
|
4121
4814
|
isEdit.current = value;
|
|
4122
4815
|
}, []);
|
|
4123
|
-
const resetUploadSession = (0, react.useCallback)(() => {
|
|
4816
|
+
const resetUploadSession = (0, react$1.useCallback)(() => {
|
|
4124
4817
|
uploadedFiles.current = [];
|
|
4125
4818
|
}, []);
|
|
4126
|
-
const setExistingMedia = (0, react.useCallback)((media) => {
|
|
4819
|
+
const setExistingMedia = (0, react$1.useCallback)((media) => {
|
|
4127
4820
|
existingMediaByField.current = media;
|
|
4128
4821
|
}, []);
|
|
4129
|
-
const clearExistingMedia = (0, react.useCallback)((name) => {
|
|
4822
|
+
const clearExistingMedia = (0, react$1.useCallback)((name) => {
|
|
4130
4823
|
delete existingMediaByField.current[name];
|
|
4131
4824
|
}, []);
|
|
4132
|
-
const resetPrependData = (0, react.useCallback)(() => {
|
|
4825
|
+
const resetPrependData = (0, react$1.useCallback)(() => {
|
|
4133
4826
|
prependDataRef.current = /* @__PURE__ */ new Map();
|
|
4134
4827
|
}, []);
|
|
4135
|
-
const clearDetailCellError = (0, react.useCallback)((rowIndex, fieldName) => {
|
|
4828
|
+
const clearDetailCellError = (0, react$1.useCallback)((rowIndex, fieldName) => {
|
|
4136
4829
|
setDetailErrors((current) => {
|
|
4137
4830
|
const rowErrors = current[rowIndex];
|
|
4138
4831
|
if (!rowErrors?.[fieldName]) return current;
|
|
@@ -4144,10 +4837,10 @@ function useFormState({ fields, onFieldDataChanged }) {
|
|
|
4144
4837
|
return next;
|
|
4145
4838
|
});
|
|
4146
4839
|
}, []);
|
|
4147
|
-
(0, react.useEffect)(() => {
|
|
4840
|
+
(0, react$1.useEffect)(() => {
|
|
4148
4841
|
formDataRef.current = formData;
|
|
4149
4842
|
}, [formData]);
|
|
4150
|
-
(0, react.useEffect)(() => {
|
|
4843
|
+
(0, react$1.useEffect)(() => {
|
|
4151
4844
|
detailRowsRef.current = detailRows;
|
|
4152
4845
|
}, [detailRows]);
|
|
4153
4846
|
return {
|
|
@@ -5288,7 +5981,7 @@ function fromOperations(supportedOperations) {
|
|
|
5288
5981
|
*/
|
|
5289
5982
|
function usePermissions(resource, supportedOperations = []) {
|
|
5290
5983
|
const opsKey = supportedOperations.slice().sort().join(",");
|
|
5291
|
-
return (0, react.useMemo)(() => {
|
|
5984
|
+
return (0, react$1.useMemo)(() => {
|
|
5292
5985
|
const p = resource.permissions;
|
|
5293
5986
|
const inferred = fromOperations(supportedOperations);
|
|
5294
5987
|
function resolveWithInferred(permValue, inferredValue, platformDefault) {
|
|
@@ -5313,15 +6006,15 @@ function usePermissions(resource, supportedOperations = []) {
|
|
|
5313
6006
|
//#endregion
|
|
5314
6007
|
//#region packages/crud/crud/useSelectionState.ts
|
|
5315
6008
|
function useSelectionState(identityField) {
|
|
5316
|
-
const [selectedIds, setSelectedIds] = (0, react.useState)([]);
|
|
5317
|
-
const onSelectionChanged = (0, react.useCallback)((selectedRows) => {
|
|
6009
|
+
const [selectedIds, setSelectedIds] = (0, react$1.useState)([]);
|
|
6010
|
+
const onSelectionChanged = (0, react$1.useCallback)((selectedRows) => {
|
|
5318
6011
|
setSelectedIds(selectedRows.map((row) => {
|
|
5319
6012
|
const val = row[identityField];
|
|
5320
6013
|
if (typeof val === "string" || typeof val === "number") return val;
|
|
5321
6014
|
return String(val);
|
|
5322
6015
|
}));
|
|
5323
6016
|
}, [identityField]);
|
|
5324
|
-
const clearSelection = (0, react.useCallback)(() => {
|
|
6017
|
+
const clearSelection = (0, react$1.useCallback)(() => {
|
|
5325
6018
|
setSelectedIds([]);
|
|
5326
6019
|
}, []);
|
|
5327
6020
|
return {
|
|
@@ -5352,7 +6045,7 @@ function resolveVisibleColumns(resource, activeKey) {
|
|
|
5352
6045
|
return resource.columnPresets.find((p) => p.key === activeKey)?.columns ?? null;
|
|
5353
6046
|
}
|
|
5354
6047
|
function useColumnPreset(resource) {
|
|
5355
|
-
const [activePreset, setActivePresetState] = (0, react.useState)(() => {
|
|
6048
|
+
const [activePreset, setActivePresetState] = (0, react$1.useState)(() => {
|
|
5356
6049
|
if (!resource.columnPresets?.length) return null;
|
|
5357
6050
|
const stored = readFromStorage(resource.id);
|
|
5358
6051
|
if (stored && resource.columnPresets.some((p) => p.key === stored)) return stored;
|
|
@@ -5360,7 +6053,7 @@ function useColumnPreset(resource) {
|
|
|
5360
6053
|
});
|
|
5361
6054
|
return {
|
|
5362
6055
|
activePreset,
|
|
5363
|
-
setPreset: (0, react.useCallback)((key) => {
|
|
6056
|
+
setPreset: (0, react$1.useCallback)((key) => {
|
|
5364
6057
|
writeToStorage(resource.id, key);
|
|
5365
6058
|
setActivePresetState(key);
|
|
5366
6059
|
}, [resource.id]),
|
|
@@ -5681,19 +6374,19 @@ function buildFields(items) {
|
|
|
5681
6374
|
//#endregion
|
|
5682
6375
|
//#region packages/crud/crud/useCrudPage.ts
|
|
5683
6376
|
function useCrudPage(resource, externalFormRef) {
|
|
5684
|
-
const resolvedResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
|
|
6377
|
+
const resolvedResource = (0, react$1.useMemo)(() => resolveCrudResource(resource), [resource]);
|
|
5685
6378
|
const events = resolvedResource.events;
|
|
5686
|
-
const _internalFormRef = (0, react.useRef)(null);
|
|
6379
|
+
const _internalFormRef = (0, react$1.useRef)(null);
|
|
5687
6380
|
const formRef = externalFormRef ?? _internalFormRef;
|
|
5688
|
-
const fields = (0, react.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
|
|
6381
|
+
const fields = (0, react$1.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
|
|
5689
6382
|
return {
|
|
5690
6383
|
events,
|
|
5691
6384
|
resource: resolvedResource,
|
|
5692
6385
|
fields,
|
|
5693
|
-
formFields: (0, react.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
|
|
6386
|
+
formFields: (0, react$1.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
|
|
5694
6387
|
formRef,
|
|
5695
6388
|
permissions: usePermissions(resolvedResource, resolvedResource._supportedOperations ?? []),
|
|
5696
|
-
selectionState: useSelectionState((0, react.useMemo)(() => {
|
|
6389
|
+
selectionState: useSelectionState((0, react$1.useMemo)(() => {
|
|
5697
6390
|
return fields.find((f) => f.isIdentity)?.name ?? "id";
|
|
5698
6391
|
}, [fields])),
|
|
5699
6392
|
presetState: useColumnPreset(resolvedResource)
|
|
@@ -6052,7 +6745,7 @@ function useDialogStoreContext() {
|
|
|
6052
6745
|
function useCrudDialogStore(resourceId) {
|
|
6053
6746
|
const { state, dispatch } = useDialogStoreContext();
|
|
6054
6747
|
const dialogState = state[resourceId] ?? initialDialogState();
|
|
6055
|
-
const openDialog = (0, react.useCallback)((mode, rowData = null) => {
|
|
6748
|
+
const openDialog = (0, react$1.useCallback)((mode, rowData = null) => {
|
|
6056
6749
|
dispatch({
|
|
6057
6750
|
type: "OPEN",
|
|
6058
6751
|
resourceId,
|
|
@@ -6060,7 +6753,7 @@ function useCrudDialogStore(resourceId) {
|
|
|
6060
6753
|
rowData
|
|
6061
6754
|
});
|
|
6062
6755
|
}, [dispatch, resourceId]);
|
|
6063
|
-
const closeDialog = (0, react.useCallback)(() => {
|
|
6756
|
+
const closeDialog = (0, react$1.useCallback)(() => {
|
|
6064
6757
|
dispatch({
|
|
6065
6758
|
type: "CLOSE",
|
|
6066
6759
|
resourceId
|
|
@@ -6506,6 +7199,8 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
|
|
|
6506
7199
|
selectionMode: hasMultipleSelection ? "multiple" : "single",
|
|
6507
7200
|
onSelectionChanged: handleSelectionChanged,
|
|
6508
7201
|
editMode: resolvedResource.editMode,
|
|
7202
|
+
inlineEditToolbar: resolvedResource.inlineEditToolbar,
|
|
7203
|
+
inlineRowActions: resolvedResource.inlineRowActions,
|
|
6509
7204
|
visibleColumns: presetState.visibleColumns,
|
|
6510
7205
|
beforeToolbar: renderPresetSelector,
|
|
6511
7206
|
aboveGrid: aboveGridContent,
|
|
@@ -6659,7 +7354,7 @@ function useRouting(routing) {
|
|
|
6659
7354
|
return {
|
|
6660
7355
|
initialRecordId,
|
|
6661
7356
|
initialIsNew,
|
|
6662
|
-
initialFilters: (0, react.useMemo)(() => {
|
|
7357
|
+
initialFilters: (0, react$1.useMemo)(() => {
|
|
6663
7358
|
if (!syncFiltersToUrl) return NO_FILTERS;
|
|
6664
7359
|
const filters = {};
|
|
6665
7360
|
searchParams.forEach((value, key) => {
|
|
@@ -6667,7 +7362,7 @@ function useRouting(routing) {
|
|
|
6667
7362
|
});
|
|
6668
7363
|
return filters;
|
|
6669
7364
|
}, [syncFiltersToUrl, searchParams]),
|
|
6670
|
-
syncFilters: (0, react.useCallback)((filters) => {
|
|
7365
|
+
syncFilters: (0, react$1.useCallback)((filters) => {
|
|
6671
7366
|
if (!routing?.syncFiltersToUrl) return;
|
|
6672
7367
|
const next = new URLSearchParams();
|
|
6673
7368
|
Object.entries(filters).forEach(([key, value]) => {
|
|
@@ -6778,23 +7473,23 @@ function crudReducer(state, action) {
|
|
|
6778
7473
|
*/
|
|
6779
7474
|
function useSmartCrudOperation(events, routingState) {
|
|
6780
7475
|
const [on] = (0, _nubitio_core.useEvents)();
|
|
6781
|
-
const [{ activeOperation, formData }, dispatchState] = (0, react.useReducer)(crudReducer, routingState, stateFromRouting);
|
|
6782
|
-
const handleFormDataChange = (0, react.useCallback)((data) => {
|
|
7476
|
+
const [{ activeOperation, formData }, dispatchState] = (0, react$1.useReducer)(crudReducer, routingState, stateFromRouting);
|
|
7477
|
+
const handleFormDataChange = (0, react$1.useCallback)((data) => {
|
|
6783
7478
|
dispatchState({
|
|
6784
7479
|
type: "set-form-data",
|
|
6785
7480
|
data
|
|
6786
7481
|
});
|
|
6787
7482
|
}, []);
|
|
6788
|
-
const startCreate = (0, react.useCallback)(() => {
|
|
7483
|
+
const startCreate = (0, react$1.useCallback)(() => {
|
|
6789
7484
|
dispatchState({ type: "create" });
|
|
6790
7485
|
}, []);
|
|
6791
|
-
const startEdit = (0, react.useCallback)(() => {
|
|
7486
|
+
const startEdit = (0, react$1.useCallback)(() => {
|
|
6792
7487
|
dispatchState({ type: "edit" });
|
|
6793
7488
|
}, []);
|
|
6794
|
-
const resetOperation = (0, react.useCallback)(() => {
|
|
7489
|
+
const resetOperation = (0, react$1.useCallback)(() => {
|
|
6795
7490
|
dispatchState({ type: "reset" });
|
|
6796
7491
|
}, []);
|
|
6797
|
-
(0, react.useEffect)(() => {
|
|
7492
|
+
(0, react$1.useEffect)(() => {
|
|
6798
7493
|
dispatchState({
|
|
6799
7494
|
type: "sync-routing",
|
|
6800
7495
|
routingState: {
|
|
@@ -6803,7 +7498,7 @@ function useSmartCrudOperation(events, routingState) {
|
|
|
6803
7498
|
}
|
|
6804
7499
|
});
|
|
6805
7500
|
}, [routingState.initialIsNew, routingState.initialRecordId]);
|
|
6806
|
-
(0, react.useEffect)(() => {
|
|
7501
|
+
(0, react$1.useEffect)(() => {
|
|
6807
7502
|
const subscriptions = [];
|
|
6808
7503
|
if (events?.ADD) subscriptions.push(on(events.ADD, () => {
|
|
6809
7504
|
dispatchState({ type: "create" });
|
|
@@ -6849,7 +7544,7 @@ function useSmartCrudOperation(events, routingState) {
|
|
|
6849
7544
|
* so grids and forms can resolve row keys consistently.
|
|
6850
7545
|
*/
|
|
6851
7546
|
function useFieldPermissions(fields, userRoles) {
|
|
6852
|
-
return (0, react.useMemo)(() => {
|
|
7547
|
+
return (0, react$1.useMemo)(() => {
|
|
6853
7548
|
return fields.reduce((acc, field) => {
|
|
6854
7549
|
if (field.isIdentity) {
|
|
6855
7550
|
acc.push(field);
|
|
@@ -6919,7 +7614,7 @@ function evaluateConditionalRuleState(field, formData) {
|
|
|
6919
7614
|
* are returned. Empty objects `{}` are treated as valid create-form state.
|
|
6920
7615
|
*/
|
|
6921
7616
|
function useConditionalRules(fields, formData) {
|
|
6922
|
-
return (0, react.useMemo)(() => {
|
|
7617
|
+
return (0, react$1.useMemo)(() => {
|
|
6923
7618
|
return fields.map((field) => evaluateConditionalRuleState(field, formData));
|
|
6924
7619
|
}, [fields, formData]);
|
|
6925
7620
|
}
|
|
@@ -6954,9 +7649,9 @@ function useDependsOn(fields, formData) {
|
|
|
6954
7649
|
url: field.url ?? null,
|
|
6955
7650
|
values: (field.dependsOn ?? []).map((dep) => formData[dep])
|
|
6956
7651
|
}));
|
|
6957
|
-
const isMounted = (0, react.useRef)(false);
|
|
6958
|
-
const previousEntriesRef = (0, react.useRef)(null);
|
|
6959
|
-
(0, react.useEffect)(() => {
|
|
7652
|
+
const isMounted = (0, react$1.useRef)(false);
|
|
7653
|
+
const previousEntriesRef = (0, react$1.useRef)(null);
|
|
7654
|
+
(0, react$1.useEffect)(() => {
|
|
6960
7655
|
if (!isMounted.current) {
|
|
6961
7656
|
isMounted.current = true;
|
|
6962
7657
|
previousEntriesRef.current = currentEntries;
|
|
@@ -7138,7 +7833,7 @@ function applyFieldOperationSemantics(field, operation, behavior, owner = `Field
|
|
|
7138
7833
|
* from schema loading and routing concerns.
|
|
7139
7834
|
*/
|
|
7140
7835
|
function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
7141
|
-
const permissionFilteredFields = useFieldPermissions((0, react.useMemo)(() => {
|
|
7836
|
+
const permissionFilteredFields = useFieldPermissions((0, react$1.useMemo)(() => {
|
|
7142
7837
|
if (!activeOperation) return fields;
|
|
7143
7838
|
return fields.map((field) => {
|
|
7144
7839
|
const runtimeField = field;
|
|
@@ -7150,7 +7845,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
|
7150
7845
|
useDependsOn(permissionFilteredFields, formData);
|
|
7151
7846
|
return {
|
|
7152
7847
|
gridFields: permissionFilteredFields,
|
|
7153
|
-
processedFields: (0, react.useMemo)(() => {
|
|
7848
|
+
processedFields: (0, react$1.useMemo)(() => {
|
|
7154
7849
|
const stateByName = new Map(fieldStates.map((s) => [s.name, s]));
|
|
7155
7850
|
let anyChanged = false;
|
|
7156
7851
|
const merged = permissionFilteredFields.map((field) => {
|
|
@@ -7170,7 +7865,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
|
7170
7865
|
});
|
|
7171
7866
|
return anyChanged ? merged : permissionFilteredFields;
|
|
7172
7867
|
}, [permissionFilteredFields, fieldStates]),
|
|
7173
|
-
computedValues: (0, react.useMemo)(() => fieldStates.reduce((acc, state) => {
|
|
7868
|
+
computedValues: (0, react$1.useMemo)(() => fieldStates.reduce((acc, state) => {
|
|
7174
7869
|
if (state.computedValue !== void 0) acc[state.name] = state.computedValue;
|
|
7175
7870
|
return acc;
|
|
7176
7871
|
}, {}), [fieldStates])
|