@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.mjs CHANGED
@@ -4,6 +4,9 @@ import { createPortal } from "react-dom";
4
4
  import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, FileDropzone, IconButton, Skeleton, Timeline, TimelineItem } from "@nubitio/ui";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createCrudEvents, createScopedEventBus, getCoreCurrency, getCoreLocale, getCoreTimezone, useCoreHttpClient, useCoreRuntime, useCoreTranslation, useEvents, useMercureSubscription } from "@nubitio/core";
7
+ import { EditorContent, useEditor } from "@tiptap/react";
8
+ import StarterKit from "@tiptap/starter-kit";
9
+ import Link from "@tiptap/extension-link";
7
10
  import { useQueryClient } from "@tanstack/react-query";
8
11
  //#region packages/crud/crud/defineResource.ts
9
12
  const stringResourceCache = /* @__PURE__ */ new Map();
@@ -1428,19 +1431,179 @@ const fileTypeModule = {
1428
1431
  }
1429
1432
  };
1430
1433
  //#endregion
1434
+ //#region packages/crud/field/registry/types/HtmlEditor.tsx
1435
+ function ToolbarButton({ active, disabled, title, onClick, children }) {
1436
+ return /* @__PURE__ */ jsx("button", {
1437
+ type: "button",
1438
+ title,
1439
+ disabled,
1440
+ className: `nb-html-editor__btn${active ? " nb-html-editor__btn--active" : ""}`,
1441
+ onMouseDown: (e) => {
1442
+ e.preventDefault();
1443
+ onClick();
1444
+ },
1445
+ children
1446
+ });
1447
+ }
1448
+ function ToolbarDivider() {
1449
+ return /* @__PURE__ */ jsx("span", {
1450
+ className: "nb-html-editor__divider",
1451
+ "aria-hidden": true
1452
+ });
1453
+ }
1454
+ function HtmlEditor({ id, name, value, disabled, readOnly, hasError, onChange }) {
1455
+ const editable = !disabled && !readOnly;
1456
+ const editor = useEditor({
1457
+ extensions: [StarterKit, Link.configure({
1458
+ openOnClick: false,
1459
+ autolink: true
1460
+ })],
1461
+ content: value,
1462
+ editable,
1463
+ onUpdate: ({ editor: e }) => {
1464
+ onChange(e.isEmpty ? "" : e.getHTML());
1465
+ }
1466
+ });
1467
+ useEffect(() => {
1468
+ if (!editor) return;
1469
+ if ((editor.isEmpty ? "" : editor.getHTML()) !== value) editor.commands.setContent(value ?? "");
1470
+ }, [value, editor]);
1471
+ useEffect(() => {
1472
+ editor?.setEditable(editable);
1473
+ }, [editable, editor]);
1474
+ const handleLinkToggle = useCallback(() => {
1475
+ if (!editor) return;
1476
+ if (editor.isActive("link")) editor.chain().focus().unsetLink().run();
1477
+ else {
1478
+ const url = window.prompt("URL");
1479
+ if (url) editor.chain().focus().setLink({ href: url }).run();
1480
+ }
1481
+ }, [editor]);
1482
+ return /* @__PURE__ */ jsxs("div", {
1483
+ className: `nb-html-editor${hasError ? " nb-html-editor--error" : ""}${!editable ? " nb-html-editor--readonly" : ""}`,
1484
+ children: [
1485
+ editable && /* @__PURE__ */ jsxs("div", {
1486
+ className: "nb-html-editor__toolbar",
1487
+ role: "toolbar",
1488
+ "aria-label": "Text formatting",
1489
+ children: [
1490
+ /* @__PURE__ */ jsx(ToolbarButton, {
1491
+ title: "Bold (Ctrl+B)",
1492
+ active: editor?.isActive("bold"),
1493
+ onClick: () => editor?.chain().focus().toggleBold().run(),
1494
+ children: /* @__PURE__ */ jsx("strong", { children: "B" })
1495
+ }),
1496
+ /* @__PURE__ */ jsx(ToolbarButton, {
1497
+ title: "Italic (Ctrl+I)",
1498
+ active: editor?.isActive("italic"),
1499
+ onClick: () => editor?.chain().focus().toggleItalic().run(),
1500
+ children: /* @__PURE__ */ jsx("em", { children: "I" })
1501
+ }),
1502
+ /* @__PURE__ */ jsx(ToolbarButton, {
1503
+ title: "Strikethrough",
1504
+ active: editor?.isActive("strike"),
1505
+ onClick: () => editor?.chain().focus().toggleStrike().run(),
1506
+ children: /* @__PURE__ */ jsx("s", { children: "S" })
1507
+ }),
1508
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1509
+ /* @__PURE__ */ jsx(ToolbarButton, {
1510
+ title: "Heading 2",
1511
+ active: editor?.isActive("heading", { level: 2 }),
1512
+ onClick: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(),
1513
+ children: "H2"
1514
+ }),
1515
+ /* @__PURE__ */ jsx(ToolbarButton, {
1516
+ title: "Heading 3",
1517
+ active: editor?.isActive("heading", { level: 3 }),
1518
+ onClick: () => editor?.chain().focus().toggleHeading({ level: 3 }).run(),
1519
+ children: "H3"
1520
+ }),
1521
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1522
+ /* @__PURE__ */ jsx(ToolbarButton, {
1523
+ title: "Bullet list",
1524
+ active: editor?.isActive("bulletList"),
1525
+ onClick: () => editor?.chain().focus().toggleBulletList().run(),
1526
+ children: "≡"
1527
+ }),
1528
+ /* @__PURE__ */ jsx(ToolbarButton, {
1529
+ title: "Ordered list",
1530
+ active: editor?.isActive("orderedList"),
1531
+ onClick: () => editor?.chain().focus().toggleOrderedList().run(),
1532
+ children: "1."
1533
+ }),
1534
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1535
+ /* @__PURE__ */ jsx(ToolbarButton, {
1536
+ title: "Blockquote",
1537
+ active: editor?.isActive("blockquote"),
1538
+ onClick: () => editor?.chain().focus().toggleBlockquote().run(),
1539
+ children: "“"
1540
+ }),
1541
+ /* @__PURE__ */ jsx(ToolbarButton, {
1542
+ title: editor?.isActive("link") ? "Remove link" : "Add link",
1543
+ active: editor?.isActive("link"),
1544
+ onClick: handleLinkToggle,
1545
+ children: "🔗"
1546
+ }),
1547
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
1548
+ /* @__PURE__ */ jsx(ToolbarButton, {
1549
+ title: "Undo (Ctrl+Z)",
1550
+ disabled: !editor?.can().undo(),
1551
+ onClick: () => editor?.chain().focus().undo().run(),
1552
+ children: "↩"
1553
+ }),
1554
+ /* @__PURE__ */ jsx(ToolbarButton, {
1555
+ title: "Redo (Ctrl+Y)",
1556
+ disabled: !editor?.can().redo(),
1557
+ onClick: () => editor?.chain().focus().redo().run(),
1558
+ children: "↪"
1559
+ })
1560
+ ]
1561
+ }),
1562
+ /* @__PURE__ */ jsx("input", {
1563
+ type: "hidden",
1564
+ id,
1565
+ name,
1566
+ value,
1567
+ readOnly: true
1568
+ }),
1569
+ /* @__PURE__ */ jsx(EditorContent, {
1570
+ editor,
1571
+ className: "nb-html-editor__content"
1572
+ })
1573
+ ]
1574
+ });
1575
+ }
1576
+ //#endregion
1431
1577
  //#region packages/crud/field/registry/types/html.tsx
1578
+ function stripTags(html) {
1579
+ if (html === null || html === void 0) return "";
1580
+ const str = String(html);
1581
+ if (!str.includes("<")) return str;
1582
+ try {
1583
+ return new DOMParser().parseFromString(str, "text/html").body.textContent ?? "";
1584
+ } catch {
1585
+ return str.replace(/<[^>]*>/g, "");
1586
+ }
1587
+ }
1432
1588
  const htmlTypeModule = {
1433
1589
  defaultFilterOperator: "contains",
1434
1590
  filterOperators: TEXT_OPERATORS,
1435
1591
  buildFilterTerms: defaultBuildFilterTerms,
1436
- cellText: (_field, value, ctx) => getPrimitiveDisplay(value, ctx.yesLabel, ctx.noLabel),
1592
+ cellText: (_field, value) => stripTags(value),
1437
1593
  serializeFormValue: () => KEEP,
1438
1594
  serializeDetailValue: () => KEEP,
1439
- ControlRender: ({ field, value, commonProps, errorClass, setFieldValue }) => /* @__PURE__ */ jsx("textarea", {
1440
- ...commonProps,
1441
- className: `nb-form__control nb-form__textarea${errorClass}`,
1442
- value: inputValue(value),
1443
- onChange: (event) => setFieldValue(field.name, event.target.value)
1595
+ CellRender: ({ value }) => /* @__PURE__ */ jsx("div", {
1596
+ className: "nb-datagrid__html-cell",
1597
+ dangerouslySetInnerHTML: { __html: String(value ?? "") }
1598
+ }),
1599
+ ControlRender: ({ field, value, commonProps, disabled, errorClass, setFieldValue }) => /* @__PURE__ */ jsx(HtmlEditor, {
1600
+ id: commonProps.id,
1601
+ name: field.name,
1602
+ value: String(value ?? ""),
1603
+ disabled,
1604
+ readOnly: commonProps.readOnly,
1605
+ hasError: errorClass !== "",
1606
+ onChange: (html) => setFieldValue(field.name, html)
1444
1607
  })
1445
1608
  };
1446
1609
  //#endregion
@@ -1737,7 +1900,13 @@ function renderCell(field, row, rowIndex, columnIndex, entityOptions, yesLabel =
1737
1900
  rowIndex,
1738
1901
  columnIndex
1739
1902
  });
1740
- return getFieldTypeModule(field.type).cellText(field, value, {
1903
+ const typeModule = getFieldTypeModule(field.type);
1904
+ if (typeModule.CellRender) return React.createElement(typeModule.CellRender, {
1905
+ field,
1906
+ value,
1907
+ row
1908
+ });
1909
+ return typeModule.cellText(field, value, {
1741
1910
  entityOptions,
1742
1911
  yesLabel,
1743
1912
  noLabel
@@ -1838,6 +2007,459 @@ function DetailGridSection({ fields, url }) {
1838
2007
  });
1839
2008
  }
1840
2009
  //#endregion
2010
+ //#region packages/crud/adapter/HydraAdapter.ts
2011
+ function trimTrailingSlash(value) {
2012
+ return value.replace(/\/+$/, "");
2013
+ }
2014
+ function buildIRI(url, value) {
2015
+ if (value.startsWith("/")) return value;
2016
+ return `${url}/${value}`;
2017
+ }
2018
+ /**
2019
+ * Default backend adapter for API Platform / JSON-LD + Hydra backends.
2020
+ *
2021
+ * Conventions assumed:
2022
+ * - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
2023
+ * - Entity fields are serialized as IRI strings in POST/PATCH bodies.
2024
+ * - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
2025
+ * - `_iri` is a synthetic alias for `@id` used internally by the engine.
2026
+ */
2027
+ const HydraAdapter = {
2028
+ getRowId(record, idField) {
2029
+ const direct = record[idField];
2030
+ if (direct !== void 0 && direct !== null) return String(direct);
2031
+ const iri = record["@id"] ?? record["_iri"];
2032
+ if (iri !== void 0 && iri !== null) return String(iri);
2033
+ return String(record["id"] ?? "");
2034
+ },
2035
+ buildItemUrl(baseUrl, id) {
2036
+ const str = String(id);
2037
+ if (str.startsWith("/")) return str;
2038
+ return `${trimTrailingSlash(baseUrl)}/${str}`;
2039
+ },
2040
+ serializeEntityRef(field, rawValue) {
2041
+ if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
2042
+ if (typeof rawValue === "object") {
2043
+ const entity = rawValue;
2044
+ const atId = entity["@id"];
2045
+ if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
2046
+ const idValue = entity["id"];
2047
+ const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
2048
+ return buildIRI(field.url ?? "", resolvedId);
2049
+ }
2050
+ return buildIRI(field.url ?? "", String(rawValue));
2051
+ },
2052
+ normalizeEntityValue(rawValue, field) {
2053
+ if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
2054
+ if (typeof rawValue === "object" && rawValue !== null) {
2055
+ const entity = rawValue;
2056
+ const atId = entity["@id"];
2057
+ if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
2058
+ const directValue = entity[field.valueField];
2059
+ if (directValue !== void 0 && directValue !== null) return directValue;
2060
+ }
2061
+ return rawValue;
2062
+ },
2063
+ getEntityOptionKey(item, field) {
2064
+ if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
2065
+ return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
2066
+ },
2067
+ parseListResponse(response) {
2068
+ const r = response;
2069
+ const member = r["hydra:member"];
2070
+ if (Array.isArray(member)) return {
2071
+ items: member,
2072
+ total: Number(r["hydra:totalItems"] ?? member.length)
2073
+ };
2074
+ if (Array.isArray(response)) return {
2075
+ items: response,
2076
+ total: response.length
2077
+ };
2078
+ return {
2079
+ items: [],
2080
+ total: 0
2081
+ };
2082
+ },
2083
+ synthesizeEntityKey(field, entityValue) {
2084
+ if (!field.url) return void 0;
2085
+ const base = trimTrailingSlash(field.url);
2086
+ const directId = entityValue["id"];
2087
+ if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
2088
+ const directValue = entityValue[field.valueField];
2089
+ if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
2090
+ for (const key of [
2091
+ "code",
2092
+ "uuid",
2093
+ "slug"
2094
+ ]) {
2095
+ const candidate = entityValue[key];
2096
+ if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
2097
+ }
2098
+ }
2099
+ };
2100
+ //#endregion
2101
+ //#region packages/crud/form/serializeFormData.ts
2102
+ function applySerializedValue(formData, field, result) {
2103
+ if (result.kind === "set") formData[field.name] = result.value;
2104
+ else if (result.kind === "omit") delete formData[field.name];
2105
+ }
2106
+ /**
2107
+ * Pure serialization of form data before HTTP submission.
2108
+ *
2109
+ * Applies uploaded file references and computed fields, then delegates the
2110
+ * per-type wire format (entity refs, business dates, numeric coercion, file
2111
+ * handling, NONE stripping) to each field's Field-Type module.
2112
+ *
2113
+ * This function is extracted from useFormSubmit for testability.
2114
+ */
2115
+ function serializeFormFields(rawData, fields, ctx) {
2116
+ const formData = { ...rawData };
2117
+ ctx.uploadedFiles.forEach((file) => {
2118
+ formData[file.name] = file.iri;
2119
+ });
2120
+ fields.forEach((field) => {
2121
+ if (!field.computed) return;
2122
+ const computedValue = field.computed(formData);
2123
+ if (computedValue !== void 0) formData[field.name] = computedValue;
2124
+ });
2125
+ const moduleCtx = {
2126
+ adapter: ctx.adapter ?? HydraAdapter,
2127
+ format: ctx.format,
2128
+ getFieldValue: ctx.getFieldValue
2129
+ };
2130
+ fields.forEach((field) => {
2131
+ applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
2132
+ });
2133
+ return formData;
2134
+ }
2135
+ /**
2136
+ * Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
2137
+ * Pure function — operates on an array of row records.
2138
+ */
2139
+ function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
2140
+ const details = structuredClone(rows);
2141
+ detailFields.forEach((field) => {
2142
+ const typeModule = getFieldTypeModule(field.type);
2143
+ details.forEach((detail) => {
2144
+ applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
2145
+ });
2146
+ });
2147
+ details.forEach((detail) => {
2148
+ if (!isEditMode) delete detail[detailIdField];
2149
+ else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
2150
+ });
2151
+ return details;
2152
+ }
2153
+ //#endregion
2154
+ //#region packages/crud/datagrid/useInlineEdit.ts
2155
+ /** Fields that are safe to edit inline (identity/readonly/file types are excluded). */
2156
+ function canEditFieldInline(field) {
2157
+ return !field.isIdentity && !field.readonly && field.type !== "file" && field.visibleOnForm !== false;
2158
+ }
2159
+ function valuesEqual$1(a, b) {
2160
+ if (a === b) return true;
2161
+ if (a == null && b == null) return true;
2162
+ if (a == null || b == null) return false;
2163
+ const numA = Number(a);
2164
+ const numB = Number(b);
2165
+ if (!Number.isNaN(numA) && !Number.isNaN(numB)) return numA === numB;
2166
+ return String(a) === String(b);
2167
+ }
2168
+ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError, onBatchSave }) {
2169
+ const [draftRows, setDraftRows] = useState(/* @__PURE__ */ new Map());
2170
+ const [savingRows, setSavingRows] = useState(/* @__PURE__ */ new Set());
2171
+ const [rowErrors, setRowErrors] = useState(/* @__PURE__ */ new Map());
2172
+ const [activeCell, setActiveCell] = useState(null);
2173
+ const draftRowsRef = useRef(draftRows);
2174
+ draftRowsRef.current = draftRows;
2175
+ const optsRef = useRef({
2176
+ url,
2177
+ idField,
2178
+ adapter,
2179
+ httpClient,
2180
+ fields,
2181
+ onSaveSuccess,
2182
+ onSaveError,
2183
+ onBatchSave
2184
+ });
2185
+ optsRef.current = {
2186
+ url,
2187
+ idField,
2188
+ adapter,
2189
+ httpClient,
2190
+ fields,
2191
+ onSaveSuccess,
2192
+ onSaveError,
2193
+ onBatchSave
2194
+ };
2195
+ const isEditing = useCallback((key) => draftRows.has(key), [draftRows]);
2196
+ const isCellActive = useCallback((key, fieldName) => activeCell != null && activeCell.key === key && activeCell.fieldName === fieldName, [activeCell]);
2197
+ const isCellDirty = useCallback((key, fieldName, original) => {
2198
+ const draft = draftRows.get(key);
2199
+ if (!draft) return false;
2200
+ return !valuesEqual$1(draft[fieldName], original[fieldName]);
2201
+ }, [draftRows]);
2202
+ const hasDraftChanges = useCallback((key, original) => {
2203
+ const draft = draftRows.get(key);
2204
+ if (!draft) return false;
2205
+ return optsRef.current.fields.some((field) => canEditFieldInline(field) && !valuesEqual$1(draft[field.name], original[field.name]));
2206
+ }, [draftRows]);
2207
+ const startEdit = useCallback((row) => {
2208
+ const key = row[optsRef.current.idField];
2209
+ setDraftRows((prev) => {
2210
+ const base = mode === "row" ? [] : Array.from(prev.entries());
2211
+ return new Map([...base, [key, { ...row }]]);
2212
+ });
2213
+ setRowErrors((prev) => {
2214
+ const next = new Map(prev);
2215
+ next.delete(key);
2216
+ return next;
2217
+ });
2218
+ setActiveCell(null);
2219
+ }, [mode]);
2220
+ const startCellEdit = useCallback((row, fieldName) => {
2221
+ const key = row[optsRef.current.idField];
2222
+ setDraftRows((prev) => {
2223
+ const existing = prev.get(key);
2224
+ const withoutKey = (mode === "row" ? [] : Array.from(prev.entries())).filter(([entryKey]) => entryKey !== key);
2225
+ return new Map([...withoutKey, [key, existing ? { ...existing } : { ...row }]]);
2226
+ });
2227
+ setRowErrors((prev) => {
2228
+ const next = new Map(prev);
2229
+ next.delete(key);
2230
+ return next;
2231
+ });
2232
+ setActiveCell({
2233
+ key,
2234
+ fieldName
2235
+ });
2236
+ }, [mode]);
2237
+ const stopCellEdit = useCallback(() => {
2238
+ setActiveCell(null);
2239
+ }, []);
2240
+ const cancelEdit = useCallback((key) => {
2241
+ setDraftRows((prev) => {
2242
+ const next = new Map(prev);
2243
+ next.delete(key);
2244
+ return next;
2245
+ });
2246
+ setRowErrors((prev) => {
2247
+ const next = new Map(prev);
2248
+ next.delete(key);
2249
+ return next;
2250
+ });
2251
+ setActiveCell((current) => current?.key === key ? null : current);
2252
+ }, []);
2253
+ const discardAll = useCallback(() => {
2254
+ setDraftRows(/* @__PURE__ */ new Map());
2255
+ setRowErrors(/* @__PURE__ */ new Map());
2256
+ setActiveCell(null);
2257
+ }, []);
2258
+ const updateDraft = useCallback((key, fieldName, value) => {
2259
+ setDraftRows((prev) => {
2260
+ const current = prev.get(key);
2261
+ if (!current) return prev;
2262
+ const next = new Map(prev);
2263
+ next.set(key, {
2264
+ ...current,
2265
+ [fieldName]: value
2266
+ });
2267
+ return next;
2268
+ });
2269
+ }, []);
2270
+ const doSaveRow = useCallback(async (key) => {
2271
+ const { url: u, adapter: a, httpClient: http, fields: fs, onSaveError: onErr } = optsRef.current;
2272
+ const draft = draftRowsRef.current.get(key);
2273
+ if (!draft) return false;
2274
+ const errors = {};
2275
+ fs.forEach((field) => {
2276
+ if (!canEditFieldInline(field)) return;
2277
+ if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
2278
+ });
2279
+ if (Object.keys(errors).length > 0) {
2280
+ setRowErrors((prev) => new Map(prev).set(key, errors));
2281
+ return false;
2282
+ }
2283
+ setSavingRows((prev) => new Set([...prev, key]));
2284
+ const serialized = serializeFormFields(draft, fs.filter(canEditFieldInline), {
2285
+ uploadedFiles: [],
2286
+ getFieldValue: (name) => draft[name]
2287
+ });
2288
+ try {
2289
+ await http.patch(a.buildItemUrl(u, key), serialized);
2290
+ setDraftRows((prev) => {
2291
+ const next = new Map(prev);
2292
+ next.delete(key);
2293
+ return next;
2294
+ });
2295
+ setRowErrors((prev) => {
2296
+ const next = new Map(prev);
2297
+ next.delete(key);
2298
+ return next;
2299
+ });
2300
+ return true;
2301
+ } catch (err) {
2302
+ onErr?.(key, err);
2303
+ if (typeof err === "object" && err !== null && "status" in err && err.status === 422 && "data" in err) {
2304
+ const data = err.data;
2305
+ if (typeof data === "object" && data !== null && "violations" in data) {
2306
+ const violations = data.violations ?? [];
2307
+ const fieldErrors = {};
2308
+ violations.forEach((v) => {
2309
+ fieldErrors[v.propertyPath] = v.message;
2310
+ });
2311
+ setRowErrors((prev) => new Map(prev).set(key, fieldErrors));
2312
+ }
2313
+ }
2314
+ return false;
2315
+ } finally {
2316
+ setSavingRows((prev) => {
2317
+ const next = new Set(prev);
2318
+ next.delete(key);
2319
+ return next;
2320
+ });
2321
+ }
2322
+ }, []);
2323
+ const validateDraft = useCallback((key, draft, fs) => {
2324
+ const errors = {};
2325
+ fs.forEach((field) => {
2326
+ if (!canEditFieldInline(field)) return;
2327
+ if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
2328
+ });
2329
+ if (Object.keys(errors).length === 0) return true;
2330
+ setRowErrors((prev) => new Map(prev).set(key, errors));
2331
+ return false;
2332
+ }, []);
2333
+ const doSaveBatch = useCallback(async () => {
2334
+ const { fields: fs, onBatchSave: saveBatch, onSaveError: onErr } = optsRef.current;
2335
+ if (!saveBatch) return false;
2336
+ const entries = Array.from(draftRowsRef.current.entries());
2337
+ if (entries.length === 0) return false;
2338
+ if (!entries.every(([key, draft]) => validateDraft(key, draft, fs))) return false;
2339
+ const keys = entries.map(([key]) => key);
2340
+ setSavingRows((prev) => new Set([...prev, ...keys]));
2341
+ try {
2342
+ await saveBatch(entries.map(([, draft]) => ({ ...draft })));
2343
+ setDraftRows(/* @__PURE__ */ new Map());
2344
+ setRowErrors(/* @__PURE__ */ new Map());
2345
+ return true;
2346
+ } catch (err) {
2347
+ keys.forEach((key) => onErr?.(key, err));
2348
+ return false;
2349
+ } finally {
2350
+ setSavingRows((prev) => {
2351
+ const next = new Set(prev);
2352
+ keys.forEach((key) => next.delete(key));
2353
+ return next;
2354
+ });
2355
+ }
2356
+ }, [validateDraft]);
2357
+ return {
2358
+ draftRows,
2359
+ savingRows,
2360
+ rowErrors,
2361
+ activeCell,
2362
+ isEditing,
2363
+ isCellActive,
2364
+ isCellDirty,
2365
+ hasDraftChanges,
2366
+ startEdit,
2367
+ startCellEdit,
2368
+ stopCellEdit,
2369
+ cancelEdit,
2370
+ discardAll,
2371
+ updateDraft,
2372
+ saveRow: useCallback(async (key) => {
2373
+ if (optsRef.current.onBatchSave) {
2374
+ const ok = await doSaveBatch();
2375
+ if (ok) optsRef.current.onSaveSuccess?.();
2376
+ return ok;
2377
+ }
2378
+ const ok = await doSaveRow(key);
2379
+ if (ok) optsRef.current.onSaveSuccess?.();
2380
+ return ok;
2381
+ }, [doSaveBatch, doSaveRow]),
2382
+ saveAll: useCallback(async () => {
2383
+ if (optsRef.current.onBatchSave) {
2384
+ const ok = await doSaveBatch();
2385
+ if (ok) optsRef.current.onSaveSuccess?.();
2386
+ return ok;
2387
+ }
2388
+ const keys = Array.from(draftRowsRef.current.keys());
2389
+ const anySuccess = (await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value);
2390
+ if (anySuccess) optsRef.current.onSaveSuccess?.();
2391
+ return anySuccess;
2392
+ }, [doSaveBatch, doSaveRow])
2393
+ };
2394
+ }
2395
+ function focusInlineControl(container) {
2396
+ if (!container) return;
2397
+ 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])");
2398
+ focusable?.focus();
2399
+ if (focusable instanceof HTMLInputElement && focusable.type !== "checkbox") focusable.select();
2400
+ }
2401
+ /**
2402
+ * Renders a single cell as an editable control during inline editing.
2403
+ * Uses the same FieldTypeModule.ControlRender path as the full form, with
2404
+ * compact styling so controls fit naturally inside a table cell.
2405
+ */
2406
+ function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t, autoFocus = true }) {
2407
+ const containerRef = useRef(null);
2408
+ const typeModule = getFieldTypeModule(field.type);
2409
+ const fieldError = errors?.[field.name];
2410
+ const errorClass = fieldError ? " is-error" : "";
2411
+ useEffect(() => {
2412
+ if (!autoFocus) return;
2413
+ focusInlineControl(containerRef.current);
2414
+ }, [
2415
+ autoFocus,
2416
+ field.name,
2417
+ rowKey
2418
+ ]);
2419
+ const commonProps = {
2420
+ className: `nb-inline-control${errorClass}`,
2421
+ disabled,
2422
+ id: `iec-${String(rowKey)}-${field.name}`,
2423
+ name: field.name,
2424
+ onClick: void 0,
2425
+ readOnly: false,
2426
+ required: field.required
2427
+ };
2428
+ const ctx = {
2429
+ httpClient,
2430
+ t,
2431
+ remoteOptions: allRemoteOptions,
2432
+ getPrependData: () => void 0,
2433
+ getFieldValue: (name) => draft[name],
2434
+ getExistingMedia: () => null,
2435
+ clearExistingMedia: () => {},
2436
+ upsertUploadedFile: () => {}
2437
+ };
2438
+ return /* @__PURE__ */ jsx("div", {
2439
+ ref: containerRef,
2440
+ className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
2441
+ onKeyDown: (event) => {
2442
+ if (event.key === "Escape") event.stopPropagation();
2443
+ },
2444
+ onMouseDown: (event) => {
2445
+ if (event.target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
2446
+ event.stopPropagation();
2447
+ },
2448
+ onClick: (event) => event.stopPropagation(),
2449
+ children: typeModule.ControlRender({
2450
+ field,
2451
+ value: draft[field.name],
2452
+ error: fieldError,
2453
+ errorClass,
2454
+ disabled,
2455
+ readOnly: false,
2456
+ commonProps,
2457
+ setFieldValue: onChange,
2458
+ ctx
2459
+ })
2460
+ });
2461
+ }
2462
+ //#endregion
1841
2463
  //#region packages/crud/summary/SummaryUtils.ts
1842
2464
  function toFiniteNumber(value) {
1843
2465
  if (value === null || value === void 0 || value === "") return null;
@@ -1907,19 +2529,20 @@ function resolveSummaryText(rows, item) {
1907
2529
  const DETAIL_COL_WIDTH = 36;
1908
2530
  const CHECKBOX_COL_WIDTH = 36;
1909
2531
  const ACTIONS_COL_WIDTH = 44;
2532
+ const INLINE_ACTIONS_COL_WIDTH = 72;
1910
2533
  const DEFAULT_COL_WIDTH = 120;
1911
2534
  const MIN_COL_WIDTH = 48;
1912
2535
  function getColumnWidth(field, colWidths) {
1913
2536
  return colWidths[field.name] ?? field.width ?? field.minWidth ?? DEFAULT_COL_WIDTH;
1914
2537
  }
1915
- function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth }) {
2538
+ function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth, actionsColWidth = ACTIONS_COL_WIDTH }) {
1916
2539
  let total = 0;
1917
2540
  if (hasDetail) total += DETAIL_COL_WIDTH;
1918
2541
  if (hasCheckbox) total += CHECKBOX_COL_WIDTH;
1919
2542
  visibleFields.forEach((field) => {
1920
2543
  total += getColumnWidth(field, colWidths);
1921
2544
  });
1922
- if (hasRowActions) total += ACTIONS_COL_WIDTH;
2545
+ if (hasRowActions) total += actionsColWidth;
1923
2546
  return Math.max(containerWidth, total);
1924
2547
  }
1925
2548
  function GridColumnGroup({ fields, colWidths, hasCheckbox, hasDetail, hasRowActions }) {
@@ -1951,6 +2574,25 @@ function normalizeIcon$1(icon) {
1951
2574
  function isDateLikeField(field) {
1952
2575
  return field.type === "date" || field.type === "datetime";
1953
2576
  }
2577
+ function isCellEditMode(editMode) {
2578
+ return editMode === "cell" || editMode === "batch";
2579
+ }
2580
+ function resolveInlineEditToolbar(editMode, inlineEditToolbar) {
2581
+ if (inlineEditToolbar === false) return null;
2582
+ const defaultShow = isCellEditMode(editMode);
2583
+ if (inlineEditToolbar == null) return defaultShow ? {
2584
+ save: true,
2585
+ revert: true
2586
+ } : null;
2587
+ if (typeof inlineEditToolbar === "boolean") return inlineEditToolbar ? {
2588
+ save: true,
2589
+ revert: true
2590
+ } : null;
2591
+ return {
2592
+ save: inlineEditToolbar.save !== false,
2593
+ revert: inlineEditToolbar.revert !== false
2594
+ };
2595
+ }
1954
2596
  function getToolbarKey(action, index) {
1955
2597
  return action.key ?? action.text ?? String(index);
1956
2598
  }
@@ -2302,9 +2944,20 @@ const NativeDataGridView = forwardRef((options, ref) => {
2302
2944
  ]);
2303
2945
  const fieldsRef = useRef(options.fields);
2304
2946
  fieldsRef.current = options.fields;
2947
+ const loadSeqRef = useRef(0);
2305
2948
  const loadRows = useCallback(async () => {
2949
+ if (options.data) {
2950
+ rowsRef.current = options.data;
2951
+ setRows(options.data);
2952
+ setTotalCount(options.data.length);
2953
+ setGridSummary(options.gridSummary ?? null);
2954
+ setIsGridLoading(false);
2955
+ onContentReadyRef.current?.();
2956
+ return options.data;
2957
+ }
2306
2958
  if (options.manualLoad) return rowsRef.current;
2307
2959
  setIsGridLoading(true);
2960
+ const seq = ++loadSeqRef.current;
2308
2961
  const loadOptions = {
2309
2962
  filter: buildFilterExpression(filters, filterOperators, fieldsRef.current),
2310
2963
  sort
@@ -2314,6 +2967,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
2314
2967
  loadOptions.take = pageSize;
2315
2968
  }
2316
2969
  const result = await source.load(loadOptions);
2970
+ if (seq !== loadSeqRef.current) return rowsRef.current;
2317
2971
  rowsRef.current = result.data;
2318
2972
  setRows(result.data);
2319
2973
  setTotalCount(result.totalCount);
@@ -2324,6 +2978,8 @@ const NativeDataGridView = forwardRef((options, ref) => {
2324
2978
  }, [
2325
2979
  filterOperators,
2326
2980
  filters,
2981
+ options.data,
2982
+ options.gridSummary,
2327
2983
  options.manualLoad,
2328
2984
  options.paging,
2329
2985
  page,
@@ -2367,19 +3023,58 @@ const NativeDataGridView = forwardRef((options, ref) => {
2367
3023
  resourceStoreFactory,
2368
3024
  visibleFields
2369
3025
  ]);
3026
+ const canInlineEditMode = options.editMode === "row" || options.editMode === "cell" || options.editMode === "batch";
3027
+ const cellEditMode = isCellEditMode(options.editMode);
3028
+ const rowInlineMode = options.editMode === "row";
3029
+ const inlineEditToolbar = resolveInlineEditToolbar(options.editMode, options.inlineEditToolbar);
3030
+ const showRowInlineActions = options.inlineRowActions ?? rowInlineMode;
3031
+ const inlineEdit = useInlineEdit({
3032
+ mode: cellEditMode ? "batch" : "row",
3033
+ url: options.url,
3034
+ idField,
3035
+ adapter: options.adapter,
3036
+ httpClient,
3037
+ fields: options.fields,
3038
+ onSaveSuccess: () => void loadRows(),
3039
+ onSaveError: () => {},
3040
+ onBatchSave: options.onBatchSave
3041
+ });
3042
+ const dirtyRowCount = useMemo(() => rows.filter((row) => {
3043
+ const key = row[idField] ?? row;
3044
+ return inlineEdit.draftRows.has(key) && inlineEdit.hasDraftChanges(key, row);
3045
+ }).length, [
3046
+ rows,
3047
+ idField,
3048
+ inlineEdit.draftRows,
3049
+ inlineEdit.hasDraftChanges
3050
+ ]);
3051
+ const hasPendingInlineEdits = dirtyRowCount > 0;
3052
+ useEffect(() => {
3053
+ if (!inlineEdit.activeCell) return;
3054
+ const handler = (event) => {
3055
+ const target = event.target;
3056
+ if (target.closest(".nb-datagrid__edit-cell")) return;
3057
+ if (target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
3058
+ inlineEdit.stopCellEdit();
3059
+ };
3060
+ document.addEventListener("mousedown", handler);
3061
+ return () => document.removeEventListener("mousedown", handler);
3062
+ }, [inlineEdit.activeCell, inlineEdit.stopCellEdit]);
2370
3063
  const handleStateRef = useRef({
2371
3064
  selectedKeys,
2372
3065
  filters,
2373
3066
  filterOperators,
2374
3067
  loadRows,
2375
- idField
3068
+ idField,
3069
+ inlineEdit
2376
3070
  });
2377
3071
  handleStateRef.current = {
2378
3072
  selectedKeys,
2379
3073
  filters,
2380
3074
  filterOperators,
2381
3075
  loadRows,
2382
- idField
3076
+ idField,
3077
+ inlineEdit
2383
3078
  };
2384
3079
  useImperativeHandle(ref, () => ({
2385
3080
  showLoading: (message) => {
@@ -2419,7 +3114,9 @@ const NativeDataGridView = forwardRef((options, ref) => {
2419
3114
  setFilterInputs(nextFilters);
2420
3115
  setFilterOperators(nextOperators);
2421
3116
  setPage(0);
2422
- }
3117
+ },
3118
+ hasEditData: () => handleStateRef.current.inlineEdit.draftRows.size > 0,
3119
+ saveChanges: () => handleStateRef.current.inlineEdit.saveAll()
2423
3120
  }), [options.fields, t]);
2424
3121
  const selectedRows = rows.filter((row) => selectedKeys.includes(row[idField]));
2425
3122
  const selectRow = (row) => {
@@ -2433,7 +3130,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
2433
3130
  const rowEditable = (row) => options.canEditRow?.(row) !== false;
2434
3131
  const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
2435
3132
  const buildRowActions = (row) => [
2436
- ...options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT) ? [{
3133
+ ...options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
3134
+ text: t("grid.inlineEditRow"),
3135
+ icon: "ph-pencil-simple",
3136
+ disabled: options.editDisabled,
3137
+ onClick: () => inlineEdit.startEdit(row)
3138
+ }] : [],
3139
+ ...options.allowEdit && rowEditable(row) && !canInlineEditMode && (options.onEdit || options.events?.EDIT) ? [{
2437
3140
  text: t("grid.buttonEdit"),
2438
3141
  icon: "ph-pencil-simple",
2439
3142
  disabled: options.editDisabled,
@@ -2457,6 +3160,10 @@ const NativeDataGridView = forwardRef((options, ref) => {
2457
3160
  }] : []
2458
3161
  ];
2459
3162
  const openRow = (row) => {
3163
+ if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
3164
+ inlineEdit.startEdit(row);
3165
+ return;
3166
+ }
2460
3167
  if (options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT)) {
2461
3168
  if (options.onEdit) options.onEdit(row);
2462
3169
  else emit(options.events.EDIT, { row });
@@ -2588,28 +3295,41 @@ const NativeDataGridView = forwardRef((options, ref) => {
2588
3295
  };
2589
3296
  const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
2590
3297
  const hasCheckbox = options.selectionMode === "multiple";
2591
- const hasBuiltInRowActions = Boolean(options.allowEdit && (options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
3298
+ const hasBuiltInRowActions = Boolean(options.allowEdit && (rowInlineMode && showRowInlineActions || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
2592
3299
  const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
2593
3300
  const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
2594
3301
  const hasDetail = Boolean(options.detailFields);
3302
+ const actionsColWidth = canInlineEditMode ? INLINE_ACTIONS_COL_WIDTH : ACTIONS_COL_WIDTH;
2595
3303
  const layoutWidth = computeLayoutWidth({
2596
3304
  visibleFields,
2597
3305
  colWidths,
2598
3306
  hasCheckbox,
2599
3307
  hasDetail,
2600
3308
  hasRowActions,
2601
- containerWidth
3309
+ containerWidth,
3310
+ actionsColWidth
2602
3311
  });
2603
3312
  const resolvedColWidths = useMemo(() => {
2604
- if (visibleFields.length === 0) return colWidths;
2605
- const dataTotal = visibleFields.reduce((sum, f) => sum + getColumnWidth(f, colWidths), 0);
2606
- const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? ACTIONS_COL_WIDTH : 0);
2607
- const extra = Math.max(0, containerWidth - dataTotal - fixedTotal);
3313
+ if (visibleFields.length === 0 || containerWidth <= 0) return colWidths;
3314
+ const bases = visibleFields.map((f) => getColumnWidth(f, colWidths));
3315
+ const dataTotal = bases.reduce((sum, width) => sum + width, 0);
3316
+ const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
3317
+ const available = Math.max(0, containerWidth - fixedTotal);
3318
+ if (dataTotal > available && available > 0) {
3319
+ const scale = available / dataTotal;
3320
+ const result = {};
3321
+ visibleFields.forEach((field, index) => {
3322
+ const floor = field.minWidth ?? MIN_COL_WIDTH;
3323
+ result[field.name] = Math.max(floor, Math.floor(bases[index] * scale));
3324
+ });
3325
+ return result;
3326
+ }
3327
+ const extra = Math.max(0, available - dataTotal);
2608
3328
  if (extra === 0 || dataTotal === 0) return colWidths;
2609
3329
  let distributed = 0;
2610
3330
  const result = {};
2611
3331
  visibleFields.forEach((f, i) => {
2612
- const base = getColumnWidth(f, colWidths);
3332
+ const base = bases[i];
2613
3333
  const share = i < visibleFields.length - 1 ? Math.round(extra * (base / dataTotal)) : extra - distributed;
2614
3334
  result[f.name] = base + share;
2615
3335
  distributed += share;
@@ -2621,7 +3341,8 @@ const NativeDataGridView = forwardRef((options, ref) => {
2621
3341
  hasCheckbox,
2622
3342
  hasDetail,
2623
3343
  hasRowActions,
2624
- containerWidth
3344
+ containerWidth,
3345
+ actionsColWidth
2625
3346
  ]);
2626
3347
  const tableLayoutStyle = { "--nb-datagrid-layout-width": `${layoutWidth}px` };
2627
3348
  const filterableFields = visibleFields.filter((field) => field.filterable);
@@ -2700,6 +3421,18 @@ const NativeDataGridView = forwardRef((options, ref) => {
2700
3421
  children: activeFilterCount
2701
3422
  })]
2702
3423
  }),
3424
+ inlineEditToolbar?.revert && hasPendingInlineEdits && /* @__PURE__ */ jsx(IconButton, {
3425
+ className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--revert",
3426
+ icon: "ph ph-arrow-counter-clockwise",
3427
+ label: t("grid.inlineRevertChanges"),
3428
+ onClick: () => inlineEdit.discardAll()
3429
+ }),
3430
+ inlineEditToolbar?.save && hasPendingInlineEdits && /* @__PURE__ */ jsx(IconButton, {
3431
+ className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--save",
3432
+ icon: "ph ph-floppy-disk",
3433
+ label: t("grid.inlineSaveChanges"),
3434
+ onClick: () => void inlineEdit.saveAll()
3435
+ }),
2703
3436
  toolbar.showRefresh !== false && /* @__PURE__ */ jsx(IconButton, {
2704
3437
  icon: "ph ph-arrow-clockwise",
2705
3438
  label: t("grid.buttonRefresh"),
@@ -2707,6 +3440,14 @@ const NativeDataGridView = forwardRef((options, ref) => {
2707
3440
  })
2708
3441
  ]
2709
3442
  }),
3443
+ cellEditMode && hasPendingInlineEdits && !inlineEditToolbar && /* @__PURE__ */ jsx("div", {
3444
+ className: "nb-datagrid__batch-bar nb-datagrid__batch-bar--compact",
3445
+ role: "status",
3446
+ children: /* @__PURE__ */ jsx("span", {
3447
+ className: "nb-datagrid__batch-bar-label",
3448
+ children: t("grid.inlineUnsavedRows", { count: dirtyRowCount })
3449
+ })
3450
+ }),
2710
3451
  options.aboveGrid ? /* @__PURE__ */ jsx("div", {
2711
3452
  className: "nb-datagrid__above-grid",
2712
3453
  children: options.aboveGrid
@@ -3000,16 +3741,37 @@ const NativeDataGridView = forwardRef((options, ref) => {
3000
3741
  }) }) : rows.map((row, rowIndex) => {
3001
3742
  const key = row[idField] ?? rowIndex;
3002
3743
  const selected = selectedKeys.includes(key);
3744
+ const editing = inlineEdit.draftRows.has(key);
3745
+ const rowHasChanges = editing && inlineEdit.hasDraftChanges(key, row);
3746
+ const saving = inlineEdit.savingRows.has(key);
3747
+ const rowDraft = inlineEdit.draftRows.get(key) ?? row;
3748
+ const rowFieldErrors = inlineEdit.rowErrors.get(key);
3749
+ const rowInRowEdit = rowInlineMode && editing;
3003
3750
  const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
3004
3751
  const detailUrl = options.detailUrl?.replace("{id}", String(key));
3005
3752
  const expanded = expandedKeys.has(key);
3006
3753
  const rowActions = buildRowActions(row);
3007
3754
  return /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("tr", {
3008
- className: `nb-datagrid__row ${expanded ? "nb-datagrid__row--expanded" : ""} ${selected ? "nb-datagrid__row--selected" : ""}`,
3755
+ className: [
3756
+ "nb-datagrid__row",
3757
+ expanded ? "nb-datagrid__row--expanded" : "",
3758
+ selected ? "nb-datagrid__row--selected" : "",
3759
+ rowInRowEdit ? "nb-datagrid__row--editing" : "",
3760
+ rowHasChanges ? "nb-datagrid__row--dirty" : "",
3761
+ saving ? "nb-datagrid__row--saving" : ""
3762
+ ].filter(Boolean).join(" "),
3009
3763
  tabIndex: 0,
3010
3764
  "aria-selected": selected,
3011
- onClick: () => selectRow(row),
3765
+ onClick: () => {
3766
+ if (rowInRowEdit) return;
3767
+ selectRow(row);
3768
+ },
3012
3769
  onDoubleClick: () => {
3770
+ if (rowInRowEdit || inlineEdit.activeCell) return;
3771
+ if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
3772
+ inlineEdit.startEdit(row);
3773
+ return;
3774
+ }
3013
3775
  if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
3014
3776
  if (options.onEdit) options.onEdit(row);
3015
3777
  else emit(options.events.EDIT, { row });
@@ -3018,10 +3780,14 @@ const NativeDataGridView = forwardRef((options, ref) => {
3018
3780
  if (options.allowView && options.onView) options.onView(row);
3019
3781
  },
3020
3782
  onKeyDown: (event) => {
3021
- if (event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3783
+ if (inlineEdit.activeCell && event.key === "Escape") inlineEdit.stopCellEdit();
3784
+ else if (rowInRowEdit && event.key === "Escape") inlineEdit.cancelEdit(key);
3785
+ else if (rowInRowEdit && event.key === "Enter" && showRowInlineActions) inlineEdit.saveRow(key);
3786
+ else if (!editing && event.key === "Enter" && options.allowEdit && rowInlineMode && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
3787
+ else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
3022
3788
  else emit(options.events.EDIT, { row });
3023
- else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3024
- else if (event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3789
+ else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
3790
+ else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
3025
3791
  else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
3026
3792
  event.preventDefault();
3027
3793
  const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
@@ -3066,19 +3832,87 @@ const NativeDataGridView = forwardRef((options, ref) => {
3066
3832
  })
3067
3833
  }),
3068
3834
  visibleFields.map((field, columnIndex) => {
3835
+ const width = getColumnWidth(field, resolvedColWidths);
3836
+ const editable = options.allowEdit && !options.editDisabled && rowEditable(row) && canEditFieldInline(field);
3837
+ const cellActive = inlineEdit.isCellActive(key, field.name);
3838
+ const cellDirty = editing && inlineEdit.isCellDirty(key, field.name, row);
3839
+ const showCellEditor = editable && (rowInRowEdit && editing || cellEditMode && cellActive);
3840
+ const displayRow = editing ? rowDraft : row;
3841
+ const cellClassName = [
3842
+ showCellEditor ? "nb-datagrid__edit-cell" : "nb-datagrid__data-cell",
3843
+ editable && canInlineEditMode ? "nb-datagrid__cell--editable" : "",
3844
+ cellDirty ? "nb-datagrid__cell--dirty" : "",
3845
+ cellActive ? "nb-datagrid__cell--active" : ""
3846
+ ].filter(Boolean).join(" ");
3847
+ const beginInlineEdit = () => {
3848
+ if (!editable) return;
3849
+ selectRow(row);
3850
+ if (cellEditMode) inlineEdit.startCellEdit(row, field.name);
3851
+ else if (rowInlineMode && !editing) inlineEdit.startEdit(row);
3852
+ };
3853
+ if (showCellEditor) return /* @__PURE__ */ jsx("td", {
3854
+ style: {
3855
+ width,
3856
+ textAlign: field.align
3857
+ },
3858
+ className: cellClassName,
3859
+ children: /* @__PURE__ */ jsx(InlineEditCell, {
3860
+ field,
3861
+ rowKey: key,
3862
+ draft: rowDraft,
3863
+ onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
3864
+ errors: rowFieldErrors,
3865
+ disabled: saving,
3866
+ allRemoteOptions: filterRemoteOptions,
3867
+ httpClient,
3868
+ t,
3869
+ autoFocus: true
3870
+ })
3871
+ }, field.name);
3069
3872
  return /* @__PURE__ */ jsx("td", {
3070
3873
  style: {
3071
- width: getColumnWidth(field, resolvedColWidths),
3874
+ width,
3072
3875
  textAlign: field.align
3073
3876
  },
3074
- title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
3075
- children: renderCell(field, row, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
3877
+ className: cellClassName,
3878
+ title: getCellText(field, displayRow, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
3879
+ onClick: (event) => {
3880
+ if (!editable || !canInlineEditMode) return;
3881
+ event.stopPropagation();
3882
+ beginInlineEdit();
3883
+ },
3884
+ children: renderCell(field, displayRow, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
3076
3885
  }, field.name);
3077
3886
  }),
3078
3887
  hasRowActions && /* @__PURE__ */ jsx("td", {
3079
3888
  className: "nb-datagrid__actions-cell",
3080
3889
  onClick: (e) => e.stopPropagation(),
3081
- children: /* @__PURE__ */ jsx("div", {
3890
+ children: rowInRowEdit && showRowInlineActions ? /* @__PURE__ */ jsxs("div", {
3891
+ className: "nb-datagrid__inline-actions",
3892
+ children: [/* @__PURE__ */ jsx("button", {
3893
+ type: "button",
3894
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--save",
3895
+ disabled: saving,
3896
+ "aria-label": t("grid.inlineSaveRow"),
3897
+ title: t("grid.inlineSaveRow"),
3898
+ onClick: () => void inlineEdit.saveRow(key),
3899
+ children: /* @__PURE__ */ jsx("i", {
3900
+ className: "ph ph-check",
3901
+ "aria-hidden": "true"
3902
+ })
3903
+ }), /* @__PURE__ */ jsx("button", {
3904
+ type: "button",
3905
+ className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--cancel",
3906
+ disabled: saving,
3907
+ "aria-label": t("grid.inlineCancelRow"),
3908
+ title: t("grid.inlineCancelRow"),
3909
+ onClick: () => inlineEdit.cancelEdit(key),
3910
+ children: /* @__PURE__ */ jsx("i", {
3911
+ className: "ph ph-x",
3912
+ "aria-hidden": "true"
3913
+ })
3914
+ })]
3915
+ }) : /* @__PURE__ */ jsx("div", {
3082
3916
  className: "nb-datagrid__row-actions",
3083
3917
  children: rowActions.length > 0 && /* @__PURE__ */ jsx(IconButton, {
3084
3918
  icon: "ph ph-dots-three-vertical",
@@ -3428,97 +4262,6 @@ const FORM_EVENTS = {
3428
4262
  };
3429
4263
  const FORM_ERRORS_EVENT = "form-errors";
3430
4264
  //#endregion
3431
- //#region packages/crud/adapter/HydraAdapter.ts
3432
- function trimTrailingSlash(value) {
3433
- return value.replace(/\/+$/, "");
3434
- }
3435
- function buildIRI(url, value) {
3436
- if (value.startsWith("/")) return value;
3437
- return `${url}/${value}`;
3438
- }
3439
- /**
3440
- * Default backend adapter for API Platform / JSON-LD + Hydra backends.
3441
- *
3442
- * Conventions assumed:
3443
- * - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
3444
- * - Entity fields are serialized as IRI strings in POST/PATCH bodies.
3445
- * - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
3446
- * - `_iri` is a synthetic alias for `@id` used internally by the engine.
3447
- */
3448
- const HydraAdapter = {
3449
- getRowId(record, idField) {
3450
- const direct = record[idField];
3451
- if (direct !== void 0 && direct !== null) return String(direct);
3452
- const iri = record["@id"] ?? record["_iri"];
3453
- if (iri !== void 0 && iri !== null) return String(iri);
3454
- return String(record["id"] ?? "");
3455
- },
3456
- buildItemUrl(baseUrl, id) {
3457
- const str = String(id);
3458
- if (str.startsWith("/")) return str;
3459
- return `${trimTrailingSlash(baseUrl)}/${str}`;
3460
- },
3461
- serializeEntityRef(field, rawValue) {
3462
- if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
3463
- if (typeof rawValue === "object") {
3464
- const entity = rawValue;
3465
- const atId = entity["@id"];
3466
- if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
3467
- const idValue = entity["id"];
3468
- const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
3469
- return buildIRI(field.url ?? "", resolvedId);
3470
- }
3471
- return buildIRI(field.url ?? "", String(rawValue));
3472
- },
3473
- normalizeEntityValue(rawValue, field) {
3474
- if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
3475
- if (typeof rawValue === "object" && rawValue !== null) {
3476
- const entity = rawValue;
3477
- const atId = entity["@id"];
3478
- if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
3479
- const directValue = entity[field.valueField];
3480
- if (directValue !== void 0 && directValue !== null) return directValue;
3481
- }
3482
- return rawValue;
3483
- },
3484
- getEntityOptionKey(item, field) {
3485
- if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
3486
- return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
3487
- },
3488
- parseListResponse(response) {
3489
- const r = response;
3490
- const member = r["hydra:member"];
3491
- if (Array.isArray(member)) return {
3492
- items: member,
3493
- total: Number(r["hydra:totalItems"] ?? member.length)
3494
- };
3495
- if (Array.isArray(response)) return {
3496
- items: response,
3497
- total: response.length
3498
- };
3499
- return {
3500
- items: [],
3501
- total: 0
3502
- };
3503
- },
3504
- synthesizeEntityKey(field, entityValue) {
3505
- if (!field.url) return void 0;
3506
- const base = trimTrailingSlash(field.url);
3507
- const directId = entityValue["id"];
3508
- if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
3509
- const directValue = entityValue[field.valueField];
3510
- if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
3511
- for (const key of [
3512
- "code",
3513
- "uuid",
3514
- "slug"
3515
- ]) {
3516
- const candidate = entityValue[key];
3517
- if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
3518
- }
3519
- }
3520
- };
3521
- //#endregion
3522
4265
  //#region packages/crud/form/FormDataTransform.ts
3523
4266
  function upsertPrependData(store, field, item) {
3524
4267
  const existing = store.get(field.name);
@@ -3820,59 +4563,6 @@ function buildFieldColSpanContext(options) {
3820
4563
  };
3821
4564
  }
3822
4565
  //#endregion
3823
- //#region packages/crud/form/serializeFormData.ts
3824
- function applySerializedValue(formData, field, result) {
3825
- if (result.kind === "set") formData[field.name] = result.value;
3826
- else if (result.kind === "omit") delete formData[field.name];
3827
- }
3828
- /**
3829
- * Pure serialization of form data before HTTP submission.
3830
- *
3831
- * Applies uploaded file references and computed fields, then delegates the
3832
- * per-type wire format (entity refs, business dates, numeric coercion, file
3833
- * handling, NONE stripping) to each field's Field-Type module.
3834
- *
3835
- * This function is extracted from useFormSubmit for testability.
3836
- */
3837
- function serializeFormFields(rawData, fields, ctx) {
3838
- const formData = { ...rawData };
3839
- ctx.uploadedFiles.forEach((file) => {
3840
- formData[file.name] = file.iri;
3841
- });
3842
- fields.forEach((field) => {
3843
- if (!field.computed) return;
3844
- const computedValue = field.computed(formData);
3845
- if (computedValue !== void 0) formData[field.name] = computedValue;
3846
- });
3847
- const moduleCtx = {
3848
- adapter: ctx.adapter ?? HydraAdapter,
3849
- format: ctx.format,
3850
- getFieldValue: ctx.getFieldValue
3851
- };
3852
- fields.forEach((field) => {
3853
- applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
3854
- });
3855
- return formData;
3856
- }
3857
- /**
3858
- * Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
3859
- * Pure function — operates on an array of row records.
3860
- */
3861
- function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
3862
- const details = JSON.parse(JSON.stringify(rows));
3863
- detailFields.forEach((field) => {
3864
- const typeModule = getFieldTypeModule(field.type);
3865
- details.forEach((detail) => {
3866
- applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
3867
- });
3868
- });
3869
- details.forEach((detail) => {
3870
- if (!isEditMode) delete detail[detailIdField];
3871
- else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
3872
- });
3873
- return details;
3874
- }
3875
- //#endregion
3876
4566
  //#region packages/crud/form/useFormSubmit.ts
3877
4567
  /**
3878
4568
  * Returns helpers for form serialization and HTTP submit/delete operations.
@@ -6482,6 +7172,8 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6482
7172
  selectionMode: hasMultipleSelection ? "multiple" : "single",
6483
7173
  onSelectionChanged: handleSelectionChanged,
6484
7174
  editMode: resolvedResource.editMode,
7175
+ inlineEditToolbar: resolvedResource.inlineEditToolbar,
7176
+ inlineRowActions: resolvedResource.inlineRowActions,
6485
7177
  visibleColumns: presetState.visibleColumns,
6486
7178
  beforeToolbar: renderPresetSelector,
6487
7179
  aboveGrid: aboveGridContent,