@nubitio/crud 0.5.24 → 0.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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,27 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2731
3261
  })
2732
3262
  ]
2733
3263
  }),
3264
+ options.editMode === "batch" && inlineEdit.draftRows.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3265
+ className: "nb-datagrid__batch-bar",
3266
+ role: "status",
3267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3268
+ className: "nb-datagrid__batch-bar-label",
3269
+ children: t("grid.inlineUnsavedRows", { count: inlineEdit.draftRows.size })
3270
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3271
+ className: "nb-datagrid__batch-bar-actions",
3272
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3273
+ type: "button",
3274
+ className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--discard",
3275
+ onClick: () => inlineEdit.discardAll(),
3276
+ children: t("grid.inlineDiscardAll")
3277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3278
+ type: "button",
3279
+ className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--save",
3280
+ onClick: () => void inlineEdit.saveAll(),
3281
+ children: t("grid.inlineSaveAll", { count: inlineEdit.draftRows.size })
3282
+ })]
3283
+ })]
3284
+ }),
2734
3285
  options.aboveGrid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2735
3286
  className: "nb-datagrid__above-grid",
2736
3287
  children: options.aboveGrid
@@ -3024,16 +3575,33 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3024
3575
  }) }) : rows.map((row, rowIndex) => {
3025
3576
  const key = row[idField] ?? rowIndex;
3026
3577
  const selected = selectedKeys.includes(key);
3578
+ const editing = inlineEdit.draftRows.has(key);
3579
+ const saving = inlineEdit.savingRows.has(key);
3580
+ const rowDraft = inlineEdit.draftRows.get(key) ?? row;
3581
+ const rowFieldErrors = inlineEdit.rowErrors.get(key);
3027
3582
  const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
3028
3583
  const detailUrl = options.detailUrl?.replace("{id}", String(key));
3029
3584
  const expanded = expandedKeys.has(key);
3030
3585
  const rowActions = buildRowActions(row);
3031
3586
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
3032
- className: `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(" "),
3033
3594
  tabIndex: 0,
3034
3595
  "aria-selected": selected,
3035
- onClick: () => selectRow(row),
3596
+ onClick: () => {
3597
+ if (!editing) selectRow(row);
3598
+ },
3036
3599
  onDoubleClick: () => {
3600
+ if (editing) return;
3601
+ if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
3602
+ inlineEdit.startEdit(row);
3603
+ return;
3604
+ }
3037
3605
  if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
3038
3606
  if (options.onEdit) options.onEdit(row);
3039
3607
  else emit(options.events.EDIT, { row });
@@ -3042,10 +3610,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3042
3610
  if (options.allowView && options.onView) options.onView(row);
3043
3611
  },
3044
3612
  onKeyDown: (event) => {
3045
- if (event.key === "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);
3046
3617
  else emit(options.events.EDIT, { row });
3047
- else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3048
- else if (event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3618
+ else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3619
+ else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3049
3620
  else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
3050
3621
  event.preventDefault();
3051
3622
  const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
@@ -3090,9 +3661,29 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3090
3661
  })
3091
3662
  }),
3092
3663
  visibleFields.map((field, columnIndex) => {
3664
+ const width = getColumnWidth(field, resolvedColWidths);
3665
+ if (editing && canEditFieldInline(field)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3666
+ style: {
3667
+ width,
3668
+ textAlign: field.align
3669
+ },
3670
+ className: "nb-datagrid__edit-cell",
3671
+ onClick: (e) => e.stopPropagation(),
3672
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineEditCell, {
3673
+ field,
3674
+ rowKey: key,
3675
+ draft: rowDraft,
3676
+ onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
3677
+ errors: rowFieldErrors,
3678
+ disabled: saving,
3679
+ allRemoteOptions: filterRemoteOptions,
3680
+ httpClient,
3681
+ t
3682
+ })
3683
+ }, field.name);
3093
3684
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3094
3685
  style: {
3095
- width: getColumnWidth(field, resolvedColWidths),
3686
+ width,
3096
3687
  textAlign: field.align
3097
3688
  },
3098
3689
  title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
@@ -3102,7 +3693,32 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3102
3693
  hasRowActions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3103
3694
  className: "nb-datagrid__actions-cell",
3104
3695
  onClick: (e) => e.stopPropagation(),
3105
- children: /* @__PURE__ */ (0, react_jsx_runtime.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", {
3106
3722
  className: "nb-datagrid__row-actions",
3107
3723
  children: rowActions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
3108
3724
  icon: "ph ph-dots-three-vertical",
@@ -3452,97 +4068,6 @@ const FORM_EVENTS = {
3452
4068
  };
3453
4069
  const FORM_ERRORS_EVENT = "form-errors";
3454
4070
  //#endregion
3455
- //#region packages/crud/adapter/HydraAdapter.ts
3456
- function trimTrailingSlash(value) {
3457
- return value.replace(/\/+$/, "");
3458
- }
3459
- function buildIRI(url, value) {
3460
- if (value.startsWith("/")) return value;
3461
- return `${url}/${value}`;
3462
- }
3463
- /**
3464
- * Default backend adapter for API Platform / JSON-LD + Hydra backends.
3465
- *
3466
- * Conventions assumed:
3467
- * - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
3468
- * - Entity fields are serialized as IRI strings in POST/PATCH bodies.
3469
- * - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
3470
- * - `_iri` is a synthetic alias for `@id` used internally by the engine.
3471
- */
3472
- const HydraAdapter = {
3473
- getRowId(record, idField) {
3474
- const direct = record[idField];
3475
- if (direct !== void 0 && direct !== null) return String(direct);
3476
- const iri = record["@id"] ?? record["_iri"];
3477
- if (iri !== void 0 && iri !== null) return String(iri);
3478
- return String(record["id"] ?? "");
3479
- },
3480
- buildItemUrl(baseUrl, id) {
3481
- const str = String(id);
3482
- if (str.startsWith("/")) return str;
3483
- return `${trimTrailingSlash(baseUrl)}/${str}`;
3484
- },
3485
- serializeEntityRef(field, rawValue) {
3486
- if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
3487
- if (typeof rawValue === "object") {
3488
- const entity = rawValue;
3489
- const atId = entity["@id"];
3490
- if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
3491
- const idValue = entity["id"];
3492
- const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
3493
- return buildIRI(field.url ?? "", resolvedId);
3494
- }
3495
- return buildIRI(field.url ?? "", String(rawValue));
3496
- },
3497
- normalizeEntityValue(rawValue, field) {
3498
- if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
3499
- if (typeof rawValue === "object" && rawValue !== null) {
3500
- const entity = rawValue;
3501
- const atId = entity["@id"];
3502
- if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
3503
- const directValue = entity[field.valueField];
3504
- if (directValue !== void 0 && directValue !== null) return directValue;
3505
- }
3506
- return rawValue;
3507
- },
3508
- getEntityOptionKey(item, field) {
3509
- if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
3510
- return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
3511
- },
3512
- parseListResponse(response) {
3513
- const r = response;
3514
- const member = r["hydra:member"];
3515
- if (Array.isArray(member)) return {
3516
- items: member,
3517
- total: Number(r["hydra:totalItems"] ?? member.length)
3518
- };
3519
- if (Array.isArray(response)) return {
3520
- items: response,
3521
- total: response.length
3522
- };
3523
- return {
3524
- items: [],
3525
- total: 0
3526
- };
3527
- },
3528
- synthesizeEntityKey(field, entityValue) {
3529
- if (!field.url) return void 0;
3530
- const base = trimTrailingSlash(field.url);
3531
- const directId = entityValue["id"];
3532
- if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
3533
- const directValue = entityValue[field.valueField];
3534
- if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
3535
- for (const key of [
3536
- "code",
3537
- "uuid",
3538
- "slug"
3539
- ]) {
3540
- const candidate = entityValue[key];
3541
- if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
3542
- }
3543
- }
3544
- };
3545
- //#endregion
3546
4071
  //#region packages/crud/form/FormDataTransform.ts
3547
4072
  function upsertPrependData(store, field, item) {
3548
4073
  const existing = store.get(field.name);
@@ -3844,59 +4369,6 @@ function buildFieldColSpanContext(options) {
3844
4369
  };
3845
4370
  }
3846
4371
  //#endregion
3847
- //#region packages/crud/form/serializeFormData.ts
3848
- function applySerializedValue(formData, field, result) {
3849
- if (result.kind === "set") formData[field.name] = result.value;
3850
- else if (result.kind === "omit") delete formData[field.name];
3851
- }
3852
- /**
3853
- * Pure serialization of form data before HTTP submission.
3854
- *
3855
- * Applies uploaded file references and computed fields, then delegates the
3856
- * per-type wire format (entity refs, business dates, numeric coercion, file
3857
- * handling, NONE stripping) to each field's Field-Type module.
3858
- *
3859
- * This function is extracted from useFormSubmit for testability.
3860
- */
3861
- function serializeFormFields(rawData, fields, ctx) {
3862
- const formData = { ...rawData };
3863
- ctx.uploadedFiles.forEach((file) => {
3864
- formData[file.name] = file.iri;
3865
- });
3866
- fields.forEach((field) => {
3867
- if (!field.computed) return;
3868
- const computedValue = field.computed(formData);
3869
- if (computedValue !== void 0) formData[field.name] = computedValue;
3870
- });
3871
- const moduleCtx = {
3872
- adapter: ctx.adapter ?? HydraAdapter,
3873
- format: ctx.format,
3874
- getFieldValue: ctx.getFieldValue
3875
- };
3876
- fields.forEach((field) => {
3877
- applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
3878
- });
3879
- return formData;
3880
- }
3881
- /**
3882
- * Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
3883
- * Pure function — operates on an array of row records.
3884
- */
3885
- function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
3886
- const details = JSON.parse(JSON.stringify(rows));
3887
- detailFields.forEach((field) => {
3888
- const typeModule = getFieldTypeModule(field.type);
3889
- details.forEach((detail) => {
3890
- applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
3891
- });
3892
- });
3893
- details.forEach((detail) => {
3894
- if (!isEditMode) delete detail[detailIdField];
3895
- else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
3896
- });
3897
- return details;
3898
- }
3899
- //#endregion
3900
4372
  //#region packages/crud/form/useFormSubmit.ts
3901
4373
  /**
3902
4374
  * Returns helpers for form serialization and HTTP submit/delete operations.
@@ -4079,30 +4551,30 @@ function mapApiViolations(violations, detailPropertyName = "items", defaultMessa
4079
4551
  * the state seam, independently testable without the 900-line closure.
4080
4552
  */
4081
4553
  function useFormState({ fields, onFieldDataChanged }) {
4082
- const isEdit = (0, react.useRef)(false);
4083
- const uploadedFiles = (0, react.useRef)([]);
4084
- const existingMediaByField = (0, react.useRef)({});
4085
- const upsertUploadedFile = (0, react.useCallback)((entry) => {
4554
+ const isEdit = (0, react$1.useRef)(false);
4555
+ const uploadedFiles = (0, react$1.useRef)([]);
4556
+ const existingMediaByField = (0, react$1.useRef)({});
4557
+ const upsertUploadedFile = (0, react$1.useCallback)((entry) => {
4086
4558
  uploadedFiles.current = [...uploadedFiles.current.filter((file) => file.name !== entry.name), entry];
4087
4559
  }, []);
4088
- const [formData, setFormData] = (0, react.useState)(() => buildEmptyRow(fields));
4089
- const formDataRef = (0, react.useRef)(formData);
4090
- const [detailRows, setDetailRows] = (0, react.useState)([]);
4091
- const detailRowsRef = (0, react.useRef)(detailRows);
4092
- const [fieldState, setFieldState] = (0, react.useState)({});
4093
- const [errors, setErrors] = (0, react.useState)({});
4094
- const [detailErrors, setDetailErrors] = (0, react.useState)({});
4095
- const prependDataRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4096
- const setNextFormData = (0, react.useCallback)((nextData) => {
4560
+ const [formData, setFormData] = (0, react$1.useState)(() => buildEmptyRow(fields));
4561
+ const formDataRef = (0, react$1.useRef)(formData);
4562
+ const [detailRows, setDetailRows] = (0, react$1.useState)([]);
4563
+ const detailRowsRef = (0, react$1.useRef)(detailRows);
4564
+ const [fieldState, setFieldState] = (0, react$1.useState)({});
4565
+ const [errors, setErrors] = (0, react$1.useState)({});
4566
+ const [detailErrors, setDetailErrors] = (0, react$1.useState)({});
4567
+ const prependDataRef = (0, react$1.useRef)(/* @__PURE__ */ new Map());
4568
+ const setNextFormData = (0, react$1.useCallback)((nextData) => {
4097
4569
  formDataRef.current = nextData;
4098
4570
  setFormData(nextData);
4099
4571
  onFieldDataChanged?.(nextData);
4100
4572
  }, [onFieldDataChanged]);
4101
- const setNextDetailRows = (0, react.useCallback)((nextRows) => {
4573
+ const setNextDetailRows = (0, react$1.useCallback)((nextRows) => {
4102
4574
  detailRowsRef.current = nextRows;
4103
4575
  setDetailRows(nextRows);
4104
4576
  }, []);
4105
- const setFieldValue = (0, react.useCallback)((name, value) => {
4577
+ const setFieldValue = (0, react$1.useCallback)((name, value) => {
4106
4578
  const field = fields.find((candidate) => candidate.name === name);
4107
4579
  const nextData = {
4108
4580
  ...formDataRef.current,
@@ -4117,22 +4589,22 @@ function useFormState({ fields, onFieldDataChanged }) {
4117
4589
  onFieldDataChanged?.(nextData);
4118
4590
  (field?.onChange)?.(value);
4119
4591
  }, [fields, onFieldDataChanged]);
4120
- const setEditMode = (0, react.useCallback)((value) => {
4592
+ const setEditMode = (0, react$1.useCallback)((value) => {
4121
4593
  isEdit.current = value;
4122
4594
  }, []);
4123
- const resetUploadSession = (0, react.useCallback)(() => {
4595
+ const resetUploadSession = (0, react$1.useCallback)(() => {
4124
4596
  uploadedFiles.current = [];
4125
4597
  }, []);
4126
- const setExistingMedia = (0, react.useCallback)((media) => {
4598
+ const setExistingMedia = (0, react$1.useCallback)((media) => {
4127
4599
  existingMediaByField.current = media;
4128
4600
  }, []);
4129
- const clearExistingMedia = (0, react.useCallback)((name) => {
4601
+ const clearExistingMedia = (0, react$1.useCallback)((name) => {
4130
4602
  delete existingMediaByField.current[name];
4131
4603
  }, []);
4132
- const resetPrependData = (0, react.useCallback)(() => {
4604
+ const resetPrependData = (0, react$1.useCallback)(() => {
4133
4605
  prependDataRef.current = /* @__PURE__ */ new Map();
4134
4606
  }, []);
4135
- const clearDetailCellError = (0, react.useCallback)((rowIndex, fieldName) => {
4607
+ const clearDetailCellError = (0, react$1.useCallback)((rowIndex, fieldName) => {
4136
4608
  setDetailErrors((current) => {
4137
4609
  const rowErrors = current[rowIndex];
4138
4610
  if (!rowErrors?.[fieldName]) return current;
@@ -4144,10 +4616,10 @@ function useFormState({ fields, onFieldDataChanged }) {
4144
4616
  return next;
4145
4617
  });
4146
4618
  }, []);
4147
- (0, react.useEffect)(() => {
4619
+ (0, react$1.useEffect)(() => {
4148
4620
  formDataRef.current = formData;
4149
4621
  }, [formData]);
4150
- (0, react.useEffect)(() => {
4622
+ (0, react$1.useEffect)(() => {
4151
4623
  detailRowsRef.current = detailRows;
4152
4624
  }, [detailRows]);
4153
4625
  return {
@@ -5288,7 +5760,7 @@ function fromOperations(supportedOperations) {
5288
5760
  */
5289
5761
  function usePermissions(resource, supportedOperations = []) {
5290
5762
  const opsKey = supportedOperations.slice().sort().join(",");
5291
- return (0, react.useMemo)(() => {
5763
+ return (0, react$1.useMemo)(() => {
5292
5764
  const p = resource.permissions;
5293
5765
  const inferred = fromOperations(supportedOperations);
5294
5766
  function resolveWithInferred(permValue, inferredValue, platformDefault) {
@@ -5313,15 +5785,15 @@ function usePermissions(resource, supportedOperations = []) {
5313
5785
  //#endregion
5314
5786
  //#region packages/crud/crud/useSelectionState.ts
5315
5787
  function useSelectionState(identityField) {
5316
- const [selectedIds, setSelectedIds] = (0, react.useState)([]);
5317
- const onSelectionChanged = (0, react.useCallback)((selectedRows) => {
5788
+ const [selectedIds, setSelectedIds] = (0, react$1.useState)([]);
5789
+ const onSelectionChanged = (0, react$1.useCallback)((selectedRows) => {
5318
5790
  setSelectedIds(selectedRows.map((row) => {
5319
5791
  const val = row[identityField];
5320
5792
  if (typeof val === "string" || typeof val === "number") return val;
5321
5793
  return String(val);
5322
5794
  }));
5323
5795
  }, [identityField]);
5324
- const clearSelection = (0, react.useCallback)(() => {
5796
+ const clearSelection = (0, react$1.useCallback)(() => {
5325
5797
  setSelectedIds([]);
5326
5798
  }, []);
5327
5799
  return {
@@ -5352,7 +5824,7 @@ function resolveVisibleColumns(resource, activeKey) {
5352
5824
  return resource.columnPresets.find((p) => p.key === activeKey)?.columns ?? null;
5353
5825
  }
5354
5826
  function useColumnPreset(resource) {
5355
- const [activePreset, setActivePresetState] = (0, react.useState)(() => {
5827
+ const [activePreset, setActivePresetState] = (0, react$1.useState)(() => {
5356
5828
  if (!resource.columnPresets?.length) return null;
5357
5829
  const stored = readFromStorage(resource.id);
5358
5830
  if (stored && resource.columnPresets.some((p) => p.key === stored)) return stored;
@@ -5360,7 +5832,7 @@ function useColumnPreset(resource) {
5360
5832
  });
5361
5833
  return {
5362
5834
  activePreset,
5363
- setPreset: (0, react.useCallback)((key) => {
5835
+ setPreset: (0, react$1.useCallback)((key) => {
5364
5836
  writeToStorage(resource.id, key);
5365
5837
  setActivePresetState(key);
5366
5838
  }, [resource.id]),
@@ -5681,19 +6153,19 @@ function buildFields(items) {
5681
6153
  //#endregion
5682
6154
  //#region packages/crud/crud/useCrudPage.ts
5683
6155
  function useCrudPage(resource, externalFormRef) {
5684
- const resolvedResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
6156
+ const resolvedResource = (0, react$1.useMemo)(() => resolveCrudResource(resource), [resource]);
5685
6157
  const events = resolvedResource.events;
5686
- const _internalFormRef = (0, react.useRef)(null);
6158
+ const _internalFormRef = (0, react$1.useRef)(null);
5687
6159
  const formRef = externalFormRef ?? _internalFormRef;
5688
- const fields = (0, react.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
6160
+ const fields = (0, react$1.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
5689
6161
  return {
5690
6162
  events,
5691
6163
  resource: resolvedResource,
5692
6164
  fields,
5693
- formFields: (0, react.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
6165
+ formFields: (0, react$1.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
5694
6166
  formRef,
5695
6167
  permissions: usePermissions(resolvedResource, resolvedResource._supportedOperations ?? []),
5696
- selectionState: useSelectionState((0, react.useMemo)(() => {
6168
+ selectionState: useSelectionState((0, react$1.useMemo)(() => {
5697
6169
  return fields.find((f) => f.isIdentity)?.name ?? "id";
5698
6170
  }, [fields])),
5699
6171
  presetState: useColumnPreset(resolvedResource)
@@ -6052,7 +6524,7 @@ function useDialogStoreContext() {
6052
6524
  function useCrudDialogStore(resourceId) {
6053
6525
  const { state, dispatch } = useDialogStoreContext();
6054
6526
  const dialogState = state[resourceId] ?? initialDialogState();
6055
- const openDialog = (0, react.useCallback)((mode, rowData = null) => {
6527
+ const openDialog = (0, react$1.useCallback)((mode, rowData = null) => {
6056
6528
  dispatch({
6057
6529
  type: "OPEN",
6058
6530
  resourceId,
@@ -6060,7 +6532,7 @@ function useCrudDialogStore(resourceId) {
6060
6532
  rowData
6061
6533
  });
6062
6534
  }, [dispatch, resourceId]);
6063
- const closeDialog = (0, react.useCallback)(() => {
6535
+ const closeDialog = (0, react$1.useCallback)(() => {
6064
6536
  dispatch({
6065
6537
  type: "CLOSE",
6066
6538
  resourceId
@@ -6659,7 +7131,7 @@ function useRouting(routing) {
6659
7131
  return {
6660
7132
  initialRecordId,
6661
7133
  initialIsNew,
6662
- initialFilters: (0, react.useMemo)(() => {
7134
+ initialFilters: (0, react$1.useMemo)(() => {
6663
7135
  if (!syncFiltersToUrl) return NO_FILTERS;
6664
7136
  const filters = {};
6665
7137
  searchParams.forEach((value, key) => {
@@ -6667,7 +7139,7 @@ function useRouting(routing) {
6667
7139
  });
6668
7140
  return filters;
6669
7141
  }, [syncFiltersToUrl, searchParams]),
6670
- syncFilters: (0, react.useCallback)((filters) => {
7142
+ syncFilters: (0, react$1.useCallback)((filters) => {
6671
7143
  if (!routing?.syncFiltersToUrl) return;
6672
7144
  const next = new URLSearchParams();
6673
7145
  Object.entries(filters).forEach(([key, value]) => {
@@ -6778,23 +7250,23 @@ function crudReducer(state, action) {
6778
7250
  */
6779
7251
  function useSmartCrudOperation(events, routingState) {
6780
7252
  const [on] = (0, _nubitio_core.useEvents)();
6781
- const [{ activeOperation, formData }, dispatchState] = (0, react.useReducer)(crudReducer, routingState, stateFromRouting);
6782
- const handleFormDataChange = (0, react.useCallback)((data) => {
7253
+ const [{ activeOperation, formData }, dispatchState] = (0, react$1.useReducer)(crudReducer, routingState, stateFromRouting);
7254
+ const handleFormDataChange = (0, react$1.useCallback)((data) => {
6783
7255
  dispatchState({
6784
7256
  type: "set-form-data",
6785
7257
  data
6786
7258
  });
6787
7259
  }, []);
6788
- const startCreate = (0, react.useCallback)(() => {
7260
+ const startCreate = (0, react$1.useCallback)(() => {
6789
7261
  dispatchState({ type: "create" });
6790
7262
  }, []);
6791
- const startEdit = (0, react.useCallback)(() => {
7263
+ const startEdit = (0, react$1.useCallback)(() => {
6792
7264
  dispatchState({ type: "edit" });
6793
7265
  }, []);
6794
- const resetOperation = (0, react.useCallback)(() => {
7266
+ const resetOperation = (0, react$1.useCallback)(() => {
6795
7267
  dispatchState({ type: "reset" });
6796
7268
  }, []);
6797
- (0, react.useEffect)(() => {
7269
+ (0, react$1.useEffect)(() => {
6798
7270
  dispatchState({
6799
7271
  type: "sync-routing",
6800
7272
  routingState: {
@@ -6803,7 +7275,7 @@ function useSmartCrudOperation(events, routingState) {
6803
7275
  }
6804
7276
  });
6805
7277
  }, [routingState.initialIsNew, routingState.initialRecordId]);
6806
- (0, react.useEffect)(() => {
7278
+ (0, react$1.useEffect)(() => {
6807
7279
  const subscriptions = [];
6808
7280
  if (events?.ADD) subscriptions.push(on(events.ADD, () => {
6809
7281
  dispatchState({ type: "create" });
@@ -6849,7 +7321,7 @@ function useSmartCrudOperation(events, routingState) {
6849
7321
  * so grids and forms can resolve row keys consistently.
6850
7322
  */
6851
7323
  function useFieldPermissions(fields, userRoles) {
6852
- return (0, react.useMemo)(() => {
7324
+ return (0, react$1.useMemo)(() => {
6853
7325
  return fields.reduce((acc, field) => {
6854
7326
  if (field.isIdentity) {
6855
7327
  acc.push(field);
@@ -6919,7 +7391,7 @@ function evaluateConditionalRuleState(field, formData) {
6919
7391
  * are returned. Empty objects `{}` are treated as valid create-form state.
6920
7392
  */
6921
7393
  function useConditionalRules(fields, formData) {
6922
- return (0, react.useMemo)(() => {
7394
+ return (0, react$1.useMemo)(() => {
6923
7395
  return fields.map((field) => evaluateConditionalRuleState(field, formData));
6924
7396
  }, [fields, formData]);
6925
7397
  }
@@ -6954,9 +7426,9 @@ function useDependsOn(fields, formData) {
6954
7426
  url: field.url ?? null,
6955
7427
  values: (field.dependsOn ?? []).map((dep) => formData[dep])
6956
7428
  }));
6957
- const isMounted = (0, react.useRef)(false);
6958
- const previousEntriesRef = (0, react.useRef)(null);
6959
- (0, react.useEffect)(() => {
7429
+ const isMounted = (0, react$1.useRef)(false);
7430
+ const previousEntriesRef = (0, react$1.useRef)(null);
7431
+ (0, react$1.useEffect)(() => {
6960
7432
  if (!isMounted.current) {
6961
7433
  isMounted.current = true;
6962
7434
  previousEntriesRef.current = currentEntries;
@@ -7138,7 +7610,7 @@ function applyFieldOperationSemantics(field, operation, behavior, owner = `Field
7138
7610
  * from schema loading and routing concerns.
7139
7611
  */
7140
7612
  function useSmartCrudFields(fields, activeOperation, formData, roles) {
7141
- const permissionFilteredFields = useFieldPermissions((0, react.useMemo)(() => {
7613
+ const permissionFilteredFields = useFieldPermissions((0, react$1.useMemo)(() => {
7142
7614
  if (!activeOperation) return fields;
7143
7615
  return fields.map((field) => {
7144
7616
  const runtimeField = field;
@@ -7150,7 +7622,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7150
7622
  useDependsOn(permissionFilteredFields, formData);
7151
7623
  return {
7152
7624
  gridFields: permissionFilteredFields,
7153
- processedFields: (0, react.useMemo)(() => {
7625
+ processedFields: (0, react$1.useMemo)(() => {
7154
7626
  const stateByName = new Map(fieldStates.map((s) => [s.name, s]));
7155
7627
  let anyChanged = false;
7156
7628
  const merged = permissionFilteredFields.map((field) => {
@@ -7170,7 +7642,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7170
7642
  });
7171
7643
  return anyChanged ? merged : permissionFilteredFields;
7172
7644
  }, [permissionFilteredFields, fieldStates]),
7173
- computedValues: (0, react.useMemo)(() => fieldStates.reduce((acc, state) => {
7645
+ computedValues: (0, react$1.useMemo)(() => fieldStates.reduce((acc, state) => {
7174
7646
  if (state.computedValue !== void 0) acc[state.name] = state.computedValue;
7175
7647
  return acc;
7176
7648
  }, {}), [fieldStates])