@nubitio/crud 0.5.24 → 0.5.27

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,459 @@ 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 valuesEqual$1(a, b) {
2187
+ if (a === b) return true;
2188
+ if (a == null && b == null) return true;
2189
+ if (a == null || b == null) return false;
2190
+ const numA = Number(a);
2191
+ const numB = Number(b);
2192
+ if (!Number.isNaN(numA) && !Number.isNaN(numB)) return numA === numB;
2193
+ return String(a) === String(b);
2194
+ }
2195
+ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError, onBatchSave }) {
2196
+ const [draftRows, setDraftRows] = (0, react$1.useState)(/* @__PURE__ */ new Map());
2197
+ const [savingRows, setSavingRows] = (0, react$1.useState)(/* @__PURE__ */ new Set());
2198
+ const [rowErrors, setRowErrors] = (0, react$1.useState)(/* @__PURE__ */ new Map());
2199
+ const [activeCell, setActiveCell] = (0, react$1.useState)(null);
2200
+ const draftRowsRef = (0, react$1.useRef)(draftRows);
2201
+ draftRowsRef.current = draftRows;
2202
+ const optsRef = (0, react$1.useRef)({
2203
+ url,
2204
+ idField,
2205
+ adapter,
2206
+ httpClient,
2207
+ fields,
2208
+ onSaveSuccess,
2209
+ onSaveError,
2210
+ onBatchSave
2211
+ });
2212
+ optsRef.current = {
2213
+ url,
2214
+ idField,
2215
+ adapter,
2216
+ httpClient,
2217
+ fields,
2218
+ onSaveSuccess,
2219
+ onSaveError,
2220
+ onBatchSave
2221
+ };
2222
+ const isEditing = (0, react$1.useCallback)((key) => draftRows.has(key), [draftRows]);
2223
+ const isCellActive = (0, react$1.useCallback)((key, fieldName) => activeCell != null && activeCell.key === key && activeCell.fieldName === fieldName, [activeCell]);
2224
+ const isCellDirty = (0, react$1.useCallback)((key, fieldName, original) => {
2225
+ const draft = draftRows.get(key);
2226
+ if (!draft) return false;
2227
+ return !valuesEqual$1(draft[fieldName], original[fieldName]);
2228
+ }, [draftRows]);
2229
+ const hasDraftChanges = (0, react$1.useCallback)((key, original) => {
2230
+ const draft = draftRows.get(key);
2231
+ if (!draft) return false;
2232
+ return optsRef.current.fields.some((field) => canEditFieldInline(field) && !valuesEqual$1(draft[field.name], original[field.name]));
2233
+ }, [draftRows]);
2234
+ const startEdit = (0, react$1.useCallback)((row) => {
2235
+ const key = row[optsRef.current.idField];
2236
+ setDraftRows((prev) => {
2237
+ const base = mode === "row" ? [] : Array.from(prev.entries());
2238
+ return new Map([...base, [key, { ...row }]]);
2239
+ });
2240
+ setRowErrors((prev) => {
2241
+ const next = new Map(prev);
2242
+ next.delete(key);
2243
+ return next;
2244
+ });
2245
+ setActiveCell(null);
2246
+ }, [mode]);
2247
+ const startCellEdit = (0, react$1.useCallback)((row, fieldName) => {
2248
+ const key = row[optsRef.current.idField];
2249
+ setDraftRows((prev) => {
2250
+ const existing = prev.get(key);
2251
+ const withoutKey = (mode === "row" ? [] : Array.from(prev.entries())).filter(([entryKey]) => entryKey !== key);
2252
+ return new Map([...withoutKey, [key, existing ? { ...existing } : { ...row }]]);
2253
+ });
2254
+ setRowErrors((prev) => {
2255
+ const next = new Map(prev);
2256
+ next.delete(key);
2257
+ return next;
2258
+ });
2259
+ setActiveCell({
2260
+ key,
2261
+ fieldName
2262
+ });
2263
+ }, [mode]);
2264
+ const stopCellEdit = (0, react$1.useCallback)(() => {
2265
+ setActiveCell(null);
2266
+ }, []);
2267
+ const cancelEdit = (0, react$1.useCallback)((key) => {
2268
+ setDraftRows((prev) => {
2269
+ const next = new Map(prev);
2270
+ next.delete(key);
2271
+ return next;
2272
+ });
2273
+ setRowErrors((prev) => {
2274
+ const next = new Map(prev);
2275
+ next.delete(key);
2276
+ return next;
2277
+ });
2278
+ setActiveCell((current) => current?.key === key ? null : current);
2279
+ }, []);
2280
+ const discardAll = (0, react$1.useCallback)(() => {
2281
+ setDraftRows(/* @__PURE__ */ new Map());
2282
+ setRowErrors(/* @__PURE__ */ new Map());
2283
+ setActiveCell(null);
2284
+ }, []);
2285
+ const updateDraft = (0, react$1.useCallback)((key, fieldName, value) => {
2286
+ setDraftRows((prev) => {
2287
+ const current = prev.get(key);
2288
+ if (!current) return prev;
2289
+ const next = new Map(prev);
2290
+ next.set(key, {
2291
+ ...current,
2292
+ [fieldName]: value
2293
+ });
2294
+ return next;
2295
+ });
2296
+ }, []);
2297
+ const doSaveRow = (0, react$1.useCallback)(async (key) => {
2298
+ const { url: u, adapter: a, httpClient: http, fields: fs, onSaveError: onErr } = optsRef.current;
2299
+ const draft = draftRowsRef.current.get(key);
2300
+ if (!draft) return false;
2301
+ const errors = {};
2302
+ fs.forEach((field) => {
2303
+ if (!canEditFieldInline(field)) return;
2304
+ if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
2305
+ });
2306
+ if (Object.keys(errors).length > 0) {
2307
+ setRowErrors((prev) => new Map(prev).set(key, errors));
2308
+ return false;
2309
+ }
2310
+ setSavingRows((prev) => new Set([...prev, key]));
2311
+ const serialized = serializeFormFields(draft, fs.filter(canEditFieldInline), {
2312
+ uploadedFiles: [],
2313
+ getFieldValue: (name) => draft[name]
2314
+ });
2315
+ try {
2316
+ await http.patch(a.buildItemUrl(u, key), serialized);
2317
+ setDraftRows((prev) => {
2318
+ const next = new Map(prev);
2319
+ next.delete(key);
2320
+ return next;
2321
+ });
2322
+ setRowErrors((prev) => {
2323
+ const next = new Map(prev);
2324
+ next.delete(key);
2325
+ return next;
2326
+ });
2327
+ return true;
2328
+ } catch (err) {
2329
+ onErr?.(key, err);
2330
+ if (typeof err === "object" && err !== null && "status" in err && err.status === 422 && "data" in err) {
2331
+ const data = err.data;
2332
+ if (typeof data === "object" && data !== null && "violations" in data) {
2333
+ const violations = data.violations ?? [];
2334
+ const fieldErrors = {};
2335
+ violations.forEach((v) => {
2336
+ fieldErrors[v.propertyPath] = v.message;
2337
+ });
2338
+ setRowErrors((prev) => new Map(prev).set(key, fieldErrors));
2339
+ }
2340
+ }
2341
+ return false;
2342
+ } finally {
2343
+ setSavingRows((prev) => {
2344
+ const next = new Set(prev);
2345
+ next.delete(key);
2346
+ return next;
2347
+ });
2348
+ }
2349
+ }, []);
2350
+ const validateDraft = (0, react$1.useCallback)((key, draft, fs) => {
2351
+ const errors = {};
2352
+ fs.forEach((field) => {
2353
+ if (!canEditFieldInline(field)) return;
2354
+ if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
2355
+ });
2356
+ if (Object.keys(errors).length === 0) return true;
2357
+ setRowErrors((prev) => new Map(prev).set(key, errors));
2358
+ return false;
2359
+ }, []);
2360
+ const doSaveBatch = (0, react$1.useCallback)(async () => {
2361
+ const { fields: fs, onBatchSave: saveBatch, onSaveError: onErr } = optsRef.current;
2362
+ if (!saveBatch) return false;
2363
+ const entries = Array.from(draftRowsRef.current.entries());
2364
+ if (entries.length === 0) return false;
2365
+ if (!entries.every(([key, draft]) => validateDraft(key, draft, fs))) return false;
2366
+ const keys = entries.map(([key]) => key);
2367
+ setSavingRows((prev) => new Set([...prev, ...keys]));
2368
+ try {
2369
+ await saveBatch(entries.map(([, draft]) => ({ ...draft })));
2370
+ setDraftRows(/* @__PURE__ */ new Map());
2371
+ setRowErrors(/* @__PURE__ */ new Map());
2372
+ return true;
2373
+ } catch (err) {
2374
+ keys.forEach((key) => onErr?.(key, err));
2375
+ return false;
2376
+ } finally {
2377
+ setSavingRows((prev) => {
2378
+ const next = new Set(prev);
2379
+ keys.forEach((key) => next.delete(key));
2380
+ return next;
2381
+ });
2382
+ }
2383
+ }, [validateDraft]);
2384
+ return {
2385
+ draftRows,
2386
+ savingRows,
2387
+ rowErrors,
2388
+ activeCell,
2389
+ isEditing,
2390
+ isCellActive,
2391
+ isCellDirty,
2392
+ hasDraftChanges,
2393
+ startEdit,
2394
+ startCellEdit,
2395
+ stopCellEdit,
2396
+ cancelEdit,
2397
+ discardAll,
2398
+ updateDraft,
2399
+ saveRow: (0, react$1.useCallback)(async (key) => {
2400
+ if (optsRef.current.onBatchSave) {
2401
+ const ok = await doSaveBatch();
2402
+ if (ok) optsRef.current.onSaveSuccess?.();
2403
+ return ok;
2404
+ }
2405
+ const ok = await doSaveRow(key);
2406
+ if (ok) optsRef.current.onSaveSuccess?.();
2407
+ return ok;
2408
+ }, [doSaveBatch, doSaveRow]),
2409
+ saveAll: (0, react$1.useCallback)(async () => {
2410
+ if (optsRef.current.onBatchSave) {
2411
+ const ok = await doSaveBatch();
2412
+ if (ok) optsRef.current.onSaveSuccess?.();
2413
+ return ok;
2414
+ }
2415
+ const keys = Array.from(draftRowsRef.current.keys());
2416
+ const anySuccess = (await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value);
2417
+ if (anySuccess) optsRef.current.onSaveSuccess?.();
2418
+ return anySuccess;
2419
+ }, [doSaveBatch, doSaveRow])
2420
+ };
2421
+ }
2422
+ function focusInlineControl(container) {
2423
+ if (!container) return;
2424
+ const focusable = container.querySelector("input:not([type=\"hidden\"]):not([disabled]):not([readonly]), select:not([disabled]), textarea:not([disabled]), .nb-date-picker__input:not([readonly]), .nb-dropdown__trigger:not([disabled])");
2425
+ focusable?.focus();
2426
+ if (focusable instanceof HTMLInputElement && focusable.type !== "checkbox") focusable.select();
2427
+ }
2428
+ /**
2429
+ * Renders a single cell as an editable control during inline editing.
2430
+ * Uses the same FieldTypeModule.ControlRender path as the full form, with
2431
+ * compact styling so controls fit naturally inside a table cell.
2432
+ */
2433
+ function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t, autoFocus = true }) {
2434
+ const containerRef = (0, react.useRef)(null);
2435
+ const typeModule = getFieldTypeModule(field.type);
2436
+ const fieldError = errors?.[field.name];
2437
+ const errorClass = fieldError ? " is-error" : "";
2438
+ (0, react.useEffect)(() => {
2439
+ if (!autoFocus) return;
2440
+ focusInlineControl(containerRef.current);
2441
+ }, [
2442
+ autoFocus,
2443
+ field.name,
2444
+ rowKey
2445
+ ]);
2446
+ const commonProps = {
2447
+ className: `nb-inline-control${errorClass}`,
2448
+ disabled,
2449
+ id: `iec-${String(rowKey)}-${field.name}`,
2450
+ name: field.name,
2451
+ onClick: void 0,
2452
+ readOnly: false,
2453
+ required: field.required
2454
+ };
2455
+ const ctx = {
2456
+ httpClient,
2457
+ t,
2458
+ remoteOptions: allRemoteOptions,
2459
+ getPrependData: () => void 0,
2460
+ getFieldValue: (name) => draft[name],
2461
+ getExistingMedia: () => null,
2462
+ clearExistingMedia: () => {},
2463
+ upsertUploadedFile: () => {}
2464
+ };
2465
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2466
+ ref: containerRef,
2467
+ className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
2468
+ onKeyDown: (event) => {
2469
+ if (event.key === "Escape") event.stopPropagation();
2470
+ },
2471
+ onMouseDown: (event) => {
2472
+ if (event.target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
2473
+ event.stopPropagation();
2474
+ },
2475
+ onClick: (event) => event.stopPropagation(),
2476
+ children: typeModule.ControlRender({
2477
+ field,
2478
+ value: draft[field.name],
2479
+ error: fieldError,
2480
+ errorClass,
2481
+ disabled,
2482
+ readOnly: false,
2483
+ commonProps,
2484
+ setFieldValue: onChange,
2485
+ ctx
2486
+ })
2487
+ });
2488
+ }
2489
+ //#endregion
1865
2490
  //#region packages/crud/summary/SummaryUtils.ts
1866
2491
  function toFiniteNumber(value) {
1867
2492
  if (value === null || value === void 0 || value === "") return null;
@@ -1931,19 +2556,20 @@ function resolveSummaryText(rows, item) {
1931
2556
  const DETAIL_COL_WIDTH = 36;
1932
2557
  const CHECKBOX_COL_WIDTH = 36;
1933
2558
  const ACTIONS_COL_WIDTH = 44;
2559
+ const INLINE_ACTIONS_COL_WIDTH = 72;
1934
2560
  const DEFAULT_COL_WIDTH = 120;
1935
2561
  const MIN_COL_WIDTH = 48;
1936
2562
  function getColumnWidth(field, colWidths) {
1937
2563
  return colWidths[field.name] ?? field.width ?? field.minWidth ?? DEFAULT_COL_WIDTH;
1938
2564
  }
1939
- function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth }) {
2565
+ function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth, actionsColWidth = ACTIONS_COL_WIDTH }) {
1940
2566
  let total = 0;
1941
2567
  if (hasDetail) total += DETAIL_COL_WIDTH;
1942
2568
  if (hasCheckbox) total += CHECKBOX_COL_WIDTH;
1943
2569
  visibleFields.forEach((field) => {
1944
2570
  total += getColumnWidth(field, colWidths);
1945
2571
  });
1946
- if (hasRowActions) total += ACTIONS_COL_WIDTH;
2572
+ if (hasRowActions) total += actionsColWidth;
1947
2573
  return Math.max(containerWidth, total);
1948
2574
  }
1949
2575
  function GridColumnGroup({ fields, colWidths, hasCheckbox, hasDetail, hasRowActions }) {
@@ -1975,6 +2601,25 @@ function normalizeIcon$1(icon) {
1975
2601
  function isDateLikeField(field) {
1976
2602
  return field.type === "date" || field.type === "datetime";
1977
2603
  }
2604
+ function isCellEditMode(editMode) {
2605
+ return editMode === "cell" || editMode === "batch";
2606
+ }
2607
+ function resolveInlineEditToolbar(editMode, inlineEditToolbar) {
2608
+ if (inlineEditToolbar === false) return null;
2609
+ const defaultShow = isCellEditMode(editMode);
2610
+ if (inlineEditToolbar == null) return defaultShow ? {
2611
+ save: true,
2612
+ revert: true
2613
+ } : null;
2614
+ if (typeof inlineEditToolbar === "boolean") return inlineEditToolbar ? {
2615
+ save: true,
2616
+ revert: true
2617
+ } : null;
2618
+ return {
2619
+ save: inlineEditToolbar.save !== false,
2620
+ revert: inlineEditToolbar.revert !== false
2621
+ };
2622
+ }
1978
2623
  function getToolbarKey(action, index) {
1979
2624
  return action.key ?? action.text ?? String(index);
1980
2625
  }
@@ -2326,9 +2971,20 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2326
2971
  ]);
2327
2972
  const fieldsRef = (0, react.useRef)(options.fields);
2328
2973
  fieldsRef.current = options.fields;
2974
+ const loadSeqRef = (0, react.useRef)(0);
2329
2975
  const loadRows = (0, react.useCallback)(async () => {
2976
+ if (options.data) {
2977
+ rowsRef.current = options.data;
2978
+ setRows(options.data);
2979
+ setTotalCount(options.data.length);
2980
+ setGridSummary(options.gridSummary ?? null);
2981
+ setIsGridLoading(false);
2982
+ onContentReadyRef.current?.();
2983
+ return options.data;
2984
+ }
2330
2985
  if (options.manualLoad) return rowsRef.current;
2331
2986
  setIsGridLoading(true);
2987
+ const seq = ++loadSeqRef.current;
2332
2988
  const loadOptions = {
2333
2989
  filter: buildFilterExpression(filters, filterOperators, fieldsRef.current),
2334
2990
  sort
@@ -2338,6 +2994,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2338
2994
  loadOptions.take = pageSize;
2339
2995
  }
2340
2996
  const result = await source.load(loadOptions);
2997
+ if (seq !== loadSeqRef.current) return rowsRef.current;
2341
2998
  rowsRef.current = result.data;
2342
2999
  setRows(result.data);
2343
3000
  setTotalCount(result.totalCount);
@@ -2348,6 +3005,8 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2348
3005
  }, [
2349
3006
  filterOperators,
2350
3007
  filters,
3008
+ options.data,
3009
+ options.gridSummary,
2351
3010
  options.manualLoad,
2352
3011
  options.paging,
2353
3012
  page,
@@ -2391,19 +3050,58 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2391
3050
  resourceStoreFactory,
2392
3051
  visibleFields
2393
3052
  ]);
3053
+ const canInlineEditMode = options.editMode === "row" || options.editMode === "cell" || options.editMode === "batch";
3054
+ const cellEditMode = isCellEditMode(options.editMode);
3055
+ const rowInlineMode = options.editMode === "row";
3056
+ const inlineEditToolbar = resolveInlineEditToolbar(options.editMode, options.inlineEditToolbar);
3057
+ const showRowInlineActions = options.inlineRowActions ?? rowInlineMode;
3058
+ const inlineEdit = useInlineEdit({
3059
+ mode: cellEditMode ? "batch" : "row",
3060
+ url: options.url,
3061
+ idField,
3062
+ adapter: options.adapter,
3063
+ httpClient,
3064
+ fields: options.fields,
3065
+ onSaveSuccess: () => void loadRows(),
3066
+ onSaveError: () => {},
3067
+ onBatchSave: options.onBatchSave
3068
+ });
3069
+ const dirtyRowCount = (0, react.useMemo)(() => rows.filter((row) => {
3070
+ const key = row[idField] ?? row;
3071
+ return inlineEdit.draftRows.has(key) && inlineEdit.hasDraftChanges(key, row);
3072
+ }).length, [
3073
+ rows,
3074
+ idField,
3075
+ inlineEdit.draftRows,
3076
+ inlineEdit.hasDraftChanges
3077
+ ]);
3078
+ const hasPendingInlineEdits = dirtyRowCount > 0;
3079
+ (0, react.useEffect)(() => {
3080
+ if (!inlineEdit.activeCell) return;
3081
+ const handler = (event) => {
3082
+ const target = event.target;
3083
+ if (target.closest(".nb-datagrid__edit-cell")) return;
3084
+ if (target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
3085
+ inlineEdit.stopCellEdit();
3086
+ };
3087
+ document.addEventListener("mousedown", handler);
3088
+ return () => document.removeEventListener("mousedown", handler);
3089
+ }, [inlineEdit.activeCell, inlineEdit.stopCellEdit]);
2394
3090
  const handleStateRef = (0, react.useRef)({
2395
3091
  selectedKeys,
2396
3092
  filters,
2397
3093
  filterOperators,
2398
3094
  loadRows,
2399
- idField
3095
+ idField,
3096
+ inlineEdit
2400
3097
  });
2401
3098
  handleStateRef.current = {
2402
3099
  selectedKeys,
2403
3100
  filters,
2404
3101
  filterOperators,
2405
3102
  loadRows,
2406
- idField
3103
+ idField,
3104
+ inlineEdit
2407
3105
  };
2408
3106
  (0, react.useImperativeHandle)(ref, () => ({
2409
3107
  showLoading: (message) => {
@@ -2443,7 +3141,9 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2443
3141
  setFilterInputs(nextFilters);
2444
3142
  setFilterOperators(nextOperators);
2445
3143
  setPage(0);
2446
- }
3144
+ },
3145
+ hasEditData: () => handleStateRef.current.inlineEdit.draftRows.size > 0,
3146
+ saveChanges: () => handleStateRef.current.inlineEdit.saveAll()
2447
3147
  }), [options.fields, t]);
2448
3148
  const selectedRows = rows.filter((row) => selectedKeys.includes(row[idField]));
2449
3149
  const selectRow = (row) => {
@@ -2457,7 +3157,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2457
3157
  const rowEditable = (row) => options.canEditRow?.(row) !== false;
2458
3158
  const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
2459
3159
  const buildRowActions = (row) => [
2460
- ...options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT) ? [{
3160
+ ...options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
3161
+ text: t("grid.inlineEditRow"),
3162
+ icon: "ph-pencil-simple",
3163
+ disabled: options.editDisabled,
3164
+ onClick: () => inlineEdit.startEdit(row)
3165
+ }] : [],
3166
+ ...options.allowEdit && rowEditable(row) && !canInlineEditMode && (options.onEdit || options.events?.EDIT) ? [{
2461
3167
  text: t("grid.buttonEdit"),
2462
3168
  icon: "ph-pencil-simple",
2463
3169
  disabled: options.editDisabled,
@@ -2481,6 +3187,10 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2481
3187
  }] : []
2482
3188
  ];
2483
3189
  const openRow = (row) => {
3190
+ if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
3191
+ inlineEdit.startEdit(row);
3192
+ return;
3193
+ }
2484
3194
  if (options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT)) {
2485
3195
  if (options.onEdit) options.onEdit(row);
2486
3196
  else emit(options.events.EDIT, { row });
@@ -2612,28 +3322,41 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2612
3322
  };
2613
3323
  const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
2614
3324
  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));
3325
+ const hasBuiltInRowActions = Boolean(options.allowEdit && (rowInlineMode && showRowInlineActions || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
2616
3326
  const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
2617
3327
  const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
2618
3328
  const hasDetail = Boolean(options.detailFields);
3329
+ const actionsColWidth = canInlineEditMode ? INLINE_ACTIONS_COL_WIDTH : ACTIONS_COL_WIDTH;
2619
3330
  const layoutWidth = computeLayoutWidth({
2620
3331
  visibleFields,
2621
3332
  colWidths,
2622
3333
  hasCheckbox,
2623
3334
  hasDetail,
2624
3335
  hasRowActions,
2625
- containerWidth
3336
+ containerWidth,
3337
+ actionsColWidth
2626
3338
  });
2627
3339
  const resolvedColWidths = (0, react.useMemo)(() => {
2628
- if (visibleFields.length === 0) return colWidths;
2629
- 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);
2631
- const extra = Math.max(0, containerWidth - dataTotal - fixedTotal);
3340
+ if (visibleFields.length === 0 || containerWidth <= 0) return colWidths;
3341
+ const bases = visibleFields.map((f) => getColumnWidth(f, colWidths));
3342
+ const dataTotal = bases.reduce((sum, width) => sum + width, 0);
3343
+ const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
3344
+ const available = Math.max(0, containerWidth - fixedTotal);
3345
+ if (dataTotal > available && available > 0) {
3346
+ const scale = available / dataTotal;
3347
+ const result = {};
3348
+ visibleFields.forEach((field, index) => {
3349
+ const floor = field.minWidth ?? MIN_COL_WIDTH;
3350
+ result[field.name] = Math.max(floor, Math.floor(bases[index] * scale));
3351
+ });
3352
+ return result;
3353
+ }
3354
+ const extra = Math.max(0, available - dataTotal);
2632
3355
  if (extra === 0 || dataTotal === 0) return colWidths;
2633
3356
  let distributed = 0;
2634
3357
  const result = {};
2635
3358
  visibleFields.forEach((f, i) => {
2636
- const base = getColumnWidth(f, colWidths);
3359
+ const base = bases[i];
2637
3360
  const share = i < visibleFields.length - 1 ? Math.round(extra * (base / dataTotal)) : extra - distributed;
2638
3361
  result[f.name] = base + share;
2639
3362
  distributed += share;
@@ -2645,7 +3368,8 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2645
3368
  hasCheckbox,
2646
3369
  hasDetail,
2647
3370
  hasRowActions,
2648
- containerWidth
3371
+ containerWidth,
3372
+ actionsColWidth
2649
3373
  ]);
2650
3374
  const tableLayoutStyle = { "--nb-datagrid-layout-width": `${layoutWidth}px` };
2651
3375
  const filterableFields = visibleFields.filter((field) => field.filterable);
@@ -2724,6 +3448,18 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2724
3448
  children: activeFilterCount
2725
3449
  })]
2726
3450
  }),
3451
+ inlineEditToolbar?.revert && hasPendingInlineEdits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
3452
+ className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--revert",
3453
+ icon: "ph ph-arrow-counter-clockwise",
3454
+ label: t("grid.inlineRevertChanges"),
3455
+ onClick: () => inlineEdit.discardAll()
3456
+ }),
3457
+ inlineEditToolbar?.save && hasPendingInlineEdits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
3458
+ className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--save",
3459
+ icon: "ph ph-floppy-disk",
3460
+ label: t("grid.inlineSaveChanges"),
3461
+ onClick: () => void inlineEdit.saveAll()
3462
+ }),
2727
3463
  toolbar.showRefresh !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
2728
3464
  icon: "ph ph-arrow-clockwise",
2729
3465
  label: t("grid.buttonRefresh"),
@@ -2731,6 +3467,14 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
2731
3467
  })
2732
3468
  ]
2733
3469
  }),
3470
+ cellEditMode && hasPendingInlineEdits && !inlineEditToolbar && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3471
+ className: "nb-datagrid__batch-bar nb-datagrid__batch-bar--compact",
3472
+ role: "status",
3473
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3474
+ className: "nb-datagrid__batch-bar-label",
3475
+ children: t("grid.inlineUnsavedRows", { count: dirtyRowCount })
3476
+ })
3477
+ }),
2734
3478
  options.aboveGrid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2735
3479
  className: "nb-datagrid__above-grid",
2736
3480
  children: options.aboveGrid
@@ -3024,16 +3768,37 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3024
3768
  }) }) : rows.map((row, rowIndex) => {
3025
3769
  const key = row[idField] ?? rowIndex;
3026
3770
  const selected = selectedKeys.includes(key);
3771
+ const editing = inlineEdit.draftRows.has(key);
3772
+ const rowHasChanges = editing && inlineEdit.hasDraftChanges(key, row);
3773
+ const saving = inlineEdit.savingRows.has(key);
3774
+ const rowDraft = inlineEdit.draftRows.get(key) ?? row;
3775
+ const rowFieldErrors = inlineEdit.rowErrors.get(key);
3776
+ const rowInRowEdit = rowInlineMode && editing;
3027
3777
  const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
3028
3778
  const detailUrl = options.detailUrl?.replace("{id}", String(key));
3029
3779
  const expanded = expandedKeys.has(key);
3030
3780
  const rowActions = buildRowActions(row);
3031
3781
  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" : ""}`,
3782
+ className: [
3783
+ "nb-datagrid__row",
3784
+ expanded ? "nb-datagrid__row--expanded" : "",
3785
+ selected ? "nb-datagrid__row--selected" : "",
3786
+ rowInRowEdit ? "nb-datagrid__row--editing" : "",
3787
+ rowHasChanges ? "nb-datagrid__row--dirty" : "",
3788
+ saving ? "nb-datagrid__row--saving" : ""
3789
+ ].filter(Boolean).join(" "),
3033
3790
  tabIndex: 0,
3034
3791
  "aria-selected": selected,
3035
- onClick: () => selectRow(row),
3792
+ onClick: () => {
3793
+ if (rowInRowEdit) return;
3794
+ selectRow(row);
3795
+ },
3036
3796
  onDoubleClick: () => {
3797
+ if (rowInRowEdit || inlineEdit.activeCell) return;
3798
+ if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
3799
+ inlineEdit.startEdit(row);
3800
+ return;
3801
+ }
3037
3802
  if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
3038
3803
  if (options.onEdit) options.onEdit(row);
3039
3804
  else emit(options.events.EDIT, { row });
@@ -3042,10 +3807,14 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3042
3807
  if (options.allowView && options.onView) options.onView(row);
3043
3808
  },
3044
3809
  onKeyDown: (event) => {
3045
- if (event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3810
+ if (inlineEdit.activeCell && event.key === "Escape") inlineEdit.stopCellEdit();
3811
+ else if (rowInRowEdit && event.key === "Escape") inlineEdit.cancelEdit(key);
3812
+ else if (rowInRowEdit && event.key === "Enter" && showRowInlineActions) inlineEdit.saveRow(key);
3813
+ else if (!editing && event.key === "Enter" && options.allowEdit && rowInlineMode && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
3814
+ else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3046
3815
  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);
3816
+ else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3817
+ else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3049
3818
  else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
3050
3819
  event.preventDefault();
3051
3820
  const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
@@ -3090,19 +3859,87 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
3090
3859
  })
3091
3860
  }),
3092
3861
  visibleFields.map((field, columnIndex) => {
3862
+ const width = getColumnWidth(field, resolvedColWidths);
3863
+ const editable = options.allowEdit && !options.editDisabled && rowEditable(row) && canEditFieldInline(field);
3864
+ const cellActive = inlineEdit.isCellActive(key, field.name);
3865
+ const cellDirty = editing && inlineEdit.isCellDirty(key, field.name, row);
3866
+ const showCellEditor = editable && (rowInRowEdit && editing || cellEditMode && cellActive);
3867
+ const displayRow = editing ? rowDraft : row;
3868
+ const cellClassName = [
3869
+ showCellEditor ? "nb-datagrid__edit-cell" : "nb-datagrid__data-cell",
3870
+ editable && canInlineEditMode ? "nb-datagrid__cell--editable" : "",
3871
+ cellDirty ? "nb-datagrid__cell--dirty" : "",
3872
+ cellActive ? "nb-datagrid__cell--active" : ""
3873
+ ].filter(Boolean).join(" ");
3874
+ const beginInlineEdit = () => {
3875
+ if (!editable) return;
3876
+ selectRow(row);
3877
+ if (cellEditMode) inlineEdit.startCellEdit(row, field.name);
3878
+ else if (rowInlineMode && !editing) inlineEdit.startEdit(row);
3879
+ };
3880
+ if (showCellEditor) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3881
+ style: {
3882
+ width,
3883
+ textAlign: field.align
3884
+ },
3885
+ className: cellClassName,
3886
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineEditCell, {
3887
+ field,
3888
+ rowKey: key,
3889
+ draft: rowDraft,
3890
+ onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
3891
+ errors: rowFieldErrors,
3892
+ disabled: saving,
3893
+ allRemoteOptions: filterRemoteOptions,
3894
+ httpClient,
3895
+ t,
3896
+ autoFocus: true
3897
+ })
3898
+ }, field.name);
3093
3899
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3094
3900
  style: {
3095
- width: getColumnWidth(field, resolvedColWidths),
3901
+ width,
3096
3902
  textAlign: field.align
3097
3903
  },
3098
- title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
3099
- children: renderCell(field, row, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
3904
+ className: cellClassName,
3905
+ title: getCellText(field, displayRow, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
3906
+ onClick: (event) => {
3907
+ if (!editable || !canInlineEditMode) return;
3908
+ event.stopPropagation();
3909
+ beginInlineEdit();
3910
+ },
3911
+ children: renderCell(field, displayRow, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
3100
3912
  }, field.name);
3101
3913
  }),
3102
3914
  hasRowActions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3103
3915
  className: "nb-datagrid__actions-cell",
3104
3916
  onClick: (e) => e.stopPropagation(),
3105
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3917
+ children: rowInRowEdit && showRowInlineActions ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3918
+ className: "nb-datagrid__inline-actions",
3919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3920
+ type: "button",
3921
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--save",
3922
+ disabled: saving,
3923
+ "aria-label": t("grid.inlineSaveRow"),
3924
+ title: t("grid.inlineSaveRow"),
3925
+ onClick: () => void inlineEdit.saveRow(key),
3926
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
3927
+ className: "ph ph-check",
3928
+ "aria-hidden": "true"
3929
+ })
3930
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3931
+ type: "button",
3932
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--cancel",
3933
+ disabled: saving,
3934
+ "aria-label": t("grid.inlineCancelRow"),
3935
+ title: t("grid.inlineCancelRow"),
3936
+ onClick: () => inlineEdit.cancelEdit(key),
3937
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
3938
+ className: "ph ph-x",
3939
+ "aria-hidden": "true"
3940
+ })
3941
+ })]
3942
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3106
3943
  className: "nb-datagrid__row-actions",
3107
3944
  children: rowActions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
3108
3945
  icon: "ph ph-dots-three-vertical",
@@ -3452,97 +4289,6 @@ const FORM_EVENTS = {
3452
4289
  };
3453
4290
  const FORM_ERRORS_EVENT = "form-errors";
3454
4291
  //#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
4292
  //#region packages/crud/form/FormDataTransform.ts
3547
4293
  function upsertPrependData(store, field, item) {
3548
4294
  const existing = store.get(field.name);
@@ -3844,59 +4590,6 @@ function buildFieldColSpanContext(options) {
3844
4590
  };
3845
4591
  }
3846
4592
  //#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
4593
  //#region packages/crud/form/useFormSubmit.ts
3901
4594
  /**
3902
4595
  * Returns helpers for form serialization and HTTP submit/delete operations.
@@ -4079,30 +4772,30 @@ function mapApiViolations(violations, detailPropertyName = "items", defaultMessa
4079
4772
  * the state seam, independently testable without the 900-line closure.
4080
4773
  */
4081
4774
  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) => {
4775
+ const isEdit = (0, react$1.useRef)(false);
4776
+ const uploadedFiles = (0, react$1.useRef)([]);
4777
+ const existingMediaByField = (0, react$1.useRef)({});
4778
+ const upsertUploadedFile = (0, react$1.useCallback)((entry) => {
4086
4779
  uploadedFiles.current = [...uploadedFiles.current.filter((file) => file.name !== entry.name), entry];
4087
4780
  }, []);
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) => {
4781
+ const [formData, setFormData] = (0, react$1.useState)(() => buildEmptyRow(fields));
4782
+ const formDataRef = (0, react$1.useRef)(formData);
4783
+ const [detailRows, setDetailRows] = (0, react$1.useState)([]);
4784
+ const detailRowsRef = (0, react$1.useRef)(detailRows);
4785
+ const [fieldState, setFieldState] = (0, react$1.useState)({});
4786
+ const [errors, setErrors] = (0, react$1.useState)({});
4787
+ const [detailErrors, setDetailErrors] = (0, react$1.useState)({});
4788
+ const prependDataRef = (0, react$1.useRef)(/* @__PURE__ */ new Map());
4789
+ const setNextFormData = (0, react$1.useCallback)((nextData) => {
4097
4790
  formDataRef.current = nextData;
4098
4791
  setFormData(nextData);
4099
4792
  onFieldDataChanged?.(nextData);
4100
4793
  }, [onFieldDataChanged]);
4101
- const setNextDetailRows = (0, react.useCallback)((nextRows) => {
4794
+ const setNextDetailRows = (0, react$1.useCallback)((nextRows) => {
4102
4795
  detailRowsRef.current = nextRows;
4103
4796
  setDetailRows(nextRows);
4104
4797
  }, []);
4105
- const setFieldValue = (0, react.useCallback)((name, value) => {
4798
+ const setFieldValue = (0, react$1.useCallback)((name, value) => {
4106
4799
  const field = fields.find((candidate) => candidate.name === name);
4107
4800
  const nextData = {
4108
4801
  ...formDataRef.current,
@@ -4117,22 +4810,22 @@ function useFormState({ fields, onFieldDataChanged }) {
4117
4810
  onFieldDataChanged?.(nextData);
4118
4811
  (field?.onChange)?.(value);
4119
4812
  }, [fields, onFieldDataChanged]);
4120
- const setEditMode = (0, react.useCallback)((value) => {
4813
+ const setEditMode = (0, react$1.useCallback)((value) => {
4121
4814
  isEdit.current = value;
4122
4815
  }, []);
4123
- const resetUploadSession = (0, react.useCallback)(() => {
4816
+ const resetUploadSession = (0, react$1.useCallback)(() => {
4124
4817
  uploadedFiles.current = [];
4125
4818
  }, []);
4126
- const setExistingMedia = (0, react.useCallback)((media) => {
4819
+ const setExistingMedia = (0, react$1.useCallback)((media) => {
4127
4820
  existingMediaByField.current = media;
4128
4821
  }, []);
4129
- const clearExistingMedia = (0, react.useCallback)((name) => {
4822
+ const clearExistingMedia = (0, react$1.useCallback)((name) => {
4130
4823
  delete existingMediaByField.current[name];
4131
4824
  }, []);
4132
- const resetPrependData = (0, react.useCallback)(() => {
4825
+ const resetPrependData = (0, react$1.useCallback)(() => {
4133
4826
  prependDataRef.current = /* @__PURE__ */ new Map();
4134
4827
  }, []);
4135
- const clearDetailCellError = (0, react.useCallback)((rowIndex, fieldName) => {
4828
+ const clearDetailCellError = (0, react$1.useCallback)((rowIndex, fieldName) => {
4136
4829
  setDetailErrors((current) => {
4137
4830
  const rowErrors = current[rowIndex];
4138
4831
  if (!rowErrors?.[fieldName]) return current;
@@ -4144,10 +4837,10 @@ function useFormState({ fields, onFieldDataChanged }) {
4144
4837
  return next;
4145
4838
  });
4146
4839
  }, []);
4147
- (0, react.useEffect)(() => {
4840
+ (0, react$1.useEffect)(() => {
4148
4841
  formDataRef.current = formData;
4149
4842
  }, [formData]);
4150
- (0, react.useEffect)(() => {
4843
+ (0, react$1.useEffect)(() => {
4151
4844
  detailRowsRef.current = detailRows;
4152
4845
  }, [detailRows]);
4153
4846
  return {
@@ -5288,7 +5981,7 @@ function fromOperations(supportedOperations) {
5288
5981
  */
5289
5982
  function usePermissions(resource, supportedOperations = []) {
5290
5983
  const opsKey = supportedOperations.slice().sort().join(",");
5291
- return (0, react.useMemo)(() => {
5984
+ return (0, react$1.useMemo)(() => {
5292
5985
  const p = resource.permissions;
5293
5986
  const inferred = fromOperations(supportedOperations);
5294
5987
  function resolveWithInferred(permValue, inferredValue, platformDefault) {
@@ -5313,15 +6006,15 @@ function usePermissions(resource, supportedOperations = []) {
5313
6006
  //#endregion
5314
6007
  //#region packages/crud/crud/useSelectionState.ts
5315
6008
  function useSelectionState(identityField) {
5316
- const [selectedIds, setSelectedIds] = (0, react.useState)([]);
5317
- const onSelectionChanged = (0, react.useCallback)((selectedRows) => {
6009
+ const [selectedIds, setSelectedIds] = (0, react$1.useState)([]);
6010
+ const onSelectionChanged = (0, react$1.useCallback)((selectedRows) => {
5318
6011
  setSelectedIds(selectedRows.map((row) => {
5319
6012
  const val = row[identityField];
5320
6013
  if (typeof val === "string" || typeof val === "number") return val;
5321
6014
  return String(val);
5322
6015
  }));
5323
6016
  }, [identityField]);
5324
- const clearSelection = (0, react.useCallback)(() => {
6017
+ const clearSelection = (0, react$1.useCallback)(() => {
5325
6018
  setSelectedIds([]);
5326
6019
  }, []);
5327
6020
  return {
@@ -5352,7 +6045,7 @@ function resolveVisibleColumns(resource, activeKey) {
5352
6045
  return resource.columnPresets.find((p) => p.key === activeKey)?.columns ?? null;
5353
6046
  }
5354
6047
  function useColumnPreset(resource) {
5355
- const [activePreset, setActivePresetState] = (0, react.useState)(() => {
6048
+ const [activePreset, setActivePresetState] = (0, react$1.useState)(() => {
5356
6049
  if (!resource.columnPresets?.length) return null;
5357
6050
  const stored = readFromStorage(resource.id);
5358
6051
  if (stored && resource.columnPresets.some((p) => p.key === stored)) return stored;
@@ -5360,7 +6053,7 @@ function useColumnPreset(resource) {
5360
6053
  });
5361
6054
  return {
5362
6055
  activePreset,
5363
- setPreset: (0, react.useCallback)((key) => {
6056
+ setPreset: (0, react$1.useCallback)((key) => {
5364
6057
  writeToStorage(resource.id, key);
5365
6058
  setActivePresetState(key);
5366
6059
  }, [resource.id]),
@@ -5681,19 +6374,19 @@ function buildFields(items) {
5681
6374
  //#endregion
5682
6375
  //#region packages/crud/crud/useCrudPage.ts
5683
6376
  function useCrudPage(resource, externalFormRef) {
5684
- const resolvedResource = (0, react.useMemo)(() => resolveCrudResource(resource), [resource]);
6377
+ const resolvedResource = (0, react$1.useMemo)(() => resolveCrudResource(resource), [resource]);
5685
6378
  const events = resolvedResource.events;
5686
- const _internalFormRef = (0, react.useRef)(null);
6379
+ const _internalFormRef = (0, react$1.useRef)(null);
5687
6380
  const formRef = externalFormRef ?? _internalFormRef;
5688
- const fields = (0, react.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
6381
+ const fields = (0, react$1.useMemo)(() => buildFields(resolvedResource.fields ?? []), [resolvedResource.fields]);
5689
6382
  return {
5690
6383
  events,
5691
6384
  resource: resolvedResource,
5692
6385
  fields,
5693
- formFields: (0, react.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
6386
+ formFields: (0, react$1.useMemo)(() => resolvedResource.formFields ? buildFields(resolvedResource.formFields) : fields, [fields, resolvedResource.formFields]),
5694
6387
  formRef,
5695
6388
  permissions: usePermissions(resolvedResource, resolvedResource._supportedOperations ?? []),
5696
- selectionState: useSelectionState((0, react.useMemo)(() => {
6389
+ selectionState: useSelectionState((0, react$1.useMemo)(() => {
5697
6390
  return fields.find((f) => f.isIdentity)?.name ?? "id";
5698
6391
  }, [fields])),
5699
6392
  presetState: useColumnPreset(resolvedResource)
@@ -6052,7 +6745,7 @@ function useDialogStoreContext() {
6052
6745
  function useCrudDialogStore(resourceId) {
6053
6746
  const { state, dispatch } = useDialogStoreContext();
6054
6747
  const dialogState = state[resourceId] ?? initialDialogState();
6055
- const openDialog = (0, react.useCallback)((mode, rowData = null) => {
6748
+ const openDialog = (0, react$1.useCallback)((mode, rowData = null) => {
6056
6749
  dispatch({
6057
6750
  type: "OPEN",
6058
6751
  resourceId,
@@ -6060,7 +6753,7 @@ function useCrudDialogStore(resourceId) {
6060
6753
  rowData
6061
6754
  });
6062
6755
  }, [dispatch, resourceId]);
6063
- const closeDialog = (0, react.useCallback)(() => {
6756
+ const closeDialog = (0, react$1.useCallback)(() => {
6064
6757
  dispatch({
6065
6758
  type: "CLOSE",
6066
6759
  resourceId
@@ -6506,6 +7199,8 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6506
7199
  selectionMode: hasMultipleSelection ? "multiple" : "single",
6507
7200
  onSelectionChanged: handleSelectionChanged,
6508
7201
  editMode: resolvedResource.editMode,
7202
+ inlineEditToolbar: resolvedResource.inlineEditToolbar,
7203
+ inlineRowActions: resolvedResource.inlineRowActions,
6509
7204
  visibleColumns: presetState.visibleColumns,
6510
7205
  beforeToolbar: renderPresetSelector,
6511
7206
  aboveGrid: aboveGridContent,
@@ -6659,7 +7354,7 @@ function useRouting(routing) {
6659
7354
  return {
6660
7355
  initialRecordId,
6661
7356
  initialIsNew,
6662
- initialFilters: (0, react.useMemo)(() => {
7357
+ initialFilters: (0, react$1.useMemo)(() => {
6663
7358
  if (!syncFiltersToUrl) return NO_FILTERS;
6664
7359
  const filters = {};
6665
7360
  searchParams.forEach((value, key) => {
@@ -6667,7 +7362,7 @@ function useRouting(routing) {
6667
7362
  });
6668
7363
  return filters;
6669
7364
  }, [syncFiltersToUrl, searchParams]),
6670
- syncFilters: (0, react.useCallback)((filters) => {
7365
+ syncFilters: (0, react$1.useCallback)((filters) => {
6671
7366
  if (!routing?.syncFiltersToUrl) return;
6672
7367
  const next = new URLSearchParams();
6673
7368
  Object.entries(filters).forEach(([key, value]) => {
@@ -6778,23 +7473,23 @@ function crudReducer(state, action) {
6778
7473
  */
6779
7474
  function useSmartCrudOperation(events, routingState) {
6780
7475
  const [on] = (0, _nubitio_core.useEvents)();
6781
- const [{ activeOperation, formData }, dispatchState] = (0, react.useReducer)(crudReducer, routingState, stateFromRouting);
6782
- const handleFormDataChange = (0, react.useCallback)((data) => {
7476
+ const [{ activeOperation, formData }, dispatchState] = (0, react$1.useReducer)(crudReducer, routingState, stateFromRouting);
7477
+ const handleFormDataChange = (0, react$1.useCallback)((data) => {
6783
7478
  dispatchState({
6784
7479
  type: "set-form-data",
6785
7480
  data
6786
7481
  });
6787
7482
  }, []);
6788
- const startCreate = (0, react.useCallback)(() => {
7483
+ const startCreate = (0, react$1.useCallback)(() => {
6789
7484
  dispatchState({ type: "create" });
6790
7485
  }, []);
6791
- const startEdit = (0, react.useCallback)(() => {
7486
+ const startEdit = (0, react$1.useCallback)(() => {
6792
7487
  dispatchState({ type: "edit" });
6793
7488
  }, []);
6794
- const resetOperation = (0, react.useCallback)(() => {
7489
+ const resetOperation = (0, react$1.useCallback)(() => {
6795
7490
  dispatchState({ type: "reset" });
6796
7491
  }, []);
6797
- (0, react.useEffect)(() => {
7492
+ (0, react$1.useEffect)(() => {
6798
7493
  dispatchState({
6799
7494
  type: "sync-routing",
6800
7495
  routingState: {
@@ -6803,7 +7498,7 @@ function useSmartCrudOperation(events, routingState) {
6803
7498
  }
6804
7499
  });
6805
7500
  }, [routingState.initialIsNew, routingState.initialRecordId]);
6806
- (0, react.useEffect)(() => {
7501
+ (0, react$1.useEffect)(() => {
6807
7502
  const subscriptions = [];
6808
7503
  if (events?.ADD) subscriptions.push(on(events.ADD, () => {
6809
7504
  dispatchState({ type: "create" });
@@ -6849,7 +7544,7 @@ function useSmartCrudOperation(events, routingState) {
6849
7544
  * so grids and forms can resolve row keys consistently.
6850
7545
  */
6851
7546
  function useFieldPermissions(fields, userRoles) {
6852
- return (0, react.useMemo)(() => {
7547
+ return (0, react$1.useMemo)(() => {
6853
7548
  return fields.reduce((acc, field) => {
6854
7549
  if (field.isIdentity) {
6855
7550
  acc.push(field);
@@ -6919,7 +7614,7 @@ function evaluateConditionalRuleState(field, formData) {
6919
7614
  * are returned. Empty objects `{}` are treated as valid create-form state.
6920
7615
  */
6921
7616
  function useConditionalRules(fields, formData) {
6922
- return (0, react.useMemo)(() => {
7617
+ return (0, react$1.useMemo)(() => {
6923
7618
  return fields.map((field) => evaluateConditionalRuleState(field, formData));
6924
7619
  }, [fields, formData]);
6925
7620
  }
@@ -6954,9 +7649,9 @@ function useDependsOn(fields, formData) {
6954
7649
  url: field.url ?? null,
6955
7650
  values: (field.dependsOn ?? []).map((dep) => formData[dep])
6956
7651
  }));
6957
- const isMounted = (0, react.useRef)(false);
6958
- const previousEntriesRef = (0, react.useRef)(null);
6959
- (0, react.useEffect)(() => {
7652
+ const isMounted = (0, react$1.useRef)(false);
7653
+ const previousEntriesRef = (0, react$1.useRef)(null);
7654
+ (0, react$1.useEffect)(() => {
6960
7655
  if (!isMounted.current) {
6961
7656
  isMounted.current = true;
6962
7657
  previousEntriesRef.current = currentEntries;
@@ -7138,7 +7833,7 @@ function applyFieldOperationSemantics(field, operation, behavior, owner = `Field
7138
7833
  * from schema loading and routing concerns.
7139
7834
  */
7140
7835
  function useSmartCrudFields(fields, activeOperation, formData, roles) {
7141
- const permissionFilteredFields = useFieldPermissions((0, react.useMemo)(() => {
7836
+ const permissionFilteredFields = useFieldPermissions((0, react$1.useMemo)(() => {
7142
7837
  if (!activeOperation) return fields;
7143
7838
  return fields.map((field) => {
7144
7839
  const runtimeField = field;
@@ -7150,7 +7845,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7150
7845
  useDependsOn(permissionFilteredFields, formData);
7151
7846
  return {
7152
7847
  gridFields: permissionFilteredFields,
7153
- processedFields: (0, react.useMemo)(() => {
7848
+ processedFields: (0, react$1.useMemo)(() => {
7154
7849
  const stateByName = new Map(fieldStates.map((s) => [s.name, s]));
7155
7850
  let anyChanged = false;
7156
7851
  const merged = permissionFilteredFields.map((field) => {
@@ -7170,7 +7865,7 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7170
7865
  });
7171
7866
  return anyChanged ? merged : permissionFilteredFields;
7172
7867
  }, [permissionFilteredFields, fieldStates]),
7173
- computedValues: (0, react.useMemo)(() => fieldStates.reduce((acc, state) => {
7868
+ computedValues: (0, react$1.useMemo)(() => fieldStates.reduce((acc, state) => {
7174
7869
  if (state.computedValue !== void 0) acc[state.name] = state.computedValue;
7175
7870
  return acc;
7176
7871
  }, {}), [fieldStates])