@nubitio/crud 0.5.24 → 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 +692 -220
- package/dist/index.mjs +633 -164
- package/dist/style.css +137 -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,27 @@ 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
|
+
}),
|
|
2734
3285
|
options.aboveGrid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2735
3286
|
className: "nb-datagrid__above-grid",
|
|
2736
3287
|
children: options.aboveGrid
|
|
@@ -3024,16 +3575,33 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3024
3575
|
}) }) : rows.map((row, rowIndex) => {
|
|
3025
3576
|
const key = row[idField] ?? rowIndex;
|
|
3026
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);
|
|
3027
3582
|
const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
|
|
3028
3583
|
const detailUrl = options.detailUrl?.replace("{id}", String(key));
|
|
3029
3584
|
const expanded = expandedKeys.has(key);
|
|
3030
3585
|
const rowActions = buildRowActions(row);
|
|
3031
3586
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
3032
|
-
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(" "),
|
|
3033
3594
|
tabIndex: 0,
|
|
3034
3595
|
"aria-selected": selected,
|
|
3035
|
-
onClick: () =>
|
|
3596
|
+
onClick: () => {
|
|
3597
|
+
if (!editing) selectRow(row);
|
|
3598
|
+
},
|
|
3036
3599
|
onDoubleClick: () => {
|
|
3600
|
+
if (editing) return;
|
|
3601
|
+
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3602
|
+
inlineEdit.startEdit(row);
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3037
3605
|
if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
|
|
3038
3606
|
if (options.onEdit) options.onEdit(row);
|
|
3039
3607
|
else emit(options.events.EDIT, { row });
|
|
@@ -3042,10 +3610,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3042
3610
|
if (options.allowView && options.onView) options.onView(row);
|
|
3043
3611
|
},
|
|
3044
3612
|
onKeyDown: (event) => {
|
|
3045
|
-
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);
|
|
3046
3617
|
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);
|
|
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);
|
|
3049
3620
|
else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
3050
3621
|
event.preventDefault();
|
|
3051
3622
|
const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
|
|
@@ -3090,9 +3661,29 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3090
3661
|
})
|
|
3091
3662
|
}),
|
|
3092
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);
|
|
3093
3684
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3094
3685
|
style: {
|
|
3095
|
-
width
|
|
3686
|
+
width,
|
|
3096
3687
|
textAlign: field.align
|
|
3097
3688
|
},
|
|
3098
3689
|
title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
|
|
@@ -3102,7 +3693,32 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3102
3693
|
hasRowActions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3103
3694
|
className: "nb-datagrid__actions-cell",
|
|
3104
3695
|
onClick: (e) => e.stopPropagation(),
|
|
3105
|
-
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", {
|
|
3106
3722
|
className: "nb-datagrid__row-actions",
|
|
3107
3723
|
children: rowActions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3108
3724
|
icon: "ph ph-dots-three-vertical",
|
|
@@ -3452,97 +4068,6 @@ const FORM_EVENTS = {
|
|
|
3452
4068
|
};
|
|
3453
4069
|
const FORM_ERRORS_EVENT = "form-errors";
|
|
3454
4070
|
//#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
4071
|
//#region packages/crud/form/FormDataTransform.ts
|
|
3547
4072
|
function upsertPrependData(store, field, item) {
|
|
3548
4073
|
const existing = store.get(field.name);
|
|
@@ -3844,59 +4369,6 @@ function buildFieldColSpanContext(options) {
|
|
|
3844
4369
|
};
|
|
3845
4370
|
}
|
|
3846
4371
|
//#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
4372
|
//#region packages/crud/form/useFormSubmit.ts
|
|
3901
4373
|
/**
|
|
3902
4374
|
* Returns helpers for form serialization and HTTP submit/delete operations.
|
|
@@ -4079,30 +4551,30 @@ function mapApiViolations(violations, detailPropertyName = "items", defaultMessa
|
|
|
4079
4551
|
* the state seam, independently testable without the 900-line closure.
|
|
4080
4552
|
*/
|
|
4081
4553
|
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) => {
|
|
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) => {
|
|
4086
4558
|
uploadedFiles.current = [...uploadedFiles.current.filter((file) => file.name !== entry.name), entry];
|
|
4087
4559
|
}, []);
|
|
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) => {
|
|
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) => {
|
|
4097
4569
|
formDataRef.current = nextData;
|
|
4098
4570
|
setFormData(nextData);
|
|
4099
4571
|
onFieldDataChanged?.(nextData);
|
|
4100
4572
|
}, [onFieldDataChanged]);
|
|
4101
|
-
const setNextDetailRows = (0, react.useCallback)((nextRows) => {
|
|
4573
|
+
const setNextDetailRows = (0, react$1.useCallback)((nextRows) => {
|
|
4102
4574
|
detailRowsRef.current = nextRows;
|
|
4103
4575
|
setDetailRows(nextRows);
|
|
4104
4576
|
}, []);
|
|
4105
|
-
const setFieldValue = (0, react.useCallback)((name, value) => {
|
|
4577
|
+
const setFieldValue = (0, react$1.useCallback)((name, value) => {
|
|
4106
4578
|
const field = fields.find((candidate) => candidate.name === name);
|
|
4107
4579
|
const nextData = {
|
|
4108
4580
|
...formDataRef.current,
|
|
@@ -4117,22 +4589,22 @@ function useFormState({ fields, onFieldDataChanged }) {
|
|
|
4117
4589
|
onFieldDataChanged?.(nextData);
|
|
4118
4590
|
(field?.onChange)?.(value);
|
|
4119
4591
|
}, [fields, onFieldDataChanged]);
|
|
4120
|
-
const setEditMode = (0, react.useCallback)((value) => {
|
|
4592
|
+
const setEditMode = (0, react$1.useCallback)((value) => {
|
|
4121
4593
|
isEdit.current = value;
|
|
4122
4594
|
}, []);
|
|
4123
|
-
const resetUploadSession = (0, react.useCallback)(() => {
|
|
4595
|
+
const resetUploadSession = (0, react$1.useCallback)(() => {
|
|
4124
4596
|
uploadedFiles.current = [];
|
|
4125
4597
|
}, []);
|
|
4126
|
-
const setExistingMedia = (0, react.useCallback)((media) => {
|
|
4598
|
+
const setExistingMedia = (0, react$1.useCallback)((media) => {
|
|
4127
4599
|
existingMediaByField.current = media;
|
|
4128
4600
|
}, []);
|
|
4129
|
-
const clearExistingMedia = (0, react.useCallback)((name) => {
|
|
4601
|
+
const clearExistingMedia = (0, react$1.useCallback)((name) => {
|
|
4130
4602
|
delete existingMediaByField.current[name];
|
|
4131
4603
|
}, []);
|
|
4132
|
-
const resetPrependData = (0, react.useCallback)(() => {
|
|
4604
|
+
const resetPrependData = (0, react$1.useCallback)(() => {
|
|
4133
4605
|
prependDataRef.current = /* @__PURE__ */ new Map();
|
|
4134
4606
|
}, []);
|
|
4135
|
-
const clearDetailCellError = (0, react.useCallback)((rowIndex, fieldName) => {
|
|
4607
|
+
const clearDetailCellError = (0, react$1.useCallback)((rowIndex, fieldName) => {
|
|
4136
4608
|
setDetailErrors((current) => {
|
|
4137
4609
|
const rowErrors = current[rowIndex];
|
|
4138
4610
|
if (!rowErrors?.[fieldName]) return current;
|
|
@@ -4144,10 +4616,10 @@ function useFormState({ fields, onFieldDataChanged }) {
|
|
|
4144
4616
|
return next;
|
|
4145
4617
|
});
|
|
4146
4618
|
}, []);
|
|
4147
|
-
(0, react.useEffect)(() => {
|
|
4619
|
+
(0, react$1.useEffect)(() => {
|
|
4148
4620
|
formDataRef.current = formData;
|
|
4149
4621
|
}, [formData]);
|
|
4150
|
-
(0, react.useEffect)(() => {
|
|
4622
|
+
(0, react$1.useEffect)(() => {
|
|
4151
4623
|
detailRowsRef.current = detailRows;
|
|
4152
4624
|
}, [detailRows]);
|
|
4153
4625
|
return {
|
|
@@ -5288,7 +5760,7 @@ function fromOperations(supportedOperations) {
|
|
|
5288
5760
|
*/
|
|
5289
5761
|
function usePermissions(resource, supportedOperations = []) {
|
|
5290
5762
|
const opsKey = supportedOperations.slice().sort().join(",");
|
|
5291
|
-
return (0, react.useMemo)(() => {
|
|
5763
|
+
return (0, react$1.useMemo)(() => {
|
|
5292
5764
|
const p = resource.permissions;
|
|
5293
5765
|
const inferred = fromOperations(supportedOperations);
|
|
5294
5766
|
function resolveWithInferred(permValue, inferredValue, platformDefault) {
|
|
@@ -5313,15 +5785,15 @@ function usePermissions(resource, supportedOperations = []) {
|
|
|
5313
5785
|
//#endregion
|
|
5314
5786
|
//#region packages/crud/crud/useSelectionState.ts
|
|
5315
5787
|
function useSelectionState(identityField) {
|
|
5316
|
-
const [selectedIds, setSelectedIds] = (0, react.useState)([]);
|
|
5317
|
-
const onSelectionChanged = (0, react.useCallback)((selectedRows) => {
|
|
5788
|
+
const [selectedIds, setSelectedIds] = (0, react$1.useState)([]);
|
|
5789
|
+
const onSelectionChanged = (0, react$1.useCallback)((selectedRows) => {
|
|
5318
5790
|
setSelectedIds(selectedRows.map((row) => {
|
|
5319
5791
|
const val = row[identityField];
|
|
5320
5792
|
if (typeof val === "string" || typeof val === "number") return val;
|
|
5321
5793
|
return String(val);
|
|
5322
5794
|
}));
|
|
5323
5795
|
}, [identityField]);
|
|
5324
|
-
const clearSelection = (0, react.useCallback)(() => {
|
|
5796
|
+
const clearSelection = (0, react$1.useCallback)(() => {
|
|
5325
5797
|
setSelectedIds([]);
|
|
5326
5798
|
}, []);
|
|
5327
5799
|
return {
|
|
@@ -5352,7 +5824,7 @@ function resolveVisibleColumns(resource, activeKey) {
|
|
|
5352
5824
|
return resource.columnPresets.find((p) => p.key === activeKey)?.columns ?? null;
|
|
5353
5825
|
}
|
|
5354
5826
|
function useColumnPreset(resource) {
|
|
5355
|
-
const [activePreset, setActivePresetState] = (0, react.useState)(() => {
|
|
5827
|
+
const [activePreset, setActivePresetState] = (0, react$1.useState)(() => {
|
|
5356
5828
|
if (!resource.columnPresets?.length) return null;
|
|
5357
5829
|
const stored = readFromStorage(resource.id);
|
|
5358
5830
|
if (stored && resource.columnPresets.some((p) => p.key === stored)) return stored;
|
|
@@ -5360,7 +5832,7 @@ function useColumnPreset(resource) {
|
|
|
5360
5832
|
});
|
|
5361
5833
|
return {
|
|
5362
5834
|
activePreset,
|
|
5363
|
-
setPreset: (0, react.useCallback)((key) => {
|
|
5835
|
+
setPreset: (0, react$1.useCallback)((key) => {
|
|
5364
5836
|
writeToStorage(resource.id, key);
|
|
5365
5837
|
setActivePresetState(key);
|
|
5366
5838
|
}, [resource.id]),
|
|
@@ -5681,19 +6153,19 @@ function buildFields(items) {
|
|
|
5681
6153
|
//#endregion
|
|
5682
6154
|
//#region packages/crud/crud/useCrudPage.ts
|
|
5683
6155
|
function useCrudPage(resource, externalFormRef) {
|
|
5684
|
-
const resolvedResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
|
|
6156
|
+
const resolvedResource = (0, react$1.useMemo)(() => resolveCrudResource(resource), [resource]);
|
|
5685
6157
|
const events = resolvedResource.events;
|
|
5686
|
-
const _internalFormRef = (0, react.useRef)(null);
|
|
6158
|
+
const _internalFormRef = (0, react$1.useRef)(null);
|
|
5687
6159
|
const formRef = externalFormRef ?? _internalFormRef;
|
|
5688
|
-
const fields = (0, react.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
|
|
6160
|
+
const fields = (0, react$1.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
|
|
5689
6161
|
return {
|
|
5690
6162
|
events,
|
|
5691
6163
|
resource: resolvedResource,
|
|
5692
6164
|
fields,
|
|
5693
|
-
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]),
|
|
5694
6166
|
formRef,
|
|
5695
6167
|
permissions: usePermissions(resolvedResource, resolvedResource._supportedOperations ?? []),
|
|
5696
|
-
selectionState: useSelectionState((0, react.useMemo)(() => {
|
|
6168
|
+
selectionState: useSelectionState((0, react$1.useMemo)(() => {
|
|
5697
6169
|
return fields.find((f) => f.isIdentity)?.name ?? "id";
|
|
5698
6170
|
}, [fields])),
|
|
5699
6171
|
presetState: useColumnPreset(resolvedResource)
|
|
@@ -6052,7 +6524,7 @@ function useDialogStoreContext() {
|
|
|
6052
6524
|
function useCrudDialogStore(resourceId) {
|
|
6053
6525
|
const { state, dispatch } = useDialogStoreContext();
|
|
6054
6526
|
const dialogState = state[resourceId] ?? initialDialogState();
|
|
6055
|
-
const openDialog = (0, react.useCallback)((mode, rowData = null) => {
|
|
6527
|
+
const openDialog = (0, react$1.useCallback)((mode, rowData = null) => {
|
|
6056
6528
|
dispatch({
|
|
6057
6529
|
type: "OPEN",
|
|
6058
6530
|
resourceId,
|
|
@@ -6060,7 +6532,7 @@ function useCrudDialogStore(resourceId) {
|
|
|
6060
6532
|
rowData
|
|
6061
6533
|
});
|
|
6062
6534
|
}, [dispatch, resourceId]);
|
|
6063
|
-
const closeDialog = (0, react.useCallback)(() => {
|
|
6535
|
+
const closeDialog = (0, react$1.useCallback)(() => {
|
|
6064
6536
|
dispatch({
|
|
6065
6537
|
type: "CLOSE",
|
|
6066
6538
|
resourceId
|
|
@@ -6659,7 +7131,7 @@ function useRouting(routing) {
|
|
|
6659
7131
|
return {
|
|
6660
7132
|
initialRecordId,
|
|
6661
7133
|
initialIsNew,
|
|
6662
|
-
initialFilters: (0, react.useMemo)(() => {
|
|
7134
|
+
initialFilters: (0, react$1.useMemo)(() => {
|
|
6663
7135
|
if (!syncFiltersToUrl) return NO_FILTERS;
|
|
6664
7136
|
const filters = {};
|
|
6665
7137
|
searchParams.forEach((value, key) => {
|
|
@@ -6667,7 +7139,7 @@ function useRouting(routing) {
|
|
|
6667
7139
|
});
|
|
6668
7140
|
return filters;
|
|
6669
7141
|
}, [syncFiltersToUrl, searchParams]),
|
|
6670
|
-
syncFilters: (0, react.useCallback)((filters) => {
|
|
7142
|
+
syncFilters: (0, react$1.useCallback)((filters) => {
|
|
6671
7143
|
if (!routing?.syncFiltersToUrl) return;
|
|
6672
7144
|
const next = new URLSearchParams();
|
|
6673
7145
|
Object.entries(filters).forEach(([key, value]) => {
|
|
@@ -6778,23 +7250,23 @@ function crudReducer(state, action) {
|
|
|
6778
7250
|
*/
|
|
6779
7251
|
function useSmartCrudOperation(events, routingState) {
|
|
6780
7252
|
const [on] = (0, _nubitio_core.useEvents)();
|
|
6781
|
-
const [{ activeOperation, formData }, dispatchState] = (0, react.useReducer)(crudReducer, routingState, stateFromRouting);
|
|
6782
|
-
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) => {
|
|
6783
7255
|
dispatchState({
|
|
6784
7256
|
type: "set-form-data",
|
|
6785
7257
|
data
|
|
6786
7258
|
});
|
|
6787
7259
|
}, []);
|
|
6788
|
-
const startCreate = (0, react.useCallback)(() => {
|
|
7260
|
+
const startCreate = (0, react$1.useCallback)(() => {
|
|
6789
7261
|
dispatchState({ type: "create" });
|
|
6790
7262
|
}, []);
|
|
6791
|
-
const startEdit = (0, react.useCallback)(() => {
|
|
7263
|
+
const startEdit = (0, react$1.useCallback)(() => {
|
|
6792
7264
|
dispatchState({ type: "edit" });
|
|
6793
7265
|
}, []);
|
|
6794
|
-
const resetOperation = (0, react.useCallback)(() => {
|
|
7266
|
+
const resetOperation = (0, react$1.useCallback)(() => {
|
|
6795
7267
|
dispatchState({ type: "reset" });
|
|
6796
7268
|
}, []);
|
|
6797
|
-
(0, react.useEffect)(() => {
|
|
7269
|
+
(0, react$1.useEffect)(() => {
|
|
6798
7270
|
dispatchState({
|
|
6799
7271
|
type: "sync-routing",
|
|
6800
7272
|
routingState: {
|
|
@@ -6803,7 +7275,7 @@ function useSmartCrudOperation(events, routingState) {
|
|
|
6803
7275
|
}
|
|
6804
7276
|
});
|
|
6805
7277
|
}, [routingState.initialIsNew, routingState.initialRecordId]);
|
|
6806
|
-
(0, react.useEffect)(() => {
|
|
7278
|
+
(0, react$1.useEffect)(() => {
|
|
6807
7279
|
const subscriptions = [];
|
|
6808
7280
|
if (events?.ADD) subscriptions.push(on(events.ADD, () => {
|
|
6809
7281
|
dispatchState({ type: "create" });
|
|
@@ -6849,7 +7321,7 @@ function useSmartCrudOperation(events, routingState) {
|
|
|
6849
7321
|
* so grids and forms can resolve row keys consistently.
|
|
6850
7322
|
*/
|
|
6851
7323
|
function useFieldPermissions(fields, userRoles) {
|
|
6852
|
-
return (0, react.useMemo)(() => {
|
|
7324
|
+
return (0, react$1.useMemo)(() => {
|
|
6853
7325
|
return fields.reduce((acc, field) => {
|
|
6854
7326
|
if (field.isIdentity) {
|
|
6855
7327
|
acc.push(field);
|
|
@@ -6919,7 +7391,7 @@ function evaluateConditionalRuleState(field, formData) {
|
|
|
6919
7391
|
* are returned. Empty objects `{}` are treated as valid create-form state.
|
|
6920
7392
|
*/
|
|
6921
7393
|
function useConditionalRules(fields, formData) {
|
|
6922
|
-
return (0, react.useMemo)(() => {
|
|
7394
|
+
return (0, react$1.useMemo)(() => {
|
|
6923
7395
|
return fields.map((field) => evaluateConditionalRuleState(field, formData));
|
|
6924
7396
|
}, [fields, formData]);
|
|
6925
7397
|
}
|
|
@@ -6954,9 +7426,9 @@ function useDependsOn(fields, formData) {
|
|
|
6954
7426
|
url: field.url ?? null,
|
|
6955
7427
|
values: (field.dependsOn ?? []).map((dep) => formData[dep])
|
|
6956
7428
|
}));
|
|
6957
|
-
const isMounted = (0, react.useRef)(false);
|
|
6958
|
-
const previousEntriesRef = (0, react.useRef)(null);
|
|
6959
|
-
(0, react.useEffect)(() => {
|
|
7429
|
+
const isMounted = (0, react$1.useRef)(false);
|
|
7430
|
+
const previousEntriesRef = (0, react$1.useRef)(null);
|
|
7431
|
+
(0, react$1.useEffect)(() => {
|
|
6960
7432
|
if (!isMounted.current) {
|
|
6961
7433
|
isMounted.current = true;
|
|
6962
7434
|
previousEntriesRef.current = currentEntries;
|
|
@@ -7138,7 +7610,7 @@ function applyFieldOperationSemantics(field, operation, behavior, owner = `Field
|
|
|
7138
7610
|
* from schema loading and routing concerns.
|
|
7139
7611
|
*/
|
|
7140
7612
|
function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
7141
|
-
const permissionFilteredFields = useFieldPermissions((0, react.useMemo)(() => {
|
|
7613
|
+
const permissionFilteredFields = useFieldPermissions((0, react$1.useMemo)(() => {
|
|
7142
7614
|
if (!activeOperation) return fields;
|
|
7143
7615
|
return fields.map((field) => {
|
|
7144
7616
|
const runtimeField = field;
|
|
@@ -7150,7 +7622,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
|
7150
7622
|
useDependsOn(permissionFilteredFields, formData);
|
|
7151
7623
|
return {
|
|
7152
7624
|
gridFields: permissionFilteredFields,
|
|
7153
|
-
processedFields: (0, react.useMemo)(() => {
|
|
7625
|
+
processedFields: (0, react$1.useMemo)(() => {
|
|
7154
7626
|
const stateByName = new Map(fieldStates.map((s) => [s.name, s]));
|
|
7155
7627
|
let anyChanged = false;
|
|
7156
7628
|
const merged = permissionFilteredFields.map((field) => {
|
|
@@ -7170,7 +7642,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
|
|
|
7170
7642
|
});
|
|
7171
7643
|
return anyChanged ? merged : permissionFilteredFields;
|
|
7172
7644
|
}, [permissionFilteredFields, fieldStates]),
|
|
7173
|
-
computedValues: (0, react.useMemo)(() => fieldStates.reduce((acc, state) => {
|
|
7645
|
+
computedValues: (0, react$1.useMemo)(() => fieldStates.reduce((acc, state) => {
|
|
7174
7646
|
if (state.computedValue !== void 0) acc[state.name] = state.computedValue;
|
|
7175
7647
|
return acc;
|
|
7176
7648
|
}, {}), [fieldStates])
|