@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 +925 -230
- package/dist/index.d.cts +27 -1
- package/dist/index.d.mts +27 -1
- package/dist/index.mjs +866 -174
- package/dist/style.css +258 -2
- package/package.json +17 -3
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
|
|
1592
|
+
cellText: (_field, value) => stripTags(value),
|
|
1437
1593
|
serializeFormValue: () => KEEP,
|
|
1438
1594
|
serializeDetailValue: () => KEEP,
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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
|
-
|
|
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 +=
|
|
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) &&
|
|
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
|
|
2606
|
-
const
|
|
2607
|
-
const
|
|
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 =
|
|
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:
|
|
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: () =>
|
|
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 === "
|
|
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
|
|
3874
|
+
width,
|
|
3072
3875
|
textAlign: field.align
|
|
3073
3876
|
},
|
|
3074
|
-
|
|
3075
|
-
|
|
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__ */
|
|
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,
|