@nubitio/crud 0.5.24 → 0.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,27 @@ 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
+ }),
2710
3258
  options.aboveGrid ? /* @__PURE__ */ jsx("div", {
2711
3259
  className: "nb-datagrid__above-grid",
2712
3260
  children: options.aboveGrid
@@ -3000,16 +3548,33 @@ const NativeDataGridView = forwardRef((options, ref) => {
3000
3548
  }) }) : rows.map((row, rowIndex) => {
3001
3549
  const key = row[idField] ?? rowIndex;
3002
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);
3003
3555
  const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
3004
3556
  const detailUrl = options.detailUrl?.replace("{id}", String(key));
3005
3557
  const expanded = expandedKeys.has(key);
3006
3558
  const rowActions = buildRowActions(row);
3007
3559
  return /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("tr", {
3008
- 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(" "),
3009
3567
  tabIndex: 0,
3010
3568
  "aria-selected": selected,
3011
- onClick: () => selectRow(row),
3569
+ onClick: () => {
3570
+ if (!editing) selectRow(row);
3571
+ },
3012
3572
  onDoubleClick: () => {
3573
+ if (editing) return;
3574
+ if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
3575
+ inlineEdit.startEdit(row);
3576
+ return;
3577
+ }
3013
3578
  if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
3014
3579
  if (options.onEdit) options.onEdit(row);
3015
3580
  else emit(options.events.EDIT, { row });
@@ -3018,10 +3583,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
3018
3583
  if (options.allowView && options.onView) options.onView(row);
3019
3584
  },
3020
3585
  onKeyDown: (event) => {
3021
- 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);
3022
3590
  else emit(options.events.EDIT, { row });
3023
- else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3024
- 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);
3025
3593
  else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
3026
3594
  event.preventDefault();
3027
3595
  const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
@@ -3066,9 +3634,29 @@ const NativeDataGridView = forwardRef((options, ref) => {
3066
3634
  })
3067
3635
  }),
3068
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);
3069
3657
  return /* @__PURE__ */ jsx("td", {
3070
3658
  style: {
3071
- width: getColumnWidth(field, resolvedColWidths),
3659
+ width,
3072
3660
  textAlign: field.align
3073
3661
  },
3074
3662
  title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
@@ -3078,7 +3666,32 @@ const NativeDataGridView = forwardRef((options, ref) => {
3078
3666
  hasRowActions && /* @__PURE__ */ jsx("td", {
3079
3667
  className: "nb-datagrid__actions-cell",
3080
3668
  onClick: (e) => e.stopPropagation(),
3081
- 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", {
3082
3695
  className: "nb-datagrid__row-actions",
3083
3696
  children: rowActions.length > 0 && /* @__PURE__ */ jsx(IconButton, {
3084
3697
  icon: "ph ph-dots-three-vertical",
@@ -3428,97 +4041,6 @@ const FORM_EVENTS = {
3428
4041
  };
3429
4042
  const FORM_ERRORS_EVENT = "form-errors";
3430
4043
  //#endregion
3431
- //#region packages/crud/adapter/HydraAdapter.ts
3432
- function trimTrailingSlash(value) {
3433
- return value.replace(/\/+$/, "");
3434
- }
3435
- function buildIRI(url, value) {
3436
- if (value.startsWith("/")) return value;
3437
- return `${url}/${value}`;
3438
- }
3439
- /**
3440
- * Default backend adapter for API Platform / JSON-LD + Hydra backends.
3441
- *
3442
- * Conventions assumed:
3443
- * - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
3444
- * - Entity fields are serialized as IRI strings in POST/PATCH bodies.
3445
- * - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
3446
- * - `_iri` is a synthetic alias for `@id` used internally by the engine.
3447
- */
3448
- const HydraAdapter = {
3449
- getRowId(record, idField) {
3450
- const direct = record[idField];
3451
- if (direct !== void 0 && direct !== null) return String(direct);
3452
- const iri = record["@id"] ?? record["_iri"];
3453
- if (iri !== void 0 && iri !== null) return String(iri);
3454
- return String(record["id"] ?? "");
3455
- },
3456
- buildItemUrl(baseUrl, id) {
3457
- const str = String(id);
3458
- if (str.startsWith("/")) return str;
3459
- return `${trimTrailingSlash(baseUrl)}/${str}`;
3460
- },
3461
- serializeEntityRef(field, rawValue) {
3462
- if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
3463
- if (typeof rawValue === "object") {
3464
- const entity = rawValue;
3465
- const atId = entity["@id"];
3466
- if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
3467
- const idValue = entity["id"];
3468
- const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
3469
- return buildIRI(field.url ?? "", resolvedId);
3470
- }
3471
- return buildIRI(field.url ?? "", String(rawValue));
3472
- },
3473
- normalizeEntityValue(rawValue, field) {
3474
- if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
3475
- if (typeof rawValue === "object" && rawValue !== null) {
3476
- const entity = rawValue;
3477
- const atId = entity["@id"];
3478
- if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
3479
- const directValue = entity[field.valueField];
3480
- if (directValue !== void 0 && directValue !== null) return directValue;
3481
- }
3482
- return rawValue;
3483
- },
3484
- getEntityOptionKey(item, field) {
3485
- if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
3486
- return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
3487
- },
3488
- parseListResponse(response) {
3489
- const r = response;
3490
- const member = r["hydra:member"];
3491
- if (Array.isArray(member)) return {
3492
- items: member,
3493
- total: Number(r["hydra:totalItems"] ?? member.length)
3494
- };
3495
- if (Array.isArray(response)) return {
3496
- items: response,
3497
- total: response.length
3498
- };
3499
- return {
3500
- items: [],
3501
- total: 0
3502
- };
3503
- },
3504
- synthesizeEntityKey(field, entityValue) {
3505
- if (!field.url) return void 0;
3506
- const base = trimTrailingSlash(field.url);
3507
- const directId = entityValue["id"];
3508
- if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
3509
- const directValue = entityValue[field.valueField];
3510
- if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
3511
- for (const key of [
3512
- "code",
3513
- "uuid",
3514
- "slug"
3515
- ]) {
3516
- const candidate = entityValue[key];
3517
- if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
3518
- }
3519
- }
3520
- };
3521
- //#endregion
3522
4044
  //#region packages/crud/form/FormDataTransform.ts
3523
4045
  function upsertPrependData(store, field, item) {
3524
4046
  const existing = store.get(field.name);
@@ -3820,59 +4342,6 @@ function buildFieldColSpanContext(options) {
3820
4342
  };
3821
4343
  }
3822
4344
  //#endregion
3823
- //#region packages/crud/form/serializeFormData.ts
3824
- function applySerializedValue(formData, field, result) {
3825
- if (result.kind === "set") formData[field.name] = result.value;
3826
- else if (result.kind === "omit") delete formData[field.name];
3827
- }
3828
- /**
3829
- * Pure serialization of form data before HTTP submission.
3830
- *
3831
- * Applies uploaded file references and computed fields, then delegates the
3832
- * per-type wire format (entity refs, business dates, numeric coercion, file
3833
- * handling, NONE stripping) to each field's Field-Type module.
3834
- *
3835
- * This function is extracted from useFormSubmit for testability.
3836
- */
3837
- function serializeFormFields(rawData, fields, ctx) {
3838
- const formData = { ...rawData };
3839
- ctx.uploadedFiles.forEach((file) => {
3840
- formData[file.name] = file.iri;
3841
- });
3842
- fields.forEach((field) => {
3843
- if (!field.computed) return;
3844
- const computedValue = field.computed(formData);
3845
- if (computedValue !== void 0) formData[field.name] = computedValue;
3846
- });
3847
- const moduleCtx = {
3848
- adapter: ctx.adapter ?? HydraAdapter,
3849
- format: ctx.format,
3850
- getFieldValue: ctx.getFieldValue
3851
- };
3852
- fields.forEach((field) => {
3853
- applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
3854
- });
3855
- return formData;
3856
- }
3857
- /**
3858
- * Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
3859
- * Pure function — operates on an array of row records.
3860
- */
3861
- function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
3862
- const details = JSON.parse(JSON.stringify(rows));
3863
- detailFields.forEach((field) => {
3864
- const typeModule = getFieldTypeModule(field.type);
3865
- details.forEach((detail) => {
3866
- applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
3867
- });
3868
- });
3869
- details.forEach((detail) => {
3870
- if (!isEditMode) delete detail[detailIdField];
3871
- else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
3872
- });
3873
- return details;
3874
- }
3875
- //#endregion
3876
4345
  //#region packages/crud/form/useFormSubmit.ts
3877
4346
  /**
3878
4347
  * Returns helpers for form serialization and HTTP submit/delete operations.