@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.mjs CHANGED
@@ -4,6 +4,9 @@ import { createPortal } from "react-dom";
4
4
  import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, FileDropzone, IconButton, Skeleton, Timeline, TimelineItem } from "@nubitio/ui";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createCrudEvents, createScopedEventBus, getCoreCurrency, getCoreLocale, getCoreTimezone, useCoreHttpClient, useCoreRuntime, useCoreTranslation, useEvents, useMercureSubscription } from "@nubitio/core";
7
+ import { EditorContent, useEditor } from "@tiptap/react";
8
+ import StarterKit from "@tiptap/starter-kit";
9
+ import Link from "@tiptap/extension-link";
7
10
  import { useQueryClient } from "@tanstack/react-query";
8
11
  //#region packages/crud/crud/defineResource.ts
9
12
  const stringResourceCache = /* @__PURE__ */ new Map();
@@ -1428,19 +1431,179 @@ const fileTypeModule = {
1428
1431
  }
1429
1432
  };
1430
1433
  //#endregion
1434
+ //#region packages/crud/field/registry/types/HtmlEditor.tsx
1435
+ function ToolbarButton({ active, disabled, title, onClick, children }) {
1436
+ return /* @__PURE__ */ jsx("button", {
1437
+ type: "button",
1438
+ title,
1439
+ disabled,
1440
+ className: `nb-html-editor__btn${active ? " nb-html-editor__btn--active" : ""}`,
1441
+ onMouseDown: (e) => {
1442
+ e.preventDefault();
1443
+ onClick();
1444
+ },
1445
+ children
1446
+ });
1447
+ }
1448
+ function ToolbarDivider() {
1449
+ return /* @__PURE__ */ jsx("span", {
1450
+ className: "nb-html-editor__divider",
1451
+ "aria-hidden": true
1452
+ });
1453
+ }
1454
+ function HtmlEditor({ id, name, value, disabled, readOnly, hasError, onChange }) {
1455
+ const editable = !disabled && !readOnly;
1456
+ const editor = useEditor({
1457
+ extensions: [StarterKit, Link.configure({
1458
+ openOnClick: false,
1459
+ autolink: true
1460
+ })],
1461
+ content: value,
1462
+ editable,
1463
+ onUpdate: ({ editor: e }) => {
1464
+ onChange(e.isEmpty ? "" : e.getHTML());
1465
+ }
1466
+ });
1467
+ useEffect(() => {
1468
+ if (!editor) return;
1469
+ if ((editor.isEmpty ? "" : editor.getHTML()) !== value) editor.commands.setContent(value ?? "");
1470
+ }, [value, editor]);
1471
+ useEffect(() => {
1472
+ editor?.setEditable(editable);
1473
+ }, [editable, editor]);
1474
+ const handleLinkToggle = useCallback(() => {
1475
+ if (!editor) return;
1476
+ if (editor.isActive("link")) editor.chain().focus().unsetLink().run();
1477
+ else {
1478
+ const url = window.prompt("URL");
1479
+ if (url) editor.chain().focus().setLink({ href: url }).run();
1480
+ }
1481
+ }, [editor]);
1482
+ return /* @__PURE__ */ jsxs("div", {
1483
+ className: `nb-html-editor${hasError ? " nb-html-editor--error" : ""}${!editable ? " nb-html-editor--readonly" : ""}`,
1484
+ children: [
1485
+ editable && /* @__PURE__ */ jsxs("div", {
1486
+ className: "nb-html-editor__toolbar",
1487
+ role: "toolbar",
1488
+ "aria-label": "Text formatting",
1489
+ children: [
1490
+ /* @__PURE__ */ jsx(ToolbarButton, {
1491
+ title: "Bold (Ctrl+B)",
1492
+ active: editor?.isActive("bold"),
1493
+ onClick: () => editor?.chain().focus().toggleBold().run(),
1494
+ children: /* @__PURE__ */ jsx("strong", { children: "B" })
1495
+ }),
1496
+ /* @__PURE__ */ jsx(ToolbarButton, {
1497
+ title: "Italic (Ctrl+I)",
1498
+ active: editor?.isActive("italic"),
1499
+ onClick: () => editor?.chain().focus().toggleItalic().run(),
1500
+ children: /* @__PURE__ */ jsx("em", { children: "I" })
1501
+ }),
1502
+ /* @__PURE__ */ jsx(ToolbarButton, {
1503
+ title: "Strikethrough",
1504
+ active: editor?.isActive("strike"),
1505
+ onClick: () => editor?.chain().focus().toggleStrike().run(),
1506
+ children: /* @__PURE__ */ jsx("s", { children: "S" })
1507
+ }),
1508
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1509
+ /* @__PURE__ */ jsx(ToolbarButton, {
1510
+ title: "Heading 2",
1511
+ active: editor?.isActive("heading", { level: 2 }),
1512
+ onClick: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(),
1513
+ children: "H2"
1514
+ }),
1515
+ /* @__PURE__ */ jsx(ToolbarButton, {
1516
+ title: "Heading 3",
1517
+ active: editor?.isActive("heading", { level: 3 }),
1518
+ onClick: () => editor?.chain().focus().toggleHeading({ level: 3 }).run(),
1519
+ children: "H3"
1520
+ }),
1521
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1522
+ /* @__PURE__ */ jsx(ToolbarButton, {
1523
+ title: "Bullet list",
1524
+ active: editor?.isActive("bulletList"),
1525
+ onClick: () => editor?.chain().focus().toggleBulletList().run(),
1526
+ children: "≡"
1527
+ }),
1528
+ /* @__PURE__ */ jsx(ToolbarButton, {
1529
+ title: "Ordered list",
1530
+ active: editor?.isActive("orderedList"),
1531
+ onClick: () => editor?.chain().focus().toggleOrderedList().run(),
1532
+ children: "1."
1533
+ }),
1534
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1535
+ /* @__PURE__ */ jsx(ToolbarButton, {
1536
+ title: "Blockquote",
1537
+ active: editor?.isActive("blockquote"),
1538
+ onClick: () => editor?.chain().focus().toggleBlockquote().run(),
1539
+ children: "“"
1540
+ }),
1541
+ /* @__PURE__ */ jsx(ToolbarButton, {
1542
+ title: editor?.isActive("link") ? "Remove link" : "Add link",
1543
+ active: editor?.isActive("link"),
1544
+ onClick: handleLinkToggle,
1545
+ children: "🔗"
1546
+ }),
1547
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1548
+ /* @__PURE__ */ jsx(ToolbarButton, {
1549
+ title: "Undo (Ctrl+Z)",
1550
+ disabled: !editor?.can().undo(),
1551
+ onClick: () => editor?.chain().focus().undo().run(),
1552
+ children: "↩"
1553
+ }),
1554
+ /* @__PURE__ */ jsx(ToolbarButton, {
1555
+ title: "Redo (Ctrl+Y)",
1556
+ disabled: !editor?.can().redo(),
1557
+ onClick: () => editor?.chain().focus().redo().run(),
1558
+ children: "↪"
1559
+ })
1560
+ ]
1561
+ }),
1562
+ /* @__PURE__ */ jsx("input", {
1563
+ type: "hidden",
1564
+ id,
1565
+ name,
1566
+ value,
1567
+ readOnly: true
1568
+ }),
1569
+ /* @__PURE__ */ jsx(EditorContent, {
1570
+ editor,
1571
+ className: "nb-html-editor__content"
1572
+ })
1573
+ ]
1574
+ });
1575
+ }
1576
+ //#endregion
1431
1577
  //#region packages/crud/field/registry/types/html.tsx
1578
+ function stripTags(html) {
1579
+ if (html === null || html === void 0) return "";
1580
+ const str = String(html);
1581
+ if (!str.includes("<")) return str;
1582
+ try {
1583
+ return new DOMParser().parseFromString(str, "text/html").body.textContent ?? "";
1584
+ } catch {
1585
+ return str.replace(/<[^>]*>/g, "");
1586
+ }
1587
+ }
1432
1588
  const htmlTypeModule = {
1433
1589
  defaultFilterOperator: "contains",
1434
1590
  filterOperators: TEXT_OPERATORS,
1435
1591
  buildFilterTerms: defaultBuildFilterTerms,
1436
- cellText: (_field, value, ctx) => getPrimitiveDisplay(value, ctx.yesLabel, ctx.noLabel),
1592
+ cellText: (_field, value) => stripTags(value),
1437
1593
  serializeFormValue: () => KEEP,
1438
1594
  serializeDetailValue: () => KEEP,
1439
- ControlRender: ({ field, value, commonProps, errorClass, setFieldValue }) => /* @__PURE__ */ jsx("textarea", {
1440
- ...commonProps,
1441
- className: `nb-form__control nb-form__textarea${errorClass}`,
1442
- value: inputValue(value),
1443
- onChange: (event) => setFieldValue(field.name, event.target.value)
1595
+ CellRender: ({ value }) => /* @__PURE__ */ jsx("div", {
1596
+ className: "nb-datagrid__html-cell",
1597
+ dangerouslySetInnerHTML: { __html: String(value ?? "") }
1598
+ }),
1599
+ ControlRender: ({ field, value, commonProps, disabled, errorClass, setFieldValue }) => /* @__PURE__ */ jsx(HtmlEditor, {
1600
+ id: commonProps.id,
1601
+ name: field.name,
1602
+ value: String(value ?? ""),
1603
+ disabled,
1604
+ readOnly: commonProps.readOnly,
1605
+ hasError: errorClass !== "",
1606
+ onChange: (html) => setFieldValue(field.name, html)
1444
1607
  })
1445
1608
  };
1446
1609
  //#endregion
@@ -1737,7 +1900,13 @@ function renderCell(field, row, rowIndex, columnIndex, entityOptions, yesLabel =
1737
1900
  rowIndex,
1738
1901
  columnIndex
1739
1902
  });
1740
- return getFieldTypeModule(field.type).cellText(field, value, {
1903
+ const typeModule = getFieldTypeModule(field.type);
1904
+ if (typeModule.CellRender) return React.createElement(typeModule.CellRender, {
1905
+ field,
1906
+ value,
1907
+ row
1908
+ });
1909
+ return typeModule.cellText(field, value, {
1741
1910
  entityOptions,
1742
1911
  yesLabel,
1743
1912
  noLabel
@@ -1838,6 +2007,337 @@ function DetailGridSection({ fields, url }) {
1838
2007
  });
1839
2008
  }
1840
2009
  //#endregion
2010
+ //#region packages/crud/adapter/HydraAdapter.ts
2011
+ function trimTrailingSlash(value) {
2012
+ return value.replace(/\/+$/, "");
2013
+ }
2014
+ function buildIRI(url, value) {
2015
+ if (value.startsWith("/")) return value;
2016
+ return `${url}/${value}`;
2017
+ }
2018
+ /**
2019
+ * Default backend adapter for API Platform / JSON-LD + Hydra backends.
2020
+ *
2021
+ * Conventions assumed:
2022
+ * - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
2023
+ * - Entity fields are serialized as IRI strings in POST/PATCH bodies.
2024
+ * - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
2025
+ * - `_iri` is a synthetic alias for `@id` used internally by the engine.
2026
+ */
2027
+ const HydraAdapter = {
2028
+ getRowId(record, idField) {
2029
+ const direct = record[idField];
2030
+ if (direct !== void 0 && direct !== null) return String(direct);
2031
+ const iri = record["@id"] ?? record["_iri"];
2032
+ if (iri !== void 0 && iri !== null) return String(iri);
2033
+ return String(record["id"] ?? "");
2034
+ },
2035
+ buildItemUrl(baseUrl, id) {
2036
+ const str = String(id);
2037
+ if (str.startsWith("/")) return str;
2038
+ return `${trimTrailingSlash(baseUrl)}/${str}`;
2039
+ },
2040
+ serializeEntityRef(field, rawValue) {
2041
+ if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
2042
+ if (typeof rawValue === "object") {
2043
+ const entity = rawValue;
2044
+ const atId = entity["@id"];
2045
+ if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
2046
+ const idValue = entity["id"];
2047
+ const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
2048
+ return buildIRI(field.url ?? "", resolvedId);
2049
+ }
2050
+ return buildIRI(field.url ?? "", String(rawValue));
2051
+ },
2052
+ normalizeEntityValue(rawValue, field) {
2053
+ if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
2054
+ if (typeof rawValue === "object" && rawValue !== null) {
2055
+ const entity = rawValue;
2056
+ const atId = entity["@id"];
2057
+ if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
2058
+ const directValue = entity[field.valueField];
2059
+ if (directValue !== void 0 && directValue !== null) return directValue;
2060
+ }
2061
+ return rawValue;
2062
+ },
2063
+ getEntityOptionKey(item, field) {
2064
+ if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
2065
+ return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
2066
+ },
2067
+ parseListResponse(response) {
2068
+ const r = response;
2069
+ const member = r["hydra:member"];
2070
+ if (Array.isArray(member)) return {
2071
+ items: member,
2072
+ total: Number(r["hydra:totalItems"] ?? member.length)
2073
+ };
2074
+ if (Array.isArray(response)) return {
2075
+ items: response,
2076
+ total: response.length
2077
+ };
2078
+ return {
2079
+ items: [],
2080
+ total: 0
2081
+ };
2082
+ },
2083
+ synthesizeEntityKey(field, entityValue) {
2084
+ if (!field.url) return void 0;
2085
+ const base = trimTrailingSlash(field.url);
2086
+ const directId = entityValue["id"];
2087
+ if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
2088
+ const directValue = entityValue[field.valueField];
2089
+ if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
2090
+ for (const key of [
2091
+ "code",
2092
+ "uuid",
2093
+ "slug"
2094
+ ]) {
2095
+ const candidate = entityValue[key];
2096
+ if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
2097
+ }
2098
+ }
2099
+ };
2100
+ //#endregion
2101
+ //#region packages/crud/form/serializeFormData.ts
2102
+ function applySerializedValue(formData, field, result) {
2103
+ if (result.kind === "set") formData[field.name] = result.value;
2104
+ else if (result.kind === "omit") delete formData[field.name];
2105
+ }
2106
+ /**
2107
+ * Pure serialization of form data before HTTP submission.
2108
+ *
2109
+ * Applies uploaded file references and computed fields, then delegates the
2110
+ * per-type wire format (entity refs, business dates, numeric coercion, file
2111
+ * handling, NONE stripping) to each field's Field-Type module.
2112
+ *
2113
+ * This function is extracted from useFormSubmit for testability.
2114
+ */
2115
+ function serializeFormFields(rawData, fields, ctx) {
2116
+ const formData = { ...rawData };
2117
+ ctx.uploadedFiles.forEach((file) => {
2118
+ formData[file.name] = file.iri;
2119
+ });
2120
+ fields.forEach((field) => {
2121
+ if (!field.computed) return;
2122
+ const computedValue = field.computed(formData);
2123
+ if (computedValue !== void 0) formData[field.name] = computedValue;
2124
+ });
2125
+ const moduleCtx = {
2126
+ adapter: ctx.adapter ?? HydraAdapter,
2127
+ format: ctx.format,
2128
+ getFieldValue: ctx.getFieldValue
2129
+ };
2130
+ fields.forEach((field) => {
2131
+ applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
2132
+ });
2133
+ return formData;
2134
+ }
2135
+ /**
2136
+ * Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
2137
+ * Pure function — operates on an array of row records.
2138
+ */
2139
+ function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
2140
+ const details = structuredClone(rows);
2141
+ detailFields.forEach((field) => {
2142
+ const typeModule = getFieldTypeModule(field.type);
2143
+ details.forEach((detail) => {
2144
+ applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
2145
+ });
2146
+ });
2147
+ details.forEach((detail) => {
2148
+ if (!isEditMode) delete detail[detailIdField];
2149
+ else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
2150
+ });
2151
+ return details;
2152
+ }
2153
+ //#endregion
2154
+ //#region packages/crud/datagrid/useInlineEdit.ts
2155
+ /** Fields that are safe to edit inline (identity/readonly/file types are excluded). */
2156
+ function canEditFieldInline(field) {
2157
+ return !field.isIdentity && !field.readonly && field.type !== "file" && field.visibleOnForm !== false;
2158
+ }
2159
+ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError }) {
2160
+ const [draftRows, setDraftRows] = useState(/* @__PURE__ */ new Map());
2161
+ const [savingRows, setSavingRows] = useState(/* @__PURE__ */ new Set());
2162
+ const [rowErrors, setRowErrors] = useState(/* @__PURE__ */ new Map());
2163
+ const draftRowsRef = useRef(draftRows);
2164
+ draftRowsRef.current = draftRows;
2165
+ const optsRef = useRef({
2166
+ url,
2167
+ idField,
2168
+ adapter,
2169
+ httpClient,
2170
+ fields,
2171
+ onSaveSuccess,
2172
+ onSaveError
2173
+ });
2174
+ optsRef.current = {
2175
+ url,
2176
+ idField,
2177
+ adapter,
2178
+ httpClient,
2179
+ fields,
2180
+ onSaveSuccess,
2181
+ onSaveError
2182
+ };
2183
+ const isEditing = useCallback((key) => draftRowsRef.current.has(key), []);
2184
+ const startEdit = useCallback((row) => {
2185
+ const key = row[optsRef.current.idField];
2186
+ setDraftRows((prev) => {
2187
+ const base = mode === "row" ? [] : Array.from(prev.entries());
2188
+ return new Map([...base, [key, { ...row }]]);
2189
+ });
2190
+ setRowErrors((prev) => {
2191
+ const next = new Map(prev);
2192
+ next.delete(key);
2193
+ return next;
2194
+ });
2195
+ }, [mode]);
2196
+ const cancelEdit = useCallback((key) => {
2197
+ setDraftRows((prev) => {
2198
+ const next = new Map(prev);
2199
+ next.delete(key);
2200
+ return next;
2201
+ });
2202
+ setRowErrors((prev) => {
2203
+ const next = new Map(prev);
2204
+ next.delete(key);
2205
+ return next;
2206
+ });
2207
+ }, []);
2208
+ const discardAll = useCallback(() => {
2209
+ setDraftRows(/* @__PURE__ */ new Map());
2210
+ setRowErrors(/* @__PURE__ */ new Map());
2211
+ }, []);
2212
+ const updateDraft = useCallback((key, fieldName, value) => {
2213
+ setDraftRows((prev) => {
2214
+ const current = prev.get(key);
2215
+ if (!current) return prev;
2216
+ const next = new Map(prev);
2217
+ next.set(key, {
2218
+ ...current,
2219
+ [fieldName]: value
2220
+ });
2221
+ return next;
2222
+ });
2223
+ }, []);
2224
+ const doSaveRow = useCallback(async (key) => {
2225
+ const { url: u, adapter: a, httpClient: http, fields: fs, onSaveError: onErr } = optsRef.current;
2226
+ const draft = draftRowsRef.current.get(key);
2227
+ if (!draft) return false;
2228
+ const errors = {};
2229
+ fs.forEach((field) => {
2230
+ if (!canEditFieldInline(field)) return;
2231
+ if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
2232
+ });
2233
+ if (Object.keys(errors).length > 0) {
2234
+ setRowErrors((prev) => new Map(prev).set(key, errors));
2235
+ return false;
2236
+ }
2237
+ setSavingRows((prev) => new Set([...prev, key]));
2238
+ const serialized = serializeFormFields(draft, fs.filter(canEditFieldInline), {
2239
+ uploadedFiles: [],
2240
+ getFieldValue: (name) => draft[name]
2241
+ });
2242
+ try {
2243
+ await http.patch(a.buildItemUrl(u, key), serialized);
2244
+ setDraftRows((prev) => {
2245
+ const next = new Map(prev);
2246
+ next.delete(key);
2247
+ return next;
2248
+ });
2249
+ setRowErrors((prev) => {
2250
+ const next = new Map(prev);
2251
+ next.delete(key);
2252
+ return next;
2253
+ });
2254
+ return true;
2255
+ } catch (err) {
2256
+ onErr?.(key, err);
2257
+ if (typeof err === "object" && err !== null && "status" in err && err.status === 422 && "data" in err) {
2258
+ const data = err.data;
2259
+ if (typeof data === "object" && data !== null && "violations" in data) {
2260
+ const violations = data.violations ?? [];
2261
+ const fieldErrors = {};
2262
+ violations.forEach((v) => {
2263
+ fieldErrors[v.propertyPath] = v.message;
2264
+ });
2265
+ setRowErrors((prev) => new Map(prev).set(key, fieldErrors));
2266
+ }
2267
+ }
2268
+ return false;
2269
+ } finally {
2270
+ setSavingRows((prev) => {
2271
+ const next = new Set(prev);
2272
+ next.delete(key);
2273
+ return next;
2274
+ });
2275
+ }
2276
+ }, []);
2277
+ return {
2278
+ draftRows,
2279
+ savingRows,
2280
+ rowErrors,
2281
+ isEditing,
2282
+ startEdit,
2283
+ cancelEdit,
2284
+ discardAll,
2285
+ updateDraft,
2286
+ saveRow: useCallback(async (key) => {
2287
+ if (await doSaveRow(key)) optsRef.current.onSaveSuccess?.();
2288
+ }, [doSaveRow]),
2289
+ saveAll: useCallback(async () => {
2290
+ const keys = Array.from(draftRowsRef.current.keys());
2291
+ if ((await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value)) optsRef.current.onSaveSuccess?.();
2292
+ }, [doSaveRow])
2293
+ };
2294
+ }
2295
+ //#endregion
2296
+ //#region packages/crud/datagrid/InlineEditCell.tsx
2297
+ /**
2298
+ * Renders a single cell as an editable control during inline row editing.
2299
+ * Uses the same FieldTypeModule.ControlRender path as the full form, but
2300
+ * with a compact common-props class so controls fit inside a table cell.
2301
+ */
2302
+ function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t }) {
2303
+ const typeModule = getFieldTypeModule(field.type);
2304
+ const fieldError = errors?.[field.name];
2305
+ const errorClass = fieldError ? " is-error" : "";
2306
+ const commonProps = {
2307
+ className: `nb-inline-control${errorClass}`,
2308
+ disabled,
2309
+ id: `iec-${String(rowKey)}-${field.name}`,
2310
+ name: field.name,
2311
+ onClick: void 0,
2312
+ readOnly: false,
2313
+ required: field.required
2314
+ };
2315
+ const ctx = {
2316
+ httpClient,
2317
+ t,
2318
+ remoteOptions: allRemoteOptions,
2319
+ getPrependData: () => void 0,
2320
+ getFieldValue: (name) => draft[name],
2321
+ getExistingMedia: () => null,
2322
+ clearExistingMedia: () => {},
2323
+ upsertUploadedFile: () => {}
2324
+ };
2325
+ return /* @__PURE__ */ jsx("div", {
2326
+ className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
2327
+ children: typeModule.ControlRender({
2328
+ field,
2329
+ value: draft[field.name],
2330
+ error: fieldError,
2331
+ errorClass,
2332
+ disabled,
2333
+ readOnly: false,
2334
+ commonProps,
2335
+ setFieldValue: onChange,
2336
+ ctx
2337
+ })
2338
+ });
2339
+ }
2340
+ //#endregion
1841
2341
  //#region packages/crud/summary/SummaryUtils.ts
1842
2342
  function toFiniteNumber(value) {
1843
2343
  if (value === null || value === void 0 || value === "") return null;
@@ -1907,19 +2407,20 @@ function resolveSummaryText(rows, item) {
1907
2407
  const DETAIL_COL_WIDTH = 36;
1908
2408
  const CHECKBOX_COL_WIDTH = 36;
1909
2409
  const ACTIONS_COL_WIDTH = 44;
2410
+ const INLINE_ACTIONS_COL_WIDTH = 72;
1910
2411
  const DEFAULT_COL_WIDTH = 120;
1911
2412
  const MIN_COL_WIDTH = 48;
1912
2413
  function getColumnWidth(field, colWidths) {
1913
2414
  return colWidths[field.name] ?? field.width ?? field.minWidth ?? DEFAULT_COL_WIDTH;
1914
2415
  }
1915
- function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth }) {
2416
+ function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth, actionsColWidth = ACTIONS_COL_WIDTH }) {
1916
2417
  let total = 0;
1917
2418
  if (hasDetail) total += DETAIL_COL_WIDTH;
1918
2419
  if (hasCheckbox) total += CHECKBOX_COL_WIDTH;
1919
2420
  visibleFields.forEach((field) => {
1920
2421
  total += getColumnWidth(field, colWidths);
1921
2422
  });
1922
- if (hasRowActions) total += ACTIONS_COL_WIDTH;
2423
+ if (hasRowActions) total += actionsColWidth;
1923
2424
  return Math.max(containerWidth, total);
1924
2425
  }
1925
2426
  function GridColumnGroup({ fields, colWidths, hasCheckbox, hasDetail, hasRowActions }) {
@@ -2302,9 +2803,11 @@ const NativeDataGridView = forwardRef((options, ref) => {
2302
2803
  ]);
2303
2804
  const fieldsRef = useRef(options.fields);
2304
2805
  fieldsRef.current = options.fields;
2806
+ const loadSeqRef = useRef(0);
2305
2807
  const loadRows = useCallback(async () => {
2306
2808
  if (options.manualLoad) return rowsRef.current;
2307
2809
  setIsGridLoading(true);
2810
+ const seq = ++loadSeqRef.current;
2308
2811
  const loadOptions = {
2309
2812
  filter: buildFilterExpression(filters, filterOperators, fieldsRef.current),
2310
2813
  sort
@@ -2314,6 +2817,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
2314
2817
  loadOptions.take = pageSize;
2315
2818
  }
2316
2819
  const result = await source.load(loadOptions);
2820
+ if (seq !== loadSeqRef.current) return rowsRef.current;
2317
2821
  rowsRef.current = result.data;
2318
2822
  setRows(result.data);
2319
2823
  setTotalCount(result.totalCount);
@@ -2367,6 +2871,17 @@ const NativeDataGridView = forwardRef((options, ref) => {
2367
2871
  resourceStoreFactory,
2368
2872
  visibleFields
2369
2873
  ]);
2874
+ const canInlineEditMode = options.editMode === "row" || options.editMode === "batch";
2875
+ const inlineEdit = useInlineEdit({
2876
+ mode: options.editMode === "batch" ? "batch" : "row",
2877
+ url: options.url,
2878
+ idField,
2879
+ adapter: options.adapter,
2880
+ httpClient,
2881
+ fields: options.fields,
2882
+ onSaveSuccess: () => void loadRows(),
2883
+ onSaveError: () => {}
2884
+ });
2370
2885
  const handleStateRef = useRef({
2371
2886
  selectedKeys,
2372
2887
  filters,
@@ -2433,7 +2948,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
2433
2948
  const rowEditable = (row) => options.canEditRow?.(row) !== false;
2434
2949
  const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
2435
2950
  const buildRowActions = (row) => [
2436
- ...options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT) ? [{
2951
+ ...options.allowEdit && rowEditable(row) && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
2952
+ text: t("grid.inlineEditRow"),
2953
+ icon: "ph-pencil-simple",
2954
+ disabled: options.editDisabled,
2955
+ onClick: () => inlineEdit.startEdit(row)
2956
+ }] : [],
2957
+ ...options.allowEdit && rowEditable(row) && !canInlineEditMode && (options.onEdit || options.events?.EDIT) ? [{
2437
2958
  text: t("grid.buttonEdit"),
2438
2959
  icon: "ph-pencil-simple",
2439
2960
  disabled: options.editDisabled,
@@ -2457,6 +2978,10 @@ const NativeDataGridView = forwardRef((options, ref) => {
2457
2978
  }] : []
2458
2979
  ];
2459
2980
  const openRow = (row) => {
2981
+ if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
2982
+ inlineEdit.startEdit(row);
2983
+ return;
2984
+ }
2460
2985
  if (options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT)) {
2461
2986
  if (options.onEdit) options.onEdit(row);
2462
2987
  else emit(options.events.EDIT, { row });
@@ -2588,22 +3113,24 @@ const NativeDataGridView = forwardRef((options, ref) => {
2588
3113
  };
2589
3114
  const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
2590
3115
  const hasCheckbox = options.selectionMode === "multiple";
2591
- const hasBuiltInRowActions = Boolean(options.allowEdit && (options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
3116
+ const hasBuiltInRowActions = Boolean(options.allowEdit && (canInlineEditMode || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
2592
3117
  const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
2593
3118
  const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
2594
3119
  const hasDetail = Boolean(options.detailFields);
3120
+ const actionsColWidth = canInlineEditMode ? INLINE_ACTIONS_COL_WIDTH : ACTIONS_COL_WIDTH;
2595
3121
  const layoutWidth = computeLayoutWidth({
2596
3122
  visibleFields,
2597
3123
  colWidths,
2598
3124
  hasCheckbox,
2599
3125
  hasDetail,
2600
3126
  hasRowActions,
2601
- containerWidth
3127
+ containerWidth,
3128
+ actionsColWidth
2602
3129
  });
2603
3130
  const resolvedColWidths = useMemo(() => {
2604
3131
  if (visibleFields.length === 0) return colWidths;
2605
3132
  const dataTotal = visibleFields.reduce((sum, f) => sum + getColumnWidth(f, colWidths), 0);
2606
- const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? ACTIONS_COL_WIDTH : 0);
3133
+ const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
2607
3134
  const extra = Math.max(0, containerWidth - dataTotal - fixedTotal);
2608
3135
  if (extra === 0 || dataTotal === 0) return colWidths;
2609
3136
  let distributed = 0;
@@ -2707,6 +3234,31 @@ const NativeDataGridView = forwardRef((options, ref) => {
2707
3234
  })
2708
3235
  ]
2709
3236
  }),
3237
+ options.editMode === "batch" && inlineEdit.draftRows.size > 0 && /* @__PURE__ */ jsxs("div", {
3238
+ className: "nb-datagrid__batch-bar",
3239
+ role: "status",
3240
+ children: [/* @__PURE__ */ jsx("span", {
3241
+ className: "nb-datagrid__batch-bar-label",
3242
+ children: t("grid.inlineUnsavedRows", { count: inlineEdit.draftRows.size })
3243
+ }), /* @__PURE__ */ jsxs("div", {
3244
+ className: "nb-datagrid__batch-bar-actions",
3245
+ children: [/* @__PURE__ */ jsx("button", {
3246
+ type: "button",
3247
+ className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--discard",
3248
+ onClick: () => inlineEdit.discardAll(),
3249
+ children: t("grid.inlineDiscardAll")
3250
+ }), /* @__PURE__ */ jsx("button", {
3251
+ type: "button",
3252
+ className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--save",
3253
+ onClick: () => void inlineEdit.saveAll(),
3254
+ children: t("grid.inlineSaveAll", { count: inlineEdit.draftRows.size })
3255
+ })]
3256
+ })]
3257
+ }),
3258
+ options.aboveGrid ? /* @__PURE__ */ jsx("div", {
3259
+ className: "nb-datagrid__above-grid",
3260
+ children: options.aboveGrid
3261
+ }) : null,
2710
3262
  isMobile && quickSearchField && (options.filterRow ?? true) && /* @__PURE__ */ jsxs("div", {
2711
3263
  className: "nb-datagrid__quick-search",
2712
3264
  children: [
@@ -2996,16 +3548,33 @@ const NativeDataGridView = forwardRef((options, ref) => {
2996
3548
  }) }) : rows.map((row, rowIndex) => {
2997
3549
  const key = row[idField] ?? rowIndex;
2998
3550
  const selected = selectedKeys.includes(key);
3551
+ const editing = inlineEdit.draftRows.has(key);
3552
+ const saving = inlineEdit.savingRows.has(key);
3553
+ const rowDraft = inlineEdit.draftRows.get(key) ?? row;
3554
+ const rowFieldErrors = inlineEdit.rowErrors.get(key);
2999
3555
  const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
3000
3556
  const detailUrl = options.detailUrl?.replace("{id}", String(key));
3001
3557
  const expanded = expandedKeys.has(key);
3002
3558
  const rowActions = buildRowActions(row);
3003
3559
  return /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("tr", {
3004
- className: `nb-datagrid__row ${expanded ? "nb-datagrid__row--expanded" : ""} ${selected ? "nb-datagrid__row--selected" : ""}`,
3560
+ className: [
3561
+ "nb-datagrid__row",
3562
+ expanded ? "nb-datagrid__row--expanded" : "",
3563
+ selected ? "nb-datagrid__row--selected" : "",
3564
+ editing ? "nb-datagrid__row--editing" : "",
3565
+ saving ? "nb-datagrid__row--saving" : ""
3566
+ ].filter(Boolean).join(" "),
3005
3567
  tabIndex: 0,
3006
3568
  "aria-selected": selected,
3007
- onClick: () => selectRow(row),
3569
+ onClick: () => {
3570
+ if (!editing) selectRow(row);
3571
+ },
3008
3572
  onDoubleClick: () => {
3573
+ if (editing) return;
3574
+ if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
3575
+ inlineEdit.startEdit(row);
3576
+ return;
3577
+ }
3009
3578
  if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
3010
3579
  if (options.onEdit) options.onEdit(row);
3011
3580
  else emit(options.events.EDIT, { row });
@@ -3014,10 +3583,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
3014
3583
  if (options.allowView && options.onView) options.onView(row);
3015
3584
  },
3016
3585
  onKeyDown: (event) => {
3017
- if (event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3586
+ if (editing && event.key === "Escape") inlineEdit.cancelEdit(key);
3587
+ else if (editing && event.key === "Enter") inlineEdit.saveRow(key);
3588
+ else if (!editing && event.key === "Enter" && options.allowEdit && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
3589
+ else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3018
3590
  else emit(options.events.EDIT, { row });
3019
- else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3020
- else if (event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3591
+ else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3592
+ else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3021
3593
  else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
3022
3594
  event.preventDefault();
3023
3595
  const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
@@ -3062,9 +3634,29 @@ const NativeDataGridView = forwardRef((options, ref) => {
3062
3634
  })
3063
3635
  }),
3064
3636
  visibleFields.map((field, columnIndex) => {
3637
+ const width = getColumnWidth(field, resolvedColWidths);
3638
+ if (editing && canEditFieldInline(field)) return /* @__PURE__ */ jsx("td", {
3639
+ style: {
3640
+ width,
3641
+ textAlign: field.align
3642
+ },
3643
+ className: "nb-datagrid__edit-cell",
3644
+ onClick: (e) => e.stopPropagation(),
3645
+ children: /* @__PURE__ */ jsx(InlineEditCell, {
3646
+ field,
3647
+ rowKey: key,
3648
+ draft: rowDraft,
3649
+ onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
3650
+ errors: rowFieldErrors,
3651
+ disabled: saving,
3652
+ allRemoteOptions: filterRemoteOptions,
3653
+ httpClient,
3654
+ t
3655
+ })
3656
+ }, field.name);
3065
3657
  return /* @__PURE__ */ jsx("td", {
3066
3658
  style: {
3067
- width: getColumnWidth(field, resolvedColWidths),
3659
+ width,
3068
3660
  textAlign: field.align
3069
3661
  },
3070
3662
  title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
@@ -3074,7 +3666,32 @@ const NativeDataGridView = forwardRef((options, ref) => {
3074
3666
  hasRowActions && /* @__PURE__ */ jsx("td", {
3075
3667
  className: "nb-datagrid__actions-cell",
3076
3668
  onClick: (e) => e.stopPropagation(),
3077
- children: /* @__PURE__ */ jsx("div", {
3669
+ children: editing ? /* @__PURE__ */ jsxs("div", {
3670
+ className: "nb-datagrid__inline-actions",
3671
+ children: [/* @__PURE__ */ jsx("button", {
3672
+ type: "button",
3673
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--save",
3674
+ disabled: saving,
3675
+ "aria-label": t("grid.inlineSaveRow"),
3676
+ title: t("grid.inlineSaveRow"),
3677
+ onClick: () => void inlineEdit.saveRow(key),
3678
+ children: /* @__PURE__ */ jsx("i", {
3679
+ className: "ph ph-check",
3680
+ "aria-hidden": "true"
3681
+ })
3682
+ }), /* @__PURE__ */ jsx("button", {
3683
+ type: "button",
3684
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--cancel",
3685
+ disabled: saving,
3686
+ "aria-label": t("grid.inlineCancelRow"),
3687
+ title: t("grid.inlineCancelRow"),
3688
+ onClick: () => inlineEdit.cancelEdit(key),
3689
+ children: /* @__PURE__ */ jsx("i", {
3690
+ className: "ph ph-x",
3691
+ "aria-hidden": "true"
3692
+ })
3693
+ })]
3694
+ }) : /* @__PURE__ */ jsx("div", {
3078
3695
  className: "nb-datagrid__row-actions",
3079
3696
  children: rowActions.length > 0 && /* @__PURE__ */ jsx(IconButton, {
3080
3697
  icon: "ph ph-dots-three-vertical",
@@ -3424,97 +4041,6 @@ const FORM_EVENTS = {
3424
4041
  };
3425
4042
  const FORM_ERRORS_EVENT = "form-errors";
3426
4043
  //#endregion
3427
- //#region packages/crud/adapter/HydraAdapter.ts
3428
- function trimTrailingSlash(value) {
3429
- return value.replace(/\/+$/, "");
3430
- }
3431
- function buildIRI(url, value) {
3432
- if (value.startsWith("/")) return value;
3433
- return `${url}/${value}`;
3434
- }
3435
- /**
3436
- * Default backend adapter for API Platform / JSON-LD + Hydra backends.
3437
- *
3438
- * Conventions assumed:
3439
- * - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
3440
- * - Entity fields are serialized as IRI strings in POST/PATCH bodies.
3441
- * - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
3442
- * - `_iri` is a synthetic alias for `@id` used internally by the engine.
3443
- */
3444
- const HydraAdapter = {
3445
- getRowId(record, idField) {
3446
- const direct = record[idField];
3447
- if (direct !== void 0 && direct !== null) return String(direct);
3448
- const iri = record["@id"] ?? record["_iri"];
3449
- if (iri !== void 0 && iri !== null) return String(iri);
3450
- return String(record["id"] ?? "");
3451
- },
3452
- buildItemUrl(baseUrl, id) {
3453
- const str = String(id);
3454
- if (str.startsWith("/")) return str;
3455
- return `${trimTrailingSlash(baseUrl)}/${str}`;
3456
- },
3457
- serializeEntityRef(field, rawValue) {
3458
- if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
3459
- if (typeof rawValue === "object") {
3460
- const entity = rawValue;
3461
- const atId = entity["@id"];
3462
- if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
3463
- const idValue = entity["id"];
3464
- const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
3465
- return buildIRI(field.url ?? "", resolvedId);
3466
- }
3467
- return buildIRI(field.url ?? "", String(rawValue));
3468
- },
3469
- normalizeEntityValue(rawValue, field) {
3470
- if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
3471
- if (typeof rawValue === "object" && rawValue !== null) {
3472
- const entity = rawValue;
3473
- const atId = entity["@id"];
3474
- if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
3475
- const directValue = entity[field.valueField];
3476
- if (directValue !== void 0 && directValue !== null) return directValue;
3477
- }
3478
- return rawValue;
3479
- },
3480
- getEntityOptionKey(item, field) {
3481
- if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
3482
- return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
3483
- },
3484
- parseListResponse(response) {
3485
- const r = response;
3486
- const member = r["hydra:member"];
3487
- if (Array.isArray(member)) return {
3488
- items: member,
3489
- total: Number(r["hydra:totalItems"] ?? member.length)
3490
- };
3491
- if (Array.isArray(response)) return {
3492
- items: response,
3493
- total: response.length
3494
- };
3495
- return {
3496
- items: [],
3497
- total: 0
3498
- };
3499
- },
3500
- synthesizeEntityKey(field, entityValue) {
3501
- if (!field.url) return void 0;
3502
- const base = trimTrailingSlash(field.url);
3503
- const directId = entityValue["id"];
3504
- if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
3505
- const directValue = entityValue[field.valueField];
3506
- if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
3507
- for (const key of [
3508
- "code",
3509
- "uuid",
3510
- "slug"
3511
- ]) {
3512
- const candidate = entityValue[key];
3513
- if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
3514
- }
3515
- }
3516
- };
3517
- //#endregion
3518
4044
  //#region packages/crud/form/FormDataTransform.ts
3519
4045
  function upsertPrependData(store, field, item) {
3520
4046
  const existing = store.get(field.name);
@@ -3816,59 +4342,6 @@ function buildFieldColSpanContext(options) {
3816
4342
  };
3817
4343
  }
3818
4344
  //#endregion
3819
- //#region packages/crud/form/serializeFormData.ts
3820
- function applySerializedValue(formData, field, result) {
3821
- if (result.kind === "set") formData[field.name] = result.value;
3822
- else if (result.kind === "omit") delete formData[field.name];
3823
- }
3824
- /**
3825
- * Pure serialization of form data before HTTP submission.
3826
- *
3827
- * Applies uploaded file references and computed fields, then delegates the
3828
- * per-type wire format (entity refs, business dates, numeric coercion, file
3829
- * handling, NONE stripping) to each field's Field-Type module.
3830
- *
3831
- * This function is extracted from useFormSubmit for testability.
3832
- */
3833
- function serializeFormFields(rawData, fields, ctx) {
3834
- const formData = { ...rawData };
3835
- ctx.uploadedFiles.forEach((file) => {
3836
- formData[file.name] = file.iri;
3837
- });
3838
- fields.forEach((field) => {
3839
- if (!field.computed) return;
3840
- const computedValue = field.computed(formData);
3841
- if (computedValue !== void 0) formData[field.name] = computedValue;
3842
- });
3843
- const moduleCtx = {
3844
- adapter: ctx.adapter ?? HydraAdapter,
3845
- format: ctx.format,
3846
- getFieldValue: ctx.getFieldValue
3847
- };
3848
- fields.forEach((field) => {
3849
- applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
3850
- });
3851
- return formData;
3852
- }
3853
- /**
3854
- * Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
3855
- * Pure function — operates on an array of row records.
3856
- */
3857
- function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
3858
- const details = JSON.parse(JSON.stringify(rows));
3859
- detailFields.forEach((field) => {
3860
- const typeModule = getFieldTypeModule(field.type);
3861
- details.forEach((detail) => {
3862
- applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
3863
- });
3864
- });
3865
- details.forEach((detail) => {
3866
- if (!isEditMode) delete detail[detailIdField];
3867
- else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
3868
- });
3869
- return details;
3870
- }
3871
- //#endregion
3872
4345
  //#region packages/crud/form/useFormSubmit.ts
3873
4346
  /**
3874
4347
  * Returns helpers for form serialization and HTTP submit/delete operations.
@@ -5276,7 +5749,11 @@ function usePermissions(resource, supportedOperations = []) {
5276
5749
  canExport: resolve(p?.canExport, false),
5277
5750
  canBulkDelete: resolve(p?.canBulkDelete, false)
5278
5751
  };
5279
- }, [resource.id, opsKey]);
5752
+ }, [
5753
+ resource.id,
5754
+ opsKey,
5755
+ resource.permissions
5756
+ ]);
5280
5757
  }
5281
5758
  //#endregion
5282
5759
  //#region packages/crud/crud/useSelectionState.ts
@@ -6125,7 +6602,7 @@ function buildRoutingFilterRules(fields, initialFilters) {
6125
6602
  * Public export — wraps CrudPageInner in its own DialogStoreProvider so that
6126
6603
  * existing pages do not need to add a provider themselves.
6127
6604
  */
6128
- const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, onOperationChange }) => /* @__PURE__ */ jsx(DialogStoreProvider, { children: /* @__PURE__ */ jsx(CrudPageInner, {
6605
+ const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid, onOperationChange }) => /* @__PURE__ */ jsx(DialogStoreProvider, { children: /* @__PURE__ */ jsx(CrudPageInner, {
6129
6606
  resource,
6130
6607
  onFormDataChange,
6131
6608
  initialRecordId,
@@ -6138,9 +6615,10 @@ const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, c
6138
6615
  editDisabled,
6139
6616
  deleteDisabled,
6140
6617
  gridRef,
6618
+ aboveGrid,
6141
6619
  onOperationChange
6142
6620
  }) });
6143
- 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 }) => {
6621
+ 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 }) => {
6144
6622
  const { t } = useCoreTranslation();
6145
6623
  const { events, resource: resolvedResource, fields, formFields, formRef, permissions, selectionState, presetState } = useCrudPage(useMemo(() => resolveCrudResource(resource), [resource]), externalFormRef);
6146
6624
  const datagridFields = useMemo(() => fields.filter((field) => field.isIdentity || field.visible !== false), [fields]);
@@ -6403,6 +6881,16 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6403
6881
  resolvedResource.rowActions,
6404
6882
  t
6405
6883
  ]);
6884
+ const aboveGridSlot = aboveGridOverride ?? resolvedResource.aboveGrid;
6885
+ const aboveGridContent = (() => {
6886
+ if (!aboveGridSlot) return;
6887
+ if (typeof aboveGridSlot === "function") return aboveGridSlot({
6888
+ resource: resolvedResource,
6889
+ gridRef,
6890
+ refresh: () => gridRef.current?.refresh()
6891
+ });
6892
+ return aboveGridSlot;
6893
+ })();
6406
6894
  /** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
6407
6895
  const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ jsx(ColumnPresetSelector, {
6408
6896
  resourceId: resolvedResource.id,
@@ -6465,6 +6953,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6465
6953
  editMode: resolvedResource.editMode,
6466
6954
  visibleColumns: presetState.visibleColumns,
6467
6955
  beforeToolbar: renderPresetSelector,
6956
+ aboveGrid: aboveGridContent,
6468
6957
  detailUrl: gridDetail?.url,
6469
6958
  detailFields: gridDetail?.fields,
6470
6959
  onFilterChange: onFiltersChange,
@@ -7586,7 +8075,7 @@ function formatRuntimeErrorMessage(error) {
7586
8075
  *
7587
8076
  * URL deep-linking is wired via `initialRecordId` / `initialIsNew` props on CrudPage.
7588
8077
  */
7589
- function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef }) {
8078
+ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid }) {
7590
8079
  const queryClient = useQueryClient();
7591
8080
  const internalGridRef = useRef(null);
7592
8081
  const effectiveGridRef = gridRef ?? internalGridRef;
@@ -7671,6 +8160,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7671
8160
  editDisabled,
7672
8161
  deleteDisabled,
7673
8162
  gridRef: effectiveGridRef,
8163
+ aboveGrid: aboveGrid ?? resource.aboveGrid,
7674
8164
  onOperationChange: (operation) => {
7675
8165
  if (operation === "create") {
7676
8166
  startCreate();