@nubitio/crud 0.5.24 → 0.5.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +692 -220
- package/dist/index.mjs +633 -164
- package/dist/style.css +137 -0
- 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,337 @@ function DetailGridSection({ fields, url }) {
|
|
|
1838
2007
|
});
|
|
1839
2008
|
}
|
|
1840
2009
|
//#endregion
|
|
2010
|
+
//#region packages/crud/adapter/HydraAdapter.ts
|
|
2011
|
+
function trimTrailingSlash(value) {
|
|
2012
|
+
return value.replace(/\/+$/, "");
|
|
2013
|
+
}
|
|
2014
|
+
function buildIRI(url, value) {
|
|
2015
|
+
if (value.startsWith("/")) return value;
|
|
2016
|
+
return `${url}/${value}`;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Default backend adapter for API Platform / JSON-LD + Hydra backends.
|
|
2020
|
+
*
|
|
2021
|
+
* Conventions assumed:
|
|
2022
|
+
* - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
|
|
2023
|
+
* - Entity fields are serialized as IRI strings in POST/PATCH bodies.
|
|
2024
|
+
* - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
|
|
2025
|
+
* - `_iri` is a synthetic alias for `@id` used internally by the engine.
|
|
2026
|
+
*/
|
|
2027
|
+
const HydraAdapter = {
|
|
2028
|
+
getRowId(record, idField) {
|
|
2029
|
+
const direct = record[idField];
|
|
2030
|
+
if (direct !== void 0 && direct !== null) return String(direct);
|
|
2031
|
+
const iri = record["@id"] ?? record["_iri"];
|
|
2032
|
+
if (iri !== void 0 && iri !== null) return String(iri);
|
|
2033
|
+
return String(record["id"] ?? "");
|
|
2034
|
+
},
|
|
2035
|
+
buildItemUrl(baseUrl, id) {
|
|
2036
|
+
const str = String(id);
|
|
2037
|
+
if (str.startsWith("/")) return str;
|
|
2038
|
+
return `${trimTrailingSlash(baseUrl)}/${str}`;
|
|
2039
|
+
},
|
|
2040
|
+
serializeEntityRef(field, rawValue) {
|
|
2041
|
+
if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
|
|
2042
|
+
if (typeof rawValue === "object") {
|
|
2043
|
+
const entity = rawValue;
|
|
2044
|
+
const atId = entity["@id"];
|
|
2045
|
+
if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
|
|
2046
|
+
const idValue = entity["id"];
|
|
2047
|
+
const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
|
|
2048
|
+
return buildIRI(field.url ?? "", resolvedId);
|
|
2049
|
+
}
|
|
2050
|
+
return buildIRI(field.url ?? "", String(rawValue));
|
|
2051
|
+
},
|
|
2052
|
+
normalizeEntityValue(rawValue, field) {
|
|
2053
|
+
if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
|
|
2054
|
+
if (typeof rawValue === "object" && rawValue !== null) {
|
|
2055
|
+
const entity = rawValue;
|
|
2056
|
+
const atId = entity["@id"];
|
|
2057
|
+
if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
|
|
2058
|
+
const directValue = entity[field.valueField];
|
|
2059
|
+
if (directValue !== void 0 && directValue !== null) return directValue;
|
|
2060
|
+
}
|
|
2061
|
+
return rawValue;
|
|
2062
|
+
},
|
|
2063
|
+
getEntityOptionKey(item, field) {
|
|
2064
|
+
if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
|
|
2065
|
+
return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
|
|
2066
|
+
},
|
|
2067
|
+
parseListResponse(response) {
|
|
2068
|
+
const r = response;
|
|
2069
|
+
const member = r["hydra:member"];
|
|
2070
|
+
if (Array.isArray(member)) return {
|
|
2071
|
+
items: member,
|
|
2072
|
+
total: Number(r["hydra:totalItems"] ?? member.length)
|
|
2073
|
+
};
|
|
2074
|
+
if (Array.isArray(response)) return {
|
|
2075
|
+
items: response,
|
|
2076
|
+
total: response.length
|
|
2077
|
+
};
|
|
2078
|
+
return {
|
|
2079
|
+
items: [],
|
|
2080
|
+
total: 0
|
|
2081
|
+
};
|
|
2082
|
+
},
|
|
2083
|
+
synthesizeEntityKey(field, entityValue) {
|
|
2084
|
+
if (!field.url) return void 0;
|
|
2085
|
+
const base = trimTrailingSlash(field.url);
|
|
2086
|
+
const directId = entityValue["id"];
|
|
2087
|
+
if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
|
|
2088
|
+
const directValue = entityValue[field.valueField];
|
|
2089
|
+
if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
|
|
2090
|
+
for (const key of [
|
|
2091
|
+
"code",
|
|
2092
|
+
"uuid",
|
|
2093
|
+
"slug"
|
|
2094
|
+
]) {
|
|
2095
|
+
const candidate = entityValue[key];
|
|
2096
|
+
if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
//#endregion
|
|
2101
|
+
//#region packages/crud/form/serializeFormData.ts
|
|
2102
|
+
function applySerializedValue(formData, field, result) {
|
|
2103
|
+
if (result.kind === "set") formData[field.name] = result.value;
|
|
2104
|
+
else if (result.kind === "omit") delete formData[field.name];
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Pure serialization of form data before HTTP submission.
|
|
2108
|
+
*
|
|
2109
|
+
* Applies uploaded file references and computed fields, then delegates the
|
|
2110
|
+
* per-type wire format (entity refs, business dates, numeric coercion, file
|
|
2111
|
+
* handling, NONE stripping) to each field's Field-Type module.
|
|
2112
|
+
*
|
|
2113
|
+
* This function is extracted from useFormSubmit for testability.
|
|
2114
|
+
*/
|
|
2115
|
+
function serializeFormFields(rawData, fields, ctx) {
|
|
2116
|
+
const formData = { ...rawData };
|
|
2117
|
+
ctx.uploadedFiles.forEach((file) => {
|
|
2118
|
+
formData[file.name] = file.iri;
|
|
2119
|
+
});
|
|
2120
|
+
fields.forEach((field) => {
|
|
2121
|
+
if (!field.computed) return;
|
|
2122
|
+
const computedValue = field.computed(formData);
|
|
2123
|
+
if (computedValue !== void 0) formData[field.name] = computedValue;
|
|
2124
|
+
});
|
|
2125
|
+
const moduleCtx = {
|
|
2126
|
+
adapter: ctx.adapter ?? HydraAdapter,
|
|
2127
|
+
format: ctx.format,
|
|
2128
|
+
getFieldValue: ctx.getFieldValue
|
|
2129
|
+
};
|
|
2130
|
+
fields.forEach((field) => {
|
|
2131
|
+
applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
|
|
2132
|
+
});
|
|
2133
|
+
return formData;
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
|
|
2137
|
+
* Pure function — operates on an array of row records.
|
|
2138
|
+
*/
|
|
2139
|
+
function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
|
|
2140
|
+
const details = structuredClone(rows);
|
|
2141
|
+
detailFields.forEach((field) => {
|
|
2142
|
+
const typeModule = getFieldTypeModule(field.type);
|
|
2143
|
+
details.forEach((detail) => {
|
|
2144
|
+
applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
|
|
2145
|
+
});
|
|
2146
|
+
});
|
|
2147
|
+
details.forEach((detail) => {
|
|
2148
|
+
if (!isEditMode) delete detail[detailIdField];
|
|
2149
|
+
else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
|
|
2150
|
+
});
|
|
2151
|
+
return details;
|
|
2152
|
+
}
|
|
2153
|
+
//#endregion
|
|
2154
|
+
//#region packages/crud/datagrid/useInlineEdit.ts
|
|
2155
|
+
/** Fields that are safe to edit inline (identity/readonly/file types are excluded). */
|
|
2156
|
+
function canEditFieldInline(field) {
|
|
2157
|
+
return !field.isIdentity && !field.readonly && field.type !== "file" && field.visibleOnForm !== false;
|
|
2158
|
+
}
|
|
2159
|
+
function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError }) {
|
|
2160
|
+
const [draftRows, setDraftRows] = useState(/* @__PURE__ */ new Map());
|
|
2161
|
+
const [savingRows, setSavingRows] = useState(/* @__PURE__ */ new Set());
|
|
2162
|
+
const [rowErrors, setRowErrors] = useState(/* @__PURE__ */ new Map());
|
|
2163
|
+
const draftRowsRef = useRef(draftRows);
|
|
2164
|
+
draftRowsRef.current = draftRows;
|
|
2165
|
+
const optsRef = useRef({
|
|
2166
|
+
url,
|
|
2167
|
+
idField,
|
|
2168
|
+
adapter,
|
|
2169
|
+
httpClient,
|
|
2170
|
+
fields,
|
|
2171
|
+
onSaveSuccess,
|
|
2172
|
+
onSaveError
|
|
2173
|
+
});
|
|
2174
|
+
optsRef.current = {
|
|
2175
|
+
url,
|
|
2176
|
+
idField,
|
|
2177
|
+
adapter,
|
|
2178
|
+
httpClient,
|
|
2179
|
+
fields,
|
|
2180
|
+
onSaveSuccess,
|
|
2181
|
+
onSaveError
|
|
2182
|
+
};
|
|
2183
|
+
const isEditing = useCallback((key) => draftRowsRef.current.has(key), []);
|
|
2184
|
+
const startEdit = useCallback((row) => {
|
|
2185
|
+
const key = row[optsRef.current.idField];
|
|
2186
|
+
setDraftRows((prev) => {
|
|
2187
|
+
const base = mode === "row" ? [] : Array.from(prev.entries());
|
|
2188
|
+
return new Map([...base, [key, { ...row }]]);
|
|
2189
|
+
});
|
|
2190
|
+
setRowErrors((prev) => {
|
|
2191
|
+
const next = new Map(prev);
|
|
2192
|
+
next.delete(key);
|
|
2193
|
+
return next;
|
|
2194
|
+
});
|
|
2195
|
+
}, [mode]);
|
|
2196
|
+
const cancelEdit = useCallback((key) => {
|
|
2197
|
+
setDraftRows((prev) => {
|
|
2198
|
+
const next = new Map(prev);
|
|
2199
|
+
next.delete(key);
|
|
2200
|
+
return next;
|
|
2201
|
+
});
|
|
2202
|
+
setRowErrors((prev) => {
|
|
2203
|
+
const next = new Map(prev);
|
|
2204
|
+
next.delete(key);
|
|
2205
|
+
return next;
|
|
2206
|
+
});
|
|
2207
|
+
}, []);
|
|
2208
|
+
const discardAll = useCallback(() => {
|
|
2209
|
+
setDraftRows(/* @__PURE__ */ new Map());
|
|
2210
|
+
setRowErrors(/* @__PURE__ */ new Map());
|
|
2211
|
+
}, []);
|
|
2212
|
+
const updateDraft = useCallback((key, fieldName, value) => {
|
|
2213
|
+
setDraftRows((prev) => {
|
|
2214
|
+
const current = prev.get(key);
|
|
2215
|
+
if (!current) return prev;
|
|
2216
|
+
const next = new Map(prev);
|
|
2217
|
+
next.set(key, {
|
|
2218
|
+
...current,
|
|
2219
|
+
[fieldName]: value
|
|
2220
|
+
});
|
|
2221
|
+
return next;
|
|
2222
|
+
});
|
|
2223
|
+
}, []);
|
|
2224
|
+
const doSaveRow = useCallback(async (key) => {
|
|
2225
|
+
const { url: u, adapter: a, httpClient: http, fields: fs, onSaveError: onErr } = optsRef.current;
|
|
2226
|
+
const draft = draftRowsRef.current.get(key);
|
|
2227
|
+
if (!draft) return false;
|
|
2228
|
+
const errors = {};
|
|
2229
|
+
fs.forEach((field) => {
|
|
2230
|
+
if (!canEditFieldInline(field)) return;
|
|
2231
|
+
if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
|
|
2232
|
+
});
|
|
2233
|
+
if (Object.keys(errors).length > 0) {
|
|
2234
|
+
setRowErrors((prev) => new Map(prev).set(key, errors));
|
|
2235
|
+
return false;
|
|
2236
|
+
}
|
|
2237
|
+
setSavingRows((prev) => new Set([...prev, key]));
|
|
2238
|
+
const serialized = serializeFormFields(draft, fs.filter(canEditFieldInline), {
|
|
2239
|
+
uploadedFiles: [],
|
|
2240
|
+
getFieldValue: (name) => draft[name]
|
|
2241
|
+
});
|
|
2242
|
+
try {
|
|
2243
|
+
await http.patch(a.buildItemUrl(u, key), serialized);
|
|
2244
|
+
setDraftRows((prev) => {
|
|
2245
|
+
const next = new Map(prev);
|
|
2246
|
+
next.delete(key);
|
|
2247
|
+
return next;
|
|
2248
|
+
});
|
|
2249
|
+
setRowErrors((prev) => {
|
|
2250
|
+
const next = new Map(prev);
|
|
2251
|
+
next.delete(key);
|
|
2252
|
+
return next;
|
|
2253
|
+
});
|
|
2254
|
+
return true;
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
onErr?.(key, err);
|
|
2257
|
+
if (typeof err === "object" && err !== null && "status" in err && err.status === 422 && "data" in err) {
|
|
2258
|
+
const data = err.data;
|
|
2259
|
+
if (typeof data === "object" && data !== null && "violations" in data) {
|
|
2260
|
+
const violations = data.violations ?? [];
|
|
2261
|
+
const fieldErrors = {};
|
|
2262
|
+
violations.forEach((v) => {
|
|
2263
|
+
fieldErrors[v.propertyPath] = v.message;
|
|
2264
|
+
});
|
|
2265
|
+
setRowErrors((prev) => new Map(prev).set(key, fieldErrors));
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return false;
|
|
2269
|
+
} finally {
|
|
2270
|
+
setSavingRows((prev) => {
|
|
2271
|
+
const next = new Set(prev);
|
|
2272
|
+
next.delete(key);
|
|
2273
|
+
return next;
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
}, []);
|
|
2277
|
+
return {
|
|
2278
|
+
draftRows,
|
|
2279
|
+
savingRows,
|
|
2280
|
+
rowErrors,
|
|
2281
|
+
isEditing,
|
|
2282
|
+
startEdit,
|
|
2283
|
+
cancelEdit,
|
|
2284
|
+
discardAll,
|
|
2285
|
+
updateDraft,
|
|
2286
|
+
saveRow: useCallback(async (key) => {
|
|
2287
|
+
if (await doSaveRow(key)) optsRef.current.onSaveSuccess?.();
|
|
2288
|
+
}, [doSaveRow]),
|
|
2289
|
+
saveAll: useCallback(async () => {
|
|
2290
|
+
const keys = Array.from(draftRowsRef.current.keys());
|
|
2291
|
+
if ((await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value)) optsRef.current.onSaveSuccess?.();
|
|
2292
|
+
}, [doSaveRow])
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
//#endregion
|
|
2296
|
+
//#region packages/crud/datagrid/InlineEditCell.tsx
|
|
2297
|
+
/**
|
|
2298
|
+
* Renders a single cell as an editable control during inline row editing.
|
|
2299
|
+
* Uses the same FieldTypeModule.ControlRender path as the full form, but
|
|
2300
|
+
* with a compact common-props class so controls fit inside a table cell.
|
|
2301
|
+
*/
|
|
2302
|
+
function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t }) {
|
|
2303
|
+
const typeModule = getFieldTypeModule(field.type);
|
|
2304
|
+
const fieldError = errors?.[field.name];
|
|
2305
|
+
const errorClass = fieldError ? " is-error" : "";
|
|
2306
|
+
const commonProps = {
|
|
2307
|
+
className: `nb-inline-control${errorClass}`,
|
|
2308
|
+
disabled,
|
|
2309
|
+
id: `iec-${String(rowKey)}-${field.name}`,
|
|
2310
|
+
name: field.name,
|
|
2311
|
+
onClick: void 0,
|
|
2312
|
+
readOnly: false,
|
|
2313
|
+
required: field.required
|
|
2314
|
+
};
|
|
2315
|
+
const ctx = {
|
|
2316
|
+
httpClient,
|
|
2317
|
+
t,
|
|
2318
|
+
remoteOptions: allRemoteOptions,
|
|
2319
|
+
getPrependData: () => void 0,
|
|
2320
|
+
getFieldValue: (name) => draft[name],
|
|
2321
|
+
getExistingMedia: () => null,
|
|
2322
|
+
clearExistingMedia: () => {},
|
|
2323
|
+
upsertUploadedFile: () => {}
|
|
2324
|
+
};
|
|
2325
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2326
|
+
className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
|
|
2327
|
+
children: typeModule.ControlRender({
|
|
2328
|
+
field,
|
|
2329
|
+
value: draft[field.name],
|
|
2330
|
+
error: fieldError,
|
|
2331
|
+
errorClass,
|
|
2332
|
+
disabled,
|
|
2333
|
+
readOnly: false,
|
|
2334
|
+
commonProps,
|
|
2335
|
+
setFieldValue: onChange,
|
|
2336
|
+
ctx
|
|
2337
|
+
})
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
//#endregion
|
|
1841
2341
|
//#region packages/crud/summary/SummaryUtils.ts
|
|
1842
2342
|
function toFiniteNumber(value) {
|
|
1843
2343
|
if (value === null || value === void 0 || value === "") return null;
|
|
@@ -1907,19 +2407,20 @@ function resolveSummaryText(rows, item) {
|
|
|
1907
2407
|
const DETAIL_COL_WIDTH = 36;
|
|
1908
2408
|
const CHECKBOX_COL_WIDTH = 36;
|
|
1909
2409
|
const ACTIONS_COL_WIDTH = 44;
|
|
2410
|
+
const INLINE_ACTIONS_COL_WIDTH = 72;
|
|
1910
2411
|
const DEFAULT_COL_WIDTH = 120;
|
|
1911
2412
|
const MIN_COL_WIDTH = 48;
|
|
1912
2413
|
function getColumnWidth(field, colWidths) {
|
|
1913
2414
|
return colWidths[field.name] ?? field.width ?? field.minWidth ?? DEFAULT_COL_WIDTH;
|
|
1914
2415
|
}
|
|
1915
|
-
function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth }) {
|
|
2416
|
+
function computeLayoutWidth({ visibleFields, colWidths, hasCheckbox, hasDetail, hasRowActions, containerWidth, actionsColWidth = ACTIONS_COL_WIDTH }) {
|
|
1916
2417
|
let total = 0;
|
|
1917
2418
|
if (hasDetail) total += DETAIL_COL_WIDTH;
|
|
1918
2419
|
if (hasCheckbox) total += CHECKBOX_COL_WIDTH;
|
|
1919
2420
|
visibleFields.forEach((field) => {
|
|
1920
2421
|
total += getColumnWidth(field, colWidths);
|
|
1921
2422
|
});
|
|
1922
|
-
if (hasRowActions) total +=
|
|
2423
|
+
if (hasRowActions) total += actionsColWidth;
|
|
1923
2424
|
return Math.max(containerWidth, total);
|
|
1924
2425
|
}
|
|
1925
2426
|
function GridColumnGroup({ fields, colWidths, hasCheckbox, hasDetail, hasRowActions }) {
|
|
@@ -2302,9 +2803,11 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2302
2803
|
]);
|
|
2303
2804
|
const fieldsRef = useRef(options.fields);
|
|
2304
2805
|
fieldsRef.current = options.fields;
|
|
2806
|
+
const loadSeqRef = useRef(0);
|
|
2305
2807
|
const loadRows = useCallback(async () => {
|
|
2306
2808
|
if (options.manualLoad) return rowsRef.current;
|
|
2307
2809
|
setIsGridLoading(true);
|
|
2810
|
+
const seq = ++loadSeqRef.current;
|
|
2308
2811
|
const loadOptions = {
|
|
2309
2812
|
filter: buildFilterExpression(filters, filterOperators, fieldsRef.current),
|
|
2310
2813
|
sort
|
|
@@ -2314,6 +2817,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2314
2817
|
loadOptions.take = pageSize;
|
|
2315
2818
|
}
|
|
2316
2819
|
const result = await source.load(loadOptions);
|
|
2820
|
+
if (seq !== loadSeqRef.current) return rowsRef.current;
|
|
2317
2821
|
rowsRef.current = result.data;
|
|
2318
2822
|
setRows(result.data);
|
|
2319
2823
|
setTotalCount(result.totalCount);
|
|
@@ -2367,6 +2871,17 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2367
2871
|
resourceStoreFactory,
|
|
2368
2872
|
visibleFields
|
|
2369
2873
|
]);
|
|
2874
|
+
const canInlineEditMode = options.editMode === "row" || options.editMode === "batch";
|
|
2875
|
+
const inlineEdit = useInlineEdit({
|
|
2876
|
+
mode: options.editMode === "batch" ? "batch" : "row",
|
|
2877
|
+
url: options.url,
|
|
2878
|
+
idField,
|
|
2879
|
+
adapter: options.adapter,
|
|
2880
|
+
httpClient,
|
|
2881
|
+
fields: options.fields,
|
|
2882
|
+
onSaveSuccess: () => void loadRows(),
|
|
2883
|
+
onSaveError: () => {}
|
|
2884
|
+
});
|
|
2370
2885
|
const handleStateRef = useRef({
|
|
2371
2886
|
selectedKeys,
|
|
2372
2887
|
filters,
|
|
@@ -2433,7 +2948,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2433
2948
|
const rowEditable = (row) => options.canEditRow?.(row) !== false;
|
|
2434
2949
|
const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
|
|
2435
2950
|
const buildRowActions = (row) => [
|
|
2436
|
-
...options.allowEdit && rowEditable(row) &&
|
|
2951
|
+
...options.allowEdit && rowEditable(row) && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
|
|
2952
|
+
text: t("grid.inlineEditRow"),
|
|
2953
|
+
icon: "ph-pencil-simple",
|
|
2954
|
+
disabled: options.editDisabled,
|
|
2955
|
+
onClick: () => inlineEdit.startEdit(row)
|
|
2956
|
+
}] : [],
|
|
2957
|
+
...options.allowEdit && rowEditable(row) && !canInlineEditMode && (options.onEdit || options.events?.EDIT) ? [{
|
|
2437
2958
|
text: t("grid.buttonEdit"),
|
|
2438
2959
|
icon: "ph-pencil-simple",
|
|
2439
2960
|
disabled: options.editDisabled,
|
|
@@ -2457,6 +2978,10 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2457
2978
|
}] : []
|
|
2458
2979
|
];
|
|
2459
2980
|
const openRow = (row) => {
|
|
2981
|
+
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
2982
|
+
inlineEdit.startEdit(row);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2460
2985
|
if (options.allowEdit && rowEditable(row) && (options.onEdit || options.events?.EDIT)) {
|
|
2461
2986
|
if (options.onEdit) options.onEdit(row);
|
|
2462
2987
|
else emit(options.events.EDIT, { row });
|
|
@@ -2588,22 +3113,24 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2588
3113
|
};
|
|
2589
3114
|
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
2590
3115
|
const hasCheckbox = options.selectionMode === "multiple";
|
|
2591
|
-
const hasBuiltInRowActions = Boolean(options.allowEdit && (options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
|
|
3116
|
+
const hasBuiltInRowActions = Boolean(options.allowEdit && (canInlineEditMode || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
|
|
2592
3117
|
const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
|
|
2593
3118
|
const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
|
|
2594
3119
|
const hasDetail = Boolean(options.detailFields);
|
|
3120
|
+
const actionsColWidth = canInlineEditMode ? INLINE_ACTIONS_COL_WIDTH : ACTIONS_COL_WIDTH;
|
|
2595
3121
|
const layoutWidth = computeLayoutWidth({
|
|
2596
3122
|
visibleFields,
|
|
2597
3123
|
colWidths,
|
|
2598
3124
|
hasCheckbox,
|
|
2599
3125
|
hasDetail,
|
|
2600
3126
|
hasRowActions,
|
|
2601
|
-
containerWidth
|
|
3127
|
+
containerWidth,
|
|
3128
|
+
actionsColWidth
|
|
2602
3129
|
});
|
|
2603
3130
|
const resolvedColWidths = useMemo(() => {
|
|
2604
3131
|
if (visibleFields.length === 0) return colWidths;
|
|
2605
3132
|
const dataTotal = visibleFields.reduce((sum, f) => sum + getColumnWidth(f, colWidths), 0);
|
|
2606
|
-
const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ?
|
|
3133
|
+
const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
|
|
2607
3134
|
const extra = Math.max(0, containerWidth - dataTotal - fixedTotal);
|
|
2608
3135
|
if (extra === 0 || dataTotal === 0) return colWidths;
|
|
2609
3136
|
let distributed = 0;
|
|
@@ -2707,6 +3234,27 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2707
3234
|
})
|
|
2708
3235
|
]
|
|
2709
3236
|
}),
|
|
3237
|
+
options.editMode === "batch" && inlineEdit.draftRows.size > 0 && /* @__PURE__ */ jsxs("div", {
|
|
3238
|
+
className: "nb-datagrid__batch-bar",
|
|
3239
|
+
role: "status",
|
|
3240
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
3241
|
+
className: "nb-datagrid__batch-bar-label",
|
|
3242
|
+
children: t("grid.inlineUnsavedRows", { count: inlineEdit.draftRows.size })
|
|
3243
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
3244
|
+
className: "nb-datagrid__batch-bar-actions",
|
|
3245
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
3246
|
+
type: "button",
|
|
3247
|
+
className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--discard",
|
|
3248
|
+
onClick: () => inlineEdit.discardAll(),
|
|
3249
|
+
children: t("grid.inlineDiscardAll")
|
|
3250
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
3251
|
+
type: "button",
|
|
3252
|
+
className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--save",
|
|
3253
|
+
onClick: () => void inlineEdit.saveAll(),
|
|
3254
|
+
children: t("grid.inlineSaveAll", { count: inlineEdit.draftRows.size })
|
|
3255
|
+
})]
|
|
3256
|
+
})]
|
|
3257
|
+
}),
|
|
2710
3258
|
options.aboveGrid ? /* @__PURE__ */ jsx("div", {
|
|
2711
3259
|
className: "nb-datagrid__above-grid",
|
|
2712
3260
|
children: options.aboveGrid
|
|
@@ -3000,16 +3548,33 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3000
3548
|
}) }) : rows.map((row, rowIndex) => {
|
|
3001
3549
|
const key = row[idField] ?? rowIndex;
|
|
3002
3550
|
const selected = selectedKeys.includes(key);
|
|
3551
|
+
const editing = inlineEdit.draftRows.has(key);
|
|
3552
|
+
const saving = inlineEdit.savingRows.has(key);
|
|
3553
|
+
const rowDraft = inlineEdit.draftRows.get(key) ?? row;
|
|
3554
|
+
const rowFieldErrors = inlineEdit.rowErrors.get(key);
|
|
3003
3555
|
const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
|
|
3004
3556
|
const detailUrl = options.detailUrl?.replace("{id}", String(key));
|
|
3005
3557
|
const expanded = expandedKeys.has(key);
|
|
3006
3558
|
const rowActions = buildRowActions(row);
|
|
3007
3559
|
return /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("tr", {
|
|
3008
|
-
className:
|
|
3560
|
+
className: [
|
|
3561
|
+
"nb-datagrid__row",
|
|
3562
|
+
expanded ? "nb-datagrid__row--expanded" : "",
|
|
3563
|
+
selected ? "nb-datagrid__row--selected" : "",
|
|
3564
|
+
editing ? "nb-datagrid__row--editing" : "",
|
|
3565
|
+
saving ? "nb-datagrid__row--saving" : ""
|
|
3566
|
+
].filter(Boolean).join(" "),
|
|
3009
3567
|
tabIndex: 0,
|
|
3010
3568
|
"aria-selected": selected,
|
|
3011
|
-
onClick: () =>
|
|
3569
|
+
onClick: () => {
|
|
3570
|
+
if (!editing) selectRow(row);
|
|
3571
|
+
},
|
|
3012
3572
|
onDoubleClick: () => {
|
|
3573
|
+
if (editing) return;
|
|
3574
|
+
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3575
|
+
inlineEdit.startEdit(row);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3013
3578
|
if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
|
|
3014
3579
|
if (options.onEdit) options.onEdit(row);
|
|
3015
3580
|
else emit(options.events.EDIT, { row });
|
|
@@ -3018,10 +3583,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3018
3583
|
if (options.allowView && options.onView) options.onView(row);
|
|
3019
3584
|
},
|
|
3020
3585
|
onKeyDown: (event) => {
|
|
3021
|
-
if (event.key === "
|
|
3586
|
+
if (editing && event.key === "Escape") inlineEdit.cancelEdit(key);
|
|
3587
|
+
else if (editing && event.key === "Enter") inlineEdit.saveRow(key);
|
|
3588
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
|
|
3589
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
|
|
3022
3590
|
else emit(options.events.EDIT, { row });
|
|
3023
|
-
else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
3024
|
-
else if (event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
|
|
3591
|
+
else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
3592
|
+
else if (!editing && event.key === "Delete" && options.allowDelete && (options.onDelete || options.events?.DELETE)) setConfirmRow(row);
|
|
3025
3593
|
else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
3026
3594
|
event.preventDefault();
|
|
3027
3595
|
const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
|
|
@@ -3066,9 +3634,29 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3066
3634
|
})
|
|
3067
3635
|
}),
|
|
3068
3636
|
visibleFields.map((field, columnIndex) => {
|
|
3637
|
+
const width = getColumnWidth(field, resolvedColWidths);
|
|
3638
|
+
if (editing && canEditFieldInline(field)) return /* @__PURE__ */ jsx("td", {
|
|
3639
|
+
style: {
|
|
3640
|
+
width,
|
|
3641
|
+
textAlign: field.align
|
|
3642
|
+
},
|
|
3643
|
+
className: "nb-datagrid__edit-cell",
|
|
3644
|
+
onClick: (e) => e.stopPropagation(),
|
|
3645
|
+
children: /* @__PURE__ */ jsx(InlineEditCell, {
|
|
3646
|
+
field,
|
|
3647
|
+
rowKey: key,
|
|
3648
|
+
draft: rowDraft,
|
|
3649
|
+
onChange: (name, value) => inlineEdit.updateDraft(key, name, value),
|
|
3650
|
+
errors: rowFieldErrors,
|
|
3651
|
+
disabled: saving,
|
|
3652
|
+
allRemoteOptions: filterRemoteOptions,
|
|
3653
|
+
httpClient,
|
|
3654
|
+
t
|
|
3655
|
+
})
|
|
3656
|
+
}, field.name);
|
|
3069
3657
|
return /* @__PURE__ */ jsx("td", {
|
|
3070
3658
|
style: {
|
|
3071
|
-
width
|
|
3659
|
+
width,
|
|
3072
3660
|
textAlign: field.align
|
|
3073
3661
|
},
|
|
3074
3662
|
title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
|
|
@@ -3078,7 +3666,32 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3078
3666
|
hasRowActions && /* @__PURE__ */ jsx("td", {
|
|
3079
3667
|
className: "nb-datagrid__actions-cell",
|
|
3080
3668
|
onClick: (e) => e.stopPropagation(),
|
|
3081
|
-
children: /* @__PURE__ */
|
|
3669
|
+
children: editing ? /* @__PURE__ */ jsxs("div", {
|
|
3670
|
+
className: "nb-datagrid__inline-actions",
|
|
3671
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
3672
|
+
type: "button",
|
|
3673
|
+
className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--save",
|
|
3674
|
+
disabled: saving,
|
|
3675
|
+
"aria-label": t("grid.inlineSaveRow"),
|
|
3676
|
+
title: t("grid.inlineSaveRow"),
|
|
3677
|
+
onClick: () => void inlineEdit.saveRow(key),
|
|
3678
|
+
children: /* @__PURE__ */ jsx("i", {
|
|
3679
|
+
className: "ph ph-check",
|
|
3680
|
+
"aria-hidden": "true"
|
|
3681
|
+
})
|
|
3682
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
3683
|
+
type: "button",
|
|
3684
|
+
className: "nb-datagrid__inline-btn nb-datagrid__inline-btn--cancel",
|
|
3685
|
+
disabled: saving,
|
|
3686
|
+
"aria-label": t("grid.inlineCancelRow"),
|
|
3687
|
+
title: t("grid.inlineCancelRow"),
|
|
3688
|
+
onClick: () => inlineEdit.cancelEdit(key),
|
|
3689
|
+
children: /* @__PURE__ */ jsx("i", {
|
|
3690
|
+
className: "ph ph-x",
|
|
3691
|
+
"aria-hidden": "true"
|
|
3692
|
+
})
|
|
3693
|
+
})]
|
|
3694
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
3082
3695
|
className: "nb-datagrid__row-actions",
|
|
3083
3696
|
children: rowActions.length > 0 && /* @__PURE__ */ jsx(IconButton, {
|
|
3084
3697
|
icon: "ph ph-dots-three-vertical",
|
|
@@ -3428,97 +4041,6 @@ const FORM_EVENTS = {
|
|
|
3428
4041
|
};
|
|
3429
4042
|
const FORM_ERRORS_EVENT = "form-errors";
|
|
3430
4043
|
//#endregion
|
|
3431
|
-
//#region packages/crud/adapter/HydraAdapter.ts
|
|
3432
|
-
function trimTrailingSlash(value) {
|
|
3433
|
-
return value.replace(/\/+$/, "");
|
|
3434
|
-
}
|
|
3435
|
-
function buildIRI(url, value) {
|
|
3436
|
-
if (value.startsWith("/")) return value;
|
|
3437
|
-
return `${url}/${value}`;
|
|
3438
|
-
}
|
|
3439
|
-
/**
|
|
3440
|
-
* Default backend adapter for API Platform / JSON-LD + Hydra backends.
|
|
3441
|
-
*
|
|
3442
|
-
* Conventions assumed:
|
|
3443
|
-
* - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
|
|
3444
|
-
* - Entity fields are serialized as IRI strings in POST/PATCH bodies.
|
|
3445
|
-
* - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
|
|
3446
|
-
* - `_iri` is a synthetic alias for `@id` used internally by the engine.
|
|
3447
|
-
*/
|
|
3448
|
-
const HydraAdapter = {
|
|
3449
|
-
getRowId(record, idField) {
|
|
3450
|
-
const direct = record[idField];
|
|
3451
|
-
if (direct !== void 0 && direct !== null) return String(direct);
|
|
3452
|
-
const iri = record["@id"] ?? record["_iri"];
|
|
3453
|
-
if (iri !== void 0 && iri !== null) return String(iri);
|
|
3454
|
-
return String(record["id"] ?? "");
|
|
3455
|
-
},
|
|
3456
|
-
buildItemUrl(baseUrl, id) {
|
|
3457
|
-
const str = String(id);
|
|
3458
|
-
if (str.startsWith("/")) return str;
|
|
3459
|
-
return `${trimTrailingSlash(baseUrl)}/${str}`;
|
|
3460
|
-
},
|
|
3461
|
-
serializeEntityRef(field, rawValue) {
|
|
3462
|
-
if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
|
|
3463
|
-
if (typeof rawValue === "object") {
|
|
3464
|
-
const entity = rawValue;
|
|
3465
|
-
const atId = entity["@id"];
|
|
3466
|
-
if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
|
|
3467
|
-
const idValue = entity["id"];
|
|
3468
|
-
const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
|
|
3469
|
-
return buildIRI(field.url ?? "", resolvedId);
|
|
3470
|
-
}
|
|
3471
|
-
return buildIRI(field.url ?? "", String(rawValue));
|
|
3472
|
-
},
|
|
3473
|
-
normalizeEntityValue(rawValue, field) {
|
|
3474
|
-
if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
|
|
3475
|
-
if (typeof rawValue === "object" && rawValue !== null) {
|
|
3476
|
-
const entity = rawValue;
|
|
3477
|
-
const atId = entity["@id"];
|
|
3478
|
-
if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
|
|
3479
|
-
const directValue = entity[field.valueField];
|
|
3480
|
-
if (directValue !== void 0 && directValue !== null) return directValue;
|
|
3481
|
-
}
|
|
3482
|
-
return rawValue;
|
|
3483
|
-
},
|
|
3484
|
-
getEntityOptionKey(item, field) {
|
|
3485
|
-
if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
|
|
3486
|
-
return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
|
|
3487
|
-
},
|
|
3488
|
-
parseListResponse(response) {
|
|
3489
|
-
const r = response;
|
|
3490
|
-
const member = r["hydra:member"];
|
|
3491
|
-
if (Array.isArray(member)) return {
|
|
3492
|
-
items: member,
|
|
3493
|
-
total: Number(r["hydra:totalItems"] ?? member.length)
|
|
3494
|
-
};
|
|
3495
|
-
if (Array.isArray(response)) return {
|
|
3496
|
-
items: response,
|
|
3497
|
-
total: response.length
|
|
3498
|
-
};
|
|
3499
|
-
return {
|
|
3500
|
-
items: [],
|
|
3501
|
-
total: 0
|
|
3502
|
-
};
|
|
3503
|
-
},
|
|
3504
|
-
synthesizeEntityKey(field, entityValue) {
|
|
3505
|
-
if (!field.url) return void 0;
|
|
3506
|
-
const base = trimTrailingSlash(field.url);
|
|
3507
|
-
const directId = entityValue["id"];
|
|
3508
|
-
if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
|
|
3509
|
-
const directValue = entityValue[field.valueField];
|
|
3510
|
-
if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
|
|
3511
|
-
for (const key of [
|
|
3512
|
-
"code",
|
|
3513
|
-
"uuid",
|
|
3514
|
-
"slug"
|
|
3515
|
-
]) {
|
|
3516
|
-
const candidate = entityValue[key];
|
|
3517
|
-
if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
|
|
3518
|
-
}
|
|
3519
|
-
}
|
|
3520
|
-
};
|
|
3521
|
-
//#endregion
|
|
3522
4044
|
//#region packages/crud/form/FormDataTransform.ts
|
|
3523
4045
|
function upsertPrependData(store, field, item) {
|
|
3524
4046
|
const existing = store.get(field.name);
|
|
@@ -3820,59 +4342,6 @@ function buildFieldColSpanContext(options) {
|
|
|
3820
4342
|
};
|
|
3821
4343
|
}
|
|
3822
4344
|
//#endregion
|
|
3823
|
-
//#region packages/crud/form/serializeFormData.ts
|
|
3824
|
-
function applySerializedValue(formData, field, result) {
|
|
3825
|
-
if (result.kind === "set") formData[field.name] = result.value;
|
|
3826
|
-
else if (result.kind === "omit") delete formData[field.name];
|
|
3827
|
-
}
|
|
3828
|
-
/**
|
|
3829
|
-
* Pure serialization of form data before HTTP submission.
|
|
3830
|
-
*
|
|
3831
|
-
* Applies uploaded file references and computed fields, then delegates the
|
|
3832
|
-
* per-type wire format (entity refs, business dates, numeric coercion, file
|
|
3833
|
-
* handling, NONE stripping) to each field's Field-Type module.
|
|
3834
|
-
*
|
|
3835
|
-
* This function is extracted from useFormSubmit for testability.
|
|
3836
|
-
*/
|
|
3837
|
-
function serializeFormFields(rawData, fields, ctx) {
|
|
3838
|
-
const formData = { ...rawData };
|
|
3839
|
-
ctx.uploadedFiles.forEach((file) => {
|
|
3840
|
-
formData[file.name] = file.iri;
|
|
3841
|
-
});
|
|
3842
|
-
fields.forEach((field) => {
|
|
3843
|
-
if (!field.computed) return;
|
|
3844
|
-
const computedValue = field.computed(formData);
|
|
3845
|
-
if (computedValue !== void 0) formData[field.name] = computedValue;
|
|
3846
|
-
});
|
|
3847
|
-
const moduleCtx = {
|
|
3848
|
-
adapter: ctx.adapter ?? HydraAdapter,
|
|
3849
|
-
format: ctx.format,
|
|
3850
|
-
getFieldValue: ctx.getFieldValue
|
|
3851
|
-
};
|
|
3852
|
-
fields.forEach((field) => {
|
|
3853
|
-
applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
|
|
3854
|
-
});
|
|
3855
|
-
return formData;
|
|
3856
|
-
}
|
|
3857
|
-
/**
|
|
3858
|
-
* Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
|
|
3859
|
-
* Pure function — operates on an array of row records.
|
|
3860
|
-
*/
|
|
3861
|
-
function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
|
|
3862
|
-
const details = JSON.parse(JSON.stringify(rows));
|
|
3863
|
-
detailFields.forEach((field) => {
|
|
3864
|
-
const typeModule = getFieldTypeModule(field.type);
|
|
3865
|
-
details.forEach((detail) => {
|
|
3866
|
-
applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
|
|
3867
|
-
});
|
|
3868
|
-
});
|
|
3869
|
-
details.forEach((detail) => {
|
|
3870
|
-
if (!isEditMode) delete detail[detailIdField];
|
|
3871
|
-
else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
|
|
3872
|
-
});
|
|
3873
|
-
return details;
|
|
3874
|
-
}
|
|
3875
|
-
//#endregion
|
|
3876
4345
|
//#region packages/crud/form/useFormSubmit.ts
|
|
3877
4346
|
/**
|
|
3878
4347
|
* Returns helpers for form serialization and HTTP submit/delete operations.
|