@nubitio/crud 0.5.23 → 0.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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, ctx) => getPrimitiveDisplay(value, ctx.yesLabel, ctx.noLabel),
1619
+ cellText: (_field, value) => stripTags(value),
1461
1620
  serializeFormValue: () => KEEP,
1462
1621
  serializeDetailValue: () => KEEP,
1463
- ControlRender: ({ field, value, commonProps, errorClass, setFieldValue }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1464
- ...commonProps,
1465
- className: `nb-form__control nb-form__textarea${errorClass}`,
1466
- value: inputValue(value),
1467
- onChange: (event) => setFieldValue(field.name, event.target.value)
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
- return getFieldTypeModule(field.type).cellText(field, value, {
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 += ACTIONS_COL_WIDTH;
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) && (options.onEdit || options.events?.EDIT) ? [{
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 ? ACTIONS_COL_WIDTH : 0);
3160
+ const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
2631
3161
  const extra = Math.max(0, containerWidth - dataTotal - fixedTotal);
2632
3162
  if (extra === 0 || dataTotal === 0) return colWidths;
2633
3163
  let distributed = 0;
@@ -2731,6 +3261,31 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2731
3261
  })
2732
3262
  ]
2733
3263
  }),
3264
+ options.editMode === "batch" && inlineEdit.draftRows.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3265
+ className: "nb-datagrid__batch-bar",
3266
+ role: "status",
3267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3268
+ className: "nb-datagrid__batch-bar-label",
3269
+ children: t("grid.inlineUnsavedRows", { count: inlineEdit.draftRows.size })
3270
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3271
+ className: "nb-datagrid__batch-bar-actions",
3272
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3273
+ type: "button",
3274
+ className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--discard",
3275
+ onClick: () => inlineEdit.discardAll(),
3276
+ children: t("grid.inlineDiscardAll")
3277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3278
+ type: "button",
3279
+ className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--save",
3280
+ onClick: () => void inlineEdit.saveAll(),
3281
+ children: t("grid.inlineSaveAll", { count: inlineEdit.draftRows.size })
3282
+ })]
3283
+ })]
3284
+ }),
3285
+ options.aboveGrid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3286
+ className: "nb-datagrid__above-grid",
3287
+ children: options.aboveGrid
3288
+ }) : null,
2734
3289
  isMobile && quickSearchField && (options.filterRow ?? true) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2735
3290
  className: "nb-datagrid__quick-search",
2736
3291
  children: [
@@ -3020,16 +3575,33 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3020
3575
  }) }) : rows.map((row, rowIndex) => {
3021
3576
  const key = row[idField] ?? rowIndex;
3022
3577
  const selected = selectedKeys.includes(key);
3578
+ const editing = inlineEdit.draftRows.has(key);
3579
+ const saving = inlineEdit.savingRows.has(key);
3580
+ const rowDraft = inlineEdit.draftRows.get(key) ?? row;
3581
+ const rowFieldErrors = inlineEdit.rowErrors.get(key);
3023
3582
  const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
3024
3583
  const detailUrl = options.detailUrl?.replace("{id}", String(key));
3025
3584
  const expanded = expandedKeys.has(key);
3026
3585
  const rowActions = buildRowActions(row);
3027
3586
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
3028
- className: `nb-datagrid__row ${expanded ? "nb-datagrid__row--expanded" : ""} ${selected ? "nb-datagrid__row--selected" : ""}`,
3587
+ className: [
3588
+ "nb-datagrid__row",
3589
+ expanded ? "nb-datagrid__row--expanded" : "",
3590
+ selected ? "nb-datagrid__row--selected" : "",
3591
+ editing ? "nb-datagrid__row--editing" : "",
3592
+ saving ? "nb-datagrid__row--saving" : ""
3593
+ ].filter(Boolean).join(" "),
3029
3594
  tabIndex: 0,
3030
3595
  "aria-selected": selected,
3031
- onClick: () => selectRow(row),
3596
+ onClick: () => {
3597
+ if (!editing) selectRow(row);
3598
+ },
3032
3599
  onDoubleClick: () => {
3600
+ if (editing) return;
3601
+ if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
3602
+ inlineEdit.startEdit(row);
3603
+ return;
3604
+ }
3033
3605
  if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
3034
3606
  if (options.onEdit) options.onEdit(row);
3035
3607
  else emit(options.events.EDIT, { row });
@@ -3038,10 +3610,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3038
3610
  if (options.allowView && options.onView) options.onView(row);
3039
3611
  },
3040
3612
  onKeyDown: (event) => {
3041
- if (event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3613
+ if (editing && event.key === "Escape") inlineEdit.cancelEdit(key);
3614
+ else if (editing && event.key === "Enter") inlineEdit.saveRow(key);
3615
+ else if (!editing && event.key === "Enter" && options.allowEdit && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
3616
+ else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3042
3617
  else emit(options.events.EDIT, { row });
3043
- else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3044
- else if (event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3618
+ else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3619
+ else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3045
3620
  else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
3046
3621
  event.preventDefault();
3047
3622
  const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
@@ -3086,9 +3661,29 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3086
3661
  })
3087
3662
  }),
3088
3663
  visibleFields.map((field, columnIndex) => {
3664
+ const width = getColumnWidth(field, resolvedColWidths);
3665
+ if (editing && canEditFieldInline(field)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3666
+ style: {
3667
+ width,
3668
+ textAlign: field.align
3669
+ },
3670
+ className: "nb-datagrid__edit-cell",
3671
+ onClick: (e) => e.stopPropagation(),
3672
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineEditCell, {
3673
+ field,
3674
+ rowKey: key,
3675
+ draft: rowDraft,
3676
+ onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
3677
+ errors: rowFieldErrors,
3678
+ disabled: saving,
3679
+ allRemoteOptions: filterRemoteOptions,
3680
+ httpClient,
3681
+ t
3682
+ })
3683
+ }, field.name);
3089
3684
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3090
3685
  style: {
3091
- width: getColumnWidth(field, resolvedColWidths),
3686
+ width,
3092
3687
  textAlign: field.align
3093
3688
  },
3094
3689
  title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
@@ -3098,7 +3693,32 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3098
3693
  hasRowActions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3099
3694
  className: "nb-datagrid__actions-cell",
3100
3695
  onClick: (e) => e.stopPropagation(),
3101
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3696
+ children: editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3697
+ className: "nb-datagrid__inline-actions",
3698
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3699
+ type: "button",
3700
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--save",
3701
+ disabled: saving,
3702
+ "aria-label": t("grid.inlineSaveRow"),
3703
+ title: t("grid.inlineSaveRow"),
3704
+ onClick: () => void inlineEdit.saveRow(key),
3705
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
3706
+ className: "ph ph-check",
3707
+ "aria-hidden": "true"
3708
+ })
3709
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3710
+ type: "button",
3711
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--cancel",
3712
+ disabled: saving,
3713
+ "aria-label": t("grid.inlineCancelRow"),
3714
+ title: t("grid.inlineCancelRow"),
3715
+ onClick: () => inlineEdit.cancelEdit(key),
3716
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
3717
+ className: "ph ph-x",
3718
+ "aria-hidden": "true"
3719
+ })
3720
+ })]
3721
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3102
3722
  className: "nb-datagrid__row-actions",
3103
3723
  children: rowActions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
3104
3724
  icon: "ph ph-dots-three-vertical",
@@ -3448,97 +4068,6 @@ const FORM_EVENTS = {
3448
4068
  };
3449
4069
  const FORM_ERRORS_EVENT = "form-errors";
3450
4070
  //#endregion
3451
- //#region packages/crud/adapter/HydraAdapter.ts
3452
- function trimTrailingSlash(value) {
3453
- return value.replace(/\/+$/, "");
3454
- }
3455
- function buildIRI(url, value) {
3456
- if (value.startsWith("/")) return value;
3457
- return `${url}/${value}`;
3458
- }
3459
- /**
3460
- * Default backend adapter for API Platform / JSON-LD + Hydra backends.
3461
- *
3462
- * Conventions assumed:
3463
- * - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
3464
- * - Entity fields are serialized as IRI strings in POST/PATCH bodies.
3465
- * - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
3466
- * - `_iri` is a synthetic alias for `@id` used internally by the engine.
3467
- */
3468
- const HydraAdapter = {
3469
- getRowId(record, idField) {
3470
- const direct = record[idField];
3471
- if (direct !== void 0 && direct !== null) return String(direct);
3472
- const iri = record["@id"] ?? record["_iri"];
3473
- if (iri !== void 0 && iri !== null) return String(iri);
3474
- return String(record["id"] ?? "");
3475
- },
3476
- buildItemUrl(baseUrl, id) {
3477
- const str = String(id);
3478
- if (str.startsWith("/")) return str;
3479
- return `${trimTrailingSlash(baseUrl)}/${str}`;
3480
- },
3481
- serializeEntityRef(field, rawValue) {
3482
- if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
3483
- if (typeof rawValue === "object") {
3484
- const entity = rawValue;
3485
- const atId = entity["@id"];
3486
- if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
3487
- const idValue = entity["id"];
3488
- const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
3489
- return buildIRI(field.url ?? "", resolvedId);
3490
- }
3491
- return buildIRI(field.url ?? "", String(rawValue));
3492
- },
3493
- normalizeEntityValue(rawValue, field) {
3494
- if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
3495
- if (typeof rawValue === "object" && rawValue !== null) {
3496
- const entity = rawValue;
3497
- const atId = entity["@id"];
3498
- if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
3499
- const directValue = entity[field.valueField];
3500
- if (directValue !== void 0 && directValue !== null) return directValue;
3501
- }
3502
- return rawValue;
3503
- },
3504
- getEntityOptionKey(item, field) {
3505
- if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
3506
- return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
3507
- },
3508
- parseListResponse(response) {
3509
- const r = response;
3510
- const member = r["hydra:member"];
3511
- if (Array.isArray(member)) return {
3512
- items: member,
3513
- total: Number(r["hydra:totalItems"] ?? member.length)
3514
- };
3515
- if (Array.isArray(response)) return {
3516
- items: response,
3517
- total: response.length
3518
- };
3519
- return {
3520
- items: [],
3521
- total: 0
3522
- };
3523
- },
3524
- synthesizeEntityKey(field, entityValue) {
3525
- if (!field.url) return void 0;
3526
- const base = trimTrailingSlash(field.url);
3527
- const directId = entityValue["id"];
3528
- if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
3529
- const directValue = entityValue[field.valueField];
3530
- if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
3531
- for (const key of [
3532
- "code",
3533
- "uuid",
3534
- "slug"
3535
- ]) {
3536
- const candidate = entityValue[key];
3537
- if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
3538
- }
3539
- }
3540
- };
3541
- //#endregion
3542
4071
  //#region packages/crud/form/FormDataTransform.ts
3543
4072
  function upsertPrependData(store, field, item) {
3544
4073
  const existing = store.get(field.name);
@@ -3840,59 +4369,6 @@ function buildFieldColSpanContext(options) {
3840
4369
  };
3841
4370
  }
3842
4371
  //#endregion
3843
- //#region packages/crud/form/serializeFormData.ts
3844
- function applySerializedValue(formData, field, result) {
3845
- if (result.kind === "set") formData[field.name] = result.value;
3846
- else if (result.kind === "omit") delete formData[field.name];
3847
- }
3848
- /**
3849
- * Pure serialization of form data before HTTP submission.
3850
- *
3851
- * Applies uploaded file references and computed fields, then delegates the
3852
- * per-type wire format (entity refs, business dates, numeric coercion, file
3853
- * handling, NONE stripping) to each field's Field-Type module.
3854
- *
3855
- * This function is extracted from useFormSubmit for testability.
3856
- */
3857
- function serializeFormFields(rawData, fields, ctx) {
3858
- const formData = { ...rawData };
3859
- ctx.uploadedFiles.forEach((file) => {
3860
- formData[file.name] = file.iri;
3861
- });
3862
- fields.forEach((field) => {
3863
- if (!field.computed) return;
3864
- const computedValue = field.computed(formData);
3865
- if (computedValue !== void 0) formData[field.name] = computedValue;
3866
- });
3867
- const moduleCtx = {
3868
- adapter: ctx.adapter ?? HydraAdapter,
3869
- format: ctx.format,
3870
- getFieldValue: ctx.getFieldValue
3871
- };
3872
- fields.forEach((field) => {
3873
- applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
3874
- });
3875
- return formData;
3876
- }
3877
- /**
3878
- * Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
3879
- * Pure function — operates on an array of row records.
3880
- */
3881
- function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
3882
- const details = JSON.parse(JSON.stringify(rows));
3883
- detailFields.forEach((field) => {
3884
- const typeModule = getFieldTypeModule(field.type);
3885
- details.forEach((detail) => {
3886
- applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
3887
- });
3888
- });
3889
- details.forEach((detail) => {
3890
- if (!isEditMode) delete detail[detailIdField];
3891
- else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
3892
- });
3893
- return details;
3894
- }
3895
- //#endregion
3896
4372
  //#region packages/crud/form/useFormSubmit.ts
3897
4373
  /**
3898
4374
  * Returns helpers for form serialization and HTTP submit/delete operations.
@@ -4075,30 +4551,30 @@ function mapApiViolations(violations, detailPropertyName = "items", defaultMessa
4075
4551
  * the state seam, independently testable without the 900-line closure.
4076
4552
  */
4077
4553
  function useFormState({ fields, onFieldDataChanged }) {
4078
- const isEdit = (0, react.useRef)(false);
4079
- const uploadedFiles = (0, react.useRef)([]);
4080
- const existingMediaByField = (0, react.useRef)({});
4081
- const upsertUploadedFile = (0, react.useCallback)((entry) => {
4554
+ const isEdit = (0, react$1.useRef)(false);
4555
+ const uploadedFiles = (0, react$1.useRef)([]);
4556
+ const existingMediaByField = (0, react$1.useRef)({});
4557
+ const upsertUploadedFile = (0, react$1.useCallback)((entry) => {
4082
4558
  uploadedFiles.current = [...uploadedFiles.current.filter((file) => file.name !== entry.name), entry];
4083
4559
  }, []);
4084
- const [formData, setFormData] = (0, react.useState)(() => buildEmptyRow(fields));
4085
- const formDataRef = (0, react.useRef)(formData);
4086
- const [detailRows, setDetailRows] = (0, react.useState)([]);
4087
- const detailRowsRef = (0, react.useRef)(detailRows);
4088
- const [fieldState, setFieldState] = (0, react.useState)({});
4089
- const [errors, setErrors] = (0, react.useState)({});
4090
- const [detailErrors, setDetailErrors] = (0, react.useState)({});
4091
- const prependDataRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4092
- const setNextFormData = (0, react.useCallback)((nextData) => {
4560
+ const [formData, setFormData] = (0, react$1.useState)(() => buildEmptyRow(fields));
4561
+ const formDataRef = (0, react$1.useRef)(formData);
4562
+ const [detailRows, setDetailRows] = (0, react$1.useState)([]);
4563
+ const detailRowsRef = (0, react$1.useRef)(detailRows);
4564
+ const [fieldState, setFieldState] = (0, react$1.useState)({});
4565
+ const [errors, setErrors] = (0, react$1.useState)({});
4566
+ const [detailErrors, setDetailErrors] = (0, react$1.useState)({});
4567
+ const prependDataRef = (0, react$1.useRef)(/* @__PURE__ */ new Map());
4568
+ const setNextFormData = (0, react$1.useCallback)((nextData) => {
4093
4569
  formDataRef.current = nextData;
4094
4570
  setFormData(nextData);
4095
4571
  onFieldDataChanged?.(nextData);
4096
4572
  }, [onFieldDataChanged]);
4097
- const setNextDetailRows = (0, react.useCallback)((nextRows) => {
4573
+ const setNextDetailRows = (0, react$1.useCallback)((nextRows) => {
4098
4574
  detailRowsRef.current = nextRows;
4099
4575
  setDetailRows(nextRows);
4100
4576
  }, []);
4101
- const setFieldValue = (0, react.useCallback)((name, value) => {
4577
+ const setFieldValue = (0, react$1.useCallback)((name, value) => {
4102
4578
  const field = fields.find((candidate) => candidate.name === name);
4103
4579
  const nextData = {
4104
4580
  ...formDataRef.current,
@@ -4113,22 +4589,22 @@ function useFormState({ fields, onFieldDataChanged }) {
4113
4589
  onFieldDataChanged?.(nextData);
4114
4590
  (field?.onChange)?.(value);
4115
4591
  }, [fields, onFieldDataChanged]);
4116
- const setEditMode = (0, react.useCallback)((value) => {
4592
+ const setEditMode = (0, react$1.useCallback)((value) => {
4117
4593
  isEdit.current = value;
4118
4594
  }, []);
4119
- const resetUploadSession = (0, react.useCallback)(() => {
4595
+ const resetUploadSession = (0, react$1.useCallback)(() => {
4120
4596
  uploadedFiles.current = [];
4121
4597
  }, []);
4122
- const setExistingMedia = (0, react.useCallback)((media) => {
4598
+ const setExistingMedia = (0, react$1.useCallback)((media) => {
4123
4599
  existingMediaByField.current = media;
4124
4600
  }, []);
4125
- const clearExistingMedia = (0, react.useCallback)((name) => {
4601
+ const clearExistingMedia = (0, react$1.useCallback)((name) => {
4126
4602
  delete existingMediaByField.current[name];
4127
4603
  }, []);
4128
- const resetPrependData = (0, react.useCallback)(() => {
4604
+ const resetPrependData = (0, react$1.useCallback)(() => {
4129
4605
  prependDataRef.current = /* @__PURE__ */ new Map();
4130
4606
  }, []);
4131
- const clearDetailCellError = (0, react.useCallback)((rowIndex, fieldName) => {
4607
+ const clearDetailCellError = (0, react$1.useCallback)((rowIndex, fieldName) => {
4132
4608
  setDetailErrors((current) => {
4133
4609
  const rowErrors = current[rowIndex];
4134
4610
  if (!rowErrors?.[fieldName]) return current;
@@ -4140,10 +4616,10 @@ function useFormState({ fields, onFieldDataChanged }) {
4140
4616
  return next;
4141
4617
  });
4142
4618
  }, []);
4143
- (0, react.useEffect)(() => {
4619
+ (0, react$1.useEffect)(() => {
4144
4620
  formDataRef.current = formData;
4145
4621
  }, [formData]);
4146
- (0, react.useEffect)(() => {
4622
+ (0, react$1.useEffect)(() => {
4147
4623
  detailRowsRef.current = detailRows;
4148
4624
  }, [detailRows]);
4149
4625
  return {
@@ -5284,7 +5760,7 @@ function fromOperations(supportedOperations) {
5284
5760
  */
5285
5761
  function usePermissions(resource, supportedOperations = []) {
5286
5762
  const opsKey = supportedOperations.slice().sort().join(",");
5287
- return (0, react.useMemo)(() => {
5763
+ return (0, react$1.useMemo)(() => {
5288
5764
  const p = resource.permissions;
5289
5765
  const inferred = fromOperations(supportedOperations);
5290
5766
  function resolveWithInferred(permValue, inferredValue, platformDefault) {
@@ -5300,20 +5776,24 @@ function usePermissions(resource, supportedOperations = []) {
5300
5776
  canExport: resolve(p?.canExport, false),
5301
5777
  canBulkDelete: resolve(p?.canBulkDelete, false)
5302
5778
  };
5303
- }, [resource.id, opsKey]);
5779
+ }, [
5780
+ resource.id,
5781
+ opsKey,
5782
+ resource.permissions
5783
+ ]);
5304
5784
  }
5305
5785
  //#endregion
5306
5786
  //#region packages/crud/crud/useSelectionState.ts
5307
5787
  function useSelectionState(identityField) {
5308
- const [selectedIds, setSelectedIds] = (0, react.useState)([]);
5309
- const onSelectionChanged = (0, react.useCallback)((selectedRows) => {
5788
+ const [selectedIds, setSelectedIds] = (0, react$1.useState)([]);
5789
+ const onSelectionChanged = (0, react$1.useCallback)((selectedRows) => {
5310
5790
  setSelectedIds(selectedRows.map((row) => {
5311
5791
  const val = row[identityField];
5312
5792
  if (typeof val === "string" || typeof val === "number") return val;
5313
5793
  return String(val);
5314
5794
  }));
5315
5795
  }, [identityField]);
5316
- const clearSelection = (0, react.useCallback)(() => {
5796
+ const clearSelection = (0, react$1.useCallback)(() => {
5317
5797
  setSelectedIds([]);
5318
5798
  }, []);
5319
5799
  return {
@@ -5344,7 +5824,7 @@ function resolveVisibleColumns(resource, activeKey) {
5344
5824
  return resource.columnPresets.find((p) => p.key === activeKey)?.columns ?? null;
5345
5825
  }
5346
5826
  function useColumnPreset(resource) {
5347
- const [activePreset, setActivePresetState] = (0, react.useState)(() => {
5827
+ const [activePreset, setActivePresetState] = (0, react$1.useState)(() => {
5348
5828
  if (!resource.columnPresets?.length) return null;
5349
5829
  const stored = readFromStorage(resource.id);
5350
5830
  if (stored && resource.columnPresets.some((p) => p.key === stored)) return stored;
@@ -5352,7 +5832,7 @@ function useColumnPreset(resource) {
5352
5832
  });
5353
5833
  return {
5354
5834
  activePreset,
5355
- setPreset: (0, react.useCallback)((key) => {
5835
+ setPreset: (0, react$1.useCallback)((key) => {
5356
5836
  writeToStorage(resource.id, key);
5357
5837
  setActivePresetState(key);
5358
5838
  }, [resource.id]),
@@ -5673,19 +6153,19 @@ function buildFields(items) {
5673
6153
  //#endregion
5674
6154
  //#region packages/crud/crud/useCrudPage.ts
5675
6155
  function useCrudPage(resource, externalFormRef) {
5676
- const resolvedResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
6156
+ const resolvedResource = (0, react$1.useMemo)(() => resolveCrudResource(resource), [resource]);
5677
6157
  const events = resolvedResource.events;
5678
- const _internalFormRef = (0, react.useRef)(null);
6158
+ const _internalFormRef = (0, react$1.useRef)(null);
5679
6159
  const formRef = externalFormRef ?? _internalFormRef;
5680
- const fields = (0, react.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
6160
+ const fields = (0, react$1.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
5681
6161
  return {
5682
6162
  events,
5683
6163
  resource: resolvedResource,
5684
6164
  fields,
5685
- formFields: (0, react.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
6165
+ formFields: (0, react$1.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
5686
6166
  formRef,
5687
6167
  permissions: usePermissions(resolvedResource, resolvedResource._supportedOperations ?? []),
5688
- selectionState: useSelectionState((0, react.useMemo)(() => {
6168
+ selectionState: useSelectionState((0, react$1.useMemo)(() => {
5689
6169
  return fields.find((f) => f.isIdentity)?.name ?? "id";
5690
6170
  }, [fields])),
5691
6171
  presetState: useColumnPreset(resolvedResource)
@@ -6044,7 +6524,7 @@ function useDialogStoreContext() {
6044
6524
  function useCrudDialogStore(resourceId) {
6045
6525
  const { state, dispatch } = useDialogStoreContext();
6046
6526
  const dialogState = state[resourceId] ?? initialDialogState();
6047
- const openDialog = (0, react.useCallback)((mode, rowData = null) => {
6527
+ const openDialog = (0, react$1.useCallback)((mode, rowData = null) => {
6048
6528
  dispatch({
6049
6529
  type: "OPEN",
6050
6530
  resourceId,
@@ -6052,7 +6532,7 @@ function useCrudDialogStore(resourceId) {
6052
6532
  rowData
6053
6533
  });
6054
6534
  }, [dispatch, resourceId]);
6055
- const closeDialog = (0, react.useCallback)(() => {
6535
+ const closeDialog = (0, react$1.useCallback)(() => {
6056
6536
  dispatch({
6057
6537
  type: "CLOSE",
6058
6538
  resourceId
@@ -6149,7 +6629,7 @@ function buildRoutingFilterRules(fields, initialFilters) {
6149
6629
  * Public export — wraps CrudPageInner in its own DialogStoreProvider so that
6150
6630
  * existing pages do not need to add a provider themselves.
6151
6631
  */
6152
- const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, onOperationChange }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogStoreProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudPageInner, {
6632
+ const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid, onOperationChange }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogStoreProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudPageInner, {
6153
6633
  resource,
6154
6634
  onFormDataChange,
6155
6635
  initialRecordId,
@@ -6162,9 +6642,10 @@ const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, c
6162
6642
  editDisabled,
6163
6643
  deleteDisabled,
6164
6644
  gridRef,
6645
+ aboveGrid,
6165
6646
  onOperationChange
6166
6647
  }) });
6167
- const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, initialIsNew = false, computedValues = EMPTY_COMPUTED_VALUES, initialFilters = EMPTY_INITIAL_FILTERS, onFiltersChange, formRef: externalFormRef, onSelectionChanged: externalOnSelectionChanged, editDisabled, deleteDisabled, gridRef: externalGridRef, onOperationChange }) => {
6648
+ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, initialIsNew = false, computedValues = EMPTY_COMPUTED_VALUES, initialFilters = EMPTY_INITIAL_FILTERS, onFiltersChange, formRef: externalFormRef, onSelectionChanged: externalOnSelectionChanged, editDisabled, deleteDisabled, gridRef: externalGridRef, aboveGrid: aboveGridOverride, onOperationChange }) => {
6168
6649
  const { t } = (0, _nubitio_core.useCoreTranslation)();
6169
6650
  const { events, resource: resolvedResource, fields, formFields, formRef, permissions, selectionState, presetState } = useCrudPage((0, react.useMemo)(() => resolveCrudResource(resource), [resource]), externalFormRef);
6170
6651
  const datagridFields = (0, react.useMemo)(() => fields.filter((field) => field.isIdentity || field.visible !== false), [fields]);
@@ -6427,6 +6908,16 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6427
6908
  resolvedResource.rowActions,
6428
6909
  t
6429
6910
  ]);
6911
+ const aboveGridSlot = aboveGridOverride ?? resolvedResource.aboveGrid;
6912
+ const aboveGridContent = (() => {
6913
+ if (!aboveGridSlot) return;
6914
+ if (typeof aboveGridSlot === "function") return aboveGridSlot({
6915
+ resource: resolvedResource,
6916
+ gridRef,
6917
+ refresh: () => gridRef.current?.refresh()
6918
+ });
6919
+ return aboveGridSlot;
6920
+ })();
6430
6921
  /** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
6431
6922
  const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColumnPresetSelector, {
6432
6923
  resourceId: resolvedResource.id,
@@ -6489,6 +6980,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6489
6980
  editMode: resolvedResource.editMode,
6490
6981
  visibleColumns: presetState.visibleColumns,
6491
6982
  beforeToolbar: renderPresetSelector,
6983
+ aboveGrid: aboveGridContent,
6492
6984
  detailUrl: gridDetail?.url,
6493
6985
  detailFields: gridDetail?.fields,
6494
6986
  onFilterChange: onFiltersChange,
@@ -6639,7 +7131,7 @@ function useRouting(routing) {
6639
7131
  return {
6640
7132
  initialRecordId,
6641
7133
  initialIsNew,
6642
- initialFilters: (0, react.useMemo)(() => {
7134
+ initialFilters: (0, react$1.useMemo)(() => {
6643
7135
  if (!syncFiltersToUrl) return NO_FILTERS;
6644
7136
  const filters = {};
6645
7137
  searchParams.forEach((value, key) => {
@@ -6647,7 +7139,7 @@ function useRouting(routing) {
6647
7139
  });
6648
7140
  return filters;
6649
7141
  }, [syncFiltersToUrl, searchParams]),
6650
- syncFilters: (0, react.useCallback)((filters) => {
7142
+ syncFilters: (0, react$1.useCallback)((filters) => {
6651
7143
  if (!routing?.syncFiltersToUrl) return;
6652
7144
  const next = new URLSearchParams();
6653
7145
  Object.entries(filters).forEach(([key, value]) => {
@@ -6758,23 +7250,23 @@ function crudReducer(state, action) {
6758
7250
  */
6759
7251
  function useSmartCrudOperation(events, routingState) {
6760
7252
  const [on] = (0, _nubitio_core.useEvents)();
6761
- const [{ activeOperation, formData }, dispatchState] = (0, react.useReducer)(crudReducer, routingState, stateFromRouting);
6762
- const handleFormDataChange = (0, react.useCallback)((data) => {
7253
+ const [{ activeOperation, formData }, dispatchState] = (0, react$1.useReducer)(crudReducer, routingState, stateFromRouting);
7254
+ const handleFormDataChange = (0, react$1.useCallback)((data) => {
6763
7255
  dispatchState({
6764
7256
  type: "set-form-data",
6765
7257
  data
6766
7258
  });
6767
7259
  }, []);
6768
- const startCreate = (0, react.useCallback)(() => {
7260
+ const startCreate = (0, react$1.useCallback)(() => {
6769
7261
  dispatchState({ type: "create" });
6770
7262
  }, []);
6771
- const startEdit = (0, react.useCallback)(() => {
7263
+ const startEdit = (0, react$1.useCallback)(() => {
6772
7264
  dispatchState({ type: "edit" });
6773
7265
  }, []);
6774
- const resetOperation = (0, react.useCallback)(() => {
7266
+ const resetOperation = (0, react$1.useCallback)(() => {
6775
7267
  dispatchState({ type: "reset" });
6776
7268
  }, []);
6777
- (0, react.useEffect)(() => {
7269
+ (0, react$1.useEffect)(() => {
6778
7270
  dispatchState({
6779
7271
  type: "sync-routing",
6780
7272
  routingState: {
@@ -6783,7 +7275,7 @@ function useSmartCrudOperation(events, routingState) {
6783
7275
  }
6784
7276
  });
6785
7277
  }, [routingState.initialIsNew, routingState.initialRecordId]);
6786
- (0, react.useEffect)(() => {
7278
+ (0, react$1.useEffect)(() => {
6787
7279
  const subscriptions = [];
6788
7280
  if (events?.ADD) subscriptions.push(on(events.ADD, () => {
6789
7281
  dispatchState({ type: "create" });
@@ -6829,7 +7321,7 @@ function useSmartCrudOperation(events, routingState) {
6829
7321
  * so grids and forms can resolve row keys consistently.
6830
7322
  */
6831
7323
  function useFieldPermissions(fields, userRoles) {
6832
- return (0, react.useMemo)(() => {
7324
+ return (0, react$1.useMemo)(() => {
6833
7325
  return fields.reduce((acc, field) => {
6834
7326
  if (field.isIdentity) {
6835
7327
  acc.push(field);
@@ -6899,7 +7391,7 @@ function evaluateConditionalRuleState(field, formData) {
6899
7391
  * are returned. Empty objects `{}` are treated as valid create-form state.
6900
7392
  */
6901
7393
  function useConditionalRules(fields, formData) {
6902
- return (0, react.useMemo)(() => {
7394
+ return (0, react$1.useMemo)(() => {
6903
7395
  return fields.map((field) => evaluateConditionalRuleState(field, formData));
6904
7396
  }, [fields, formData]);
6905
7397
  }
@@ -6934,9 +7426,9 @@ function useDependsOn(fields, formData) {
6934
7426
  url: field.url ?? null,
6935
7427
  values: (field.dependsOn ?? []).map((dep) => formData[dep])
6936
7428
  }));
6937
- const isMounted = (0, react.useRef)(false);
6938
- const previousEntriesRef = (0, react.useRef)(null);
6939
- (0, react.useEffect)(() => {
7429
+ const isMounted = (0, react$1.useRef)(false);
7430
+ const previousEntriesRef = (0, react$1.useRef)(null);
7431
+ (0, react$1.useEffect)(() => {
6940
7432
  if (!isMounted.current) {
6941
7433
  isMounted.current = true;
6942
7434
  previousEntriesRef.current = currentEntries;
@@ -7118,7 +7610,7 @@ function applyFieldOperationSemantics(field, operation, behavior, owner = `Field
7118
7610
  * from schema loading and routing concerns.
7119
7611
  */
7120
7612
  function useSmartCrudFields(fields, activeOperation, formData, roles) {
7121
- const permissionFilteredFields = useFieldPermissions((0, react.useMemo)(() => {
7613
+ const permissionFilteredFields = useFieldPermissions((0, react$1.useMemo)(() => {
7122
7614
  if (!activeOperation) return fields;
7123
7615
  return fields.map((field) => {
7124
7616
  const runtimeField = field;
@@ -7130,7 +7622,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7130
7622
  useDependsOn(permissionFilteredFields, formData);
7131
7623
  return {
7132
7624
  gridFields: permissionFilteredFields,
7133
- processedFields: (0, react.useMemo)(() => {
7625
+ processedFields: (0, react$1.useMemo)(() => {
7134
7626
  const stateByName = new Map(fieldStates.map((s) => [s.name, s]));
7135
7627
  let anyChanged = false;
7136
7628
  const merged = permissionFilteredFields.map((field) => {
@@ -7150,7 +7642,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7150
7642
  });
7151
7643
  return anyChanged ? merged : permissionFilteredFields;
7152
7644
  }, [permissionFilteredFields, fieldStates]),
7153
- computedValues: (0, react.useMemo)(() => fieldStates.reduce((acc, state) => {
7645
+ computedValues: (0, react$1.useMemo)(() => fieldStates.reduce((acc, state) => {
7154
7646
  if (state.computedValue !== void 0) acc[state.name] = state.computedValue;
7155
7647
  return acc;
7156
7648
  }, {}), [fieldStates])
@@ -7610,7 +8102,7 @@ function formatRuntimeErrorMessage(error) {
7610
8102
  *
7611
8103
  * URL deep-linking is wired via `initialRecordId` / `initialIsNew` props on CrudPage.
7612
8104
  */
7613
- function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef }) {
8105
+ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid }) {
7614
8106
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
7615
8107
  const internalGridRef = (0, react.useRef)(null);
7616
8108
  const effectiveGridRef = gridRef ?? internalGridRef;
@@ -7695,6 +8187,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7695
8187
  editDisabled,
7696
8188
  deleteDisabled,
7697
8189
  gridRef: effectiveGridRef,
8190
+ aboveGrid: aboveGrid ?? resource.aboveGrid,
7698
8191
  onOperationChange: (operation) => {
7699
8192
  if (operation === "create") {
7700
8193
  startCreate();