@nubitio/crud 0.5.23 → 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 +717 -224
- package/dist/index.d.cts +43 -24
- package/dist/index.d.mts +43 -24
- package/dist/index.mjs +658 -168
- package/dist/style.css +146 -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,31 @@ 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
|
+
}),
|
|
3258
|
+
options.aboveGrid ? /* @__PURE__ */ jsx("div", {
|
|
3259
|
+
className: "nb-datagrid__above-grid",
|
|
3260
|
+
children: options.aboveGrid
|
|
3261
|
+
}) : null,
|
|
2710
3262
|
isMobile && quickSearchField && (options.filterRow ?? true) && /* @__PURE__ */ jsxs("div", {
|
|
2711
3263
|
className: "nb-datagrid__quick-search",
|
|
2712
3264
|
children: [
|
|
@@ -2996,16 +3548,33 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2996
3548
|
}) }) : rows.map((row, rowIndex) => {
|
|
2997
3549
|
const key = row[idField] ?? rowIndex;
|
|
2998
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);
|
|
2999
3555
|
const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
|
|
3000
3556
|
const detailUrl = options.detailUrl?.replace("{id}", String(key));
|
|
3001
3557
|
const expanded = expandedKeys.has(key);
|
|
3002
3558
|
const rowActions = buildRowActions(row);
|
|
3003
3559
|
return /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("tr", {
|
|
3004
|
-
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(" "),
|
|
3005
3567
|
tabIndex: 0,
|
|
3006
3568
|
"aria-selected": selected,
|
|
3007
|
-
onClick: () =>
|
|
3569
|
+
onClick: () => {
|
|
3570
|
+
if (!editing) selectRow(row);
|
|
3571
|
+
},
|
|
3008
3572
|
onDoubleClick: () => {
|
|
3573
|
+
if (editing) return;
|
|
3574
|
+
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3575
|
+
inlineEdit.startEdit(row);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3009
3578
|
if (options.allowEdit && (options.onEdit || options.events?.EDIT)) {
|
|
3010
3579
|
if (options.onEdit) options.onEdit(row);
|
|
3011
3580
|
else emit(options.events.EDIT, { row });
|
|
@@ -3014,10 +3583,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3014
3583
|
if (options.allowView && options.onView) options.onView(row);
|
|
3015
3584
|
},
|
|
3016
3585
|
onKeyDown: (event) => {
|
|
3017
|
-
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);
|
|
3018
3590
|
else emit(options.events.EDIT, { row });
|
|
3019
|
-
else if (event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
3020
|
-
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);
|
|
3021
3593
|
else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
3022
3594
|
event.preventDefault();
|
|
3023
3595
|
const allRows = event.currentTarget.closest("tbody")?.querySelectorAll("tr.nb-datagrid__row");
|
|
@@ -3062,9 +3634,29 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3062
3634
|
})
|
|
3063
3635
|
}),
|
|
3064
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);
|
|
3065
3657
|
return /* @__PURE__ */ jsx("td", {
|
|
3066
3658
|
style: {
|
|
3067
|
-
width
|
|
3659
|
+
width,
|
|
3068
3660
|
textAlign: field.align
|
|
3069
3661
|
},
|
|
3070
3662
|
title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
|
|
@@ -3074,7 +3666,32 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3074
3666
|
hasRowActions && /* @__PURE__ */ jsx("td", {
|
|
3075
3667
|
className: "nb-datagrid__actions-cell",
|
|
3076
3668
|
onClick: (e) => e.stopPropagation(),
|
|
3077
|
-
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", {
|
|
3078
3695
|
className: "nb-datagrid__row-actions",
|
|
3079
3696
|
children: rowActions.length > 0 && /* @__PURE__ */ jsx(IconButton, {
|
|
3080
3697
|
icon: "ph ph-dots-three-vertical",
|
|
@@ -3424,97 +4041,6 @@ const FORM_EVENTS = {
|
|
|
3424
4041
|
};
|
|
3425
4042
|
const FORM_ERRORS_EVENT = "form-errors";
|
|
3426
4043
|
//#endregion
|
|
3427
|
-
//#region packages/crud/adapter/HydraAdapter.ts
|
|
3428
|
-
function trimTrailingSlash(value) {
|
|
3429
|
-
return value.replace(/\/+$/, "");
|
|
3430
|
-
}
|
|
3431
|
-
function buildIRI(url, value) {
|
|
3432
|
-
if (value.startsWith("/")) return value;
|
|
3433
|
-
return `${url}/${value}`;
|
|
3434
|
-
}
|
|
3435
|
-
/**
|
|
3436
|
-
* Default backend adapter for API Platform / JSON-LD + Hydra backends.
|
|
3437
|
-
*
|
|
3438
|
-
* Conventions assumed:
|
|
3439
|
-
* - Records carry an `@id` IRI (e.g. `/api/users/5`) as their canonical identifier.
|
|
3440
|
-
* - Entity fields are serialized as IRI strings in POST/PATCH bodies.
|
|
3441
|
-
* - Collection responses follow the `hydra:member` / `hydra:totalItems` shape.
|
|
3442
|
-
* - `_iri` is a synthetic alias for `@id` used internally by the engine.
|
|
3443
|
-
*/
|
|
3444
|
-
const HydraAdapter = {
|
|
3445
|
-
getRowId(record, idField) {
|
|
3446
|
-
const direct = record[idField];
|
|
3447
|
-
if (direct !== void 0 && direct !== null) return String(direct);
|
|
3448
|
-
const iri = record["@id"] ?? record["_iri"];
|
|
3449
|
-
if (iri !== void 0 && iri !== null) return String(iri);
|
|
3450
|
-
return String(record["id"] ?? "");
|
|
3451
|
-
},
|
|
3452
|
-
buildItemUrl(baseUrl, id) {
|
|
3453
|
-
const str = String(id);
|
|
3454
|
-
if (str.startsWith("/")) return str;
|
|
3455
|
-
return `${trimTrailingSlash(baseUrl)}/${str}`;
|
|
3456
|
-
},
|
|
3457
|
-
serializeEntityRef(field, rawValue) {
|
|
3458
|
-
if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === -999) return;
|
|
3459
|
-
if (typeof rawValue === "object") {
|
|
3460
|
-
const entity = rawValue;
|
|
3461
|
-
const atId = entity["@id"];
|
|
3462
|
-
if (typeof atId === "string") return buildIRI(field.url ?? "", atId);
|
|
3463
|
-
const idValue = entity["id"];
|
|
3464
|
-
const resolvedId = idValue !== void 0 && idValue !== null ? String(idValue) : String(entity[field.valueField]);
|
|
3465
|
-
return buildIRI(field.url ?? "", resolvedId);
|
|
3466
|
-
}
|
|
3467
|
-
return buildIRI(field.url ?? "", String(rawValue));
|
|
3468
|
-
},
|
|
3469
|
-
normalizeEntityValue(rawValue, field) {
|
|
3470
|
-
if (typeof rawValue === "string") return field.valueField === "_iri" ? rawValue : rawValue.split("/").pop();
|
|
3471
|
-
if (typeof rawValue === "object" && rawValue !== null) {
|
|
3472
|
-
const entity = rawValue;
|
|
3473
|
-
const atId = entity["@id"];
|
|
3474
|
-
if (typeof atId === "string") return field.valueField === "_iri" ? atId : atId.split("/").pop() ?? atId;
|
|
3475
|
-
const directValue = entity[field.valueField];
|
|
3476
|
-
if (directValue !== void 0 && directValue !== null) return directValue;
|
|
3477
|
-
}
|
|
3478
|
-
return rawValue;
|
|
3479
|
-
},
|
|
3480
|
-
getEntityOptionKey(item, field) {
|
|
3481
|
-
if (field.valueField === "_iri") return item["_iri"] ?? item["@id"] ?? item["id"];
|
|
3482
|
-
return item[field.valueField] ?? item["value"] ?? item["id"] ?? item["@id"];
|
|
3483
|
-
},
|
|
3484
|
-
parseListResponse(response) {
|
|
3485
|
-
const r = response;
|
|
3486
|
-
const member = r["hydra:member"];
|
|
3487
|
-
if (Array.isArray(member)) return {
|
|
3488
|
-
items: member,
|
|
3489
|
-
total: Number(r["hydra:totalItems"] ?? member.length)
|
|
3490
|
-
};
|
|
3491
|
-
if (Array.isArray(response)) return {
|
|
3492
|
-
items: response,
|
|
3493
|
-
total: response.length
|
|
3494
|
-
};
|
|
3495
|
-
return {
|
|
3496
|
-
items: [],
|
|
3497
|
-
total: 0
|
|
3498
|
-
};
|
|
3499
|
-
},
|
|
3500
|
-
synthesizeEntityKey(field, entityValue) {
|
|
3501
|
-
if (!field.url) return void 0;
|
|
3502
|
-
const base = trimTrailingSlash(field.url);
|
|
3503
|
-
const directId = entityValue["id"];
|
|
3504
|
-
if (typeof directId === "string" || typeof directId === "number") return `${base}/${directId}`;
|
|
3505
|
-
const directValue = entityValue[field.valueField];
|
|
3506
|
-
if (field.valueField !== "_iri" && (typeof directValue === "string" || typeof directValue === "number")) return `${base}/${directValue}`;
|
|
3507
|
-
for (const key of [
|
|
3508
|
-
"code",
|
|
3509
|
-
"uuid",
|
|
3510
|
-
"slug"
|
|
3511
|
-
]) {
|
|
3512
|
-
const candidate = entityValue[key];
|
|
3513
|
-
if (typeof candidate === "string" || typeof candidate === "number") return `${base}/${candidate}`;
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
};
|
|
3517
|
-
//#endregion
|
|
3518
4044
|
//#region packages/crud/form/FormDataTransform.ts
|
|
3519
4045
|
function upsertPrependData(store, field, item) {
|
|
3520
4046
|
const existing = store.get(field.name);
|
|
@@ -3816,59 +4342,6 @@ function buildFieldColSpanContext(options) {
|
|
|
3816
4342
|
};
|
|
3817
4343
|
}
|
|
3818
4344
|
//#endregion
|
|
3819
|
-
//#region packages/crud/form/serializeFormData.ts
|
|
3820
|
-
function applySerializedValue(formData, field, result) {
|
|
3821
|
-
if (result.kind === "set") formData[field.name] = result.value;
|
|
3822
|
-
else if (result.kind === "omit") delete formData[field.name];
|
|
3823
|
-
}
|
|
3824
|
-
/**
|
|
3825
|
-
* Pure serialization of form data before HTTP submission.
|
|
3826
|
-
*
|
|
3827
|
-
* Applies uploaded file references and computed fields, then delegates the
|
|
3828
|
-
* per-type wire format (entity refs, business dates, numeric coercion, file
|
|
3829
|
-
* handling, NONE stripping) to each field's Field-Type module.
|
|
3830
|
-
*
|
|
3831
|
-
* This function is extracted from useFormSubmit for testability.
|
|
3832
|
-
*/
|
|
3833
|
-
function serializeFormFields(rawData, fields, ctx) {
|
|
3834
|
-
const formData = { ...rawData };
|
|
3835
|
-
ctx.uploadedFiles.forEach((file) => {
|
|
3836
|
-
formData[file.name] = file.iri;
|
|
3837
|
-
});
|
|
3838
|
-
fields.forEach((field) => {
|
|
3839
|
-
if (!field.computed) return;
|
|
3840
|
-
const computedValue = field.computed(formData);
|
|
3841
|
-
if (computedValue !== void 0) formData[field.name] = computedValue;
|
|
3842
|
-
});
|
|
3843
|
-
const moduleCtx = {
|
|
3844
|
-
adapter: ctx.adapter ?? HydraAdapter,
|
|
3845
|
-
format: ctx.format,
|
|
3846
|
-
getFieldValue: ctx.getFieldValue
|
|
3847
|
-
};
|
|
3848
|
-
fields.forEach((field) => {
|
|
3849
|
-
applySerializedValue(formData, field, getFieldTypeModule(field.type).serializeFormValue(field, formData[field.name], moduleCtx));
|
|
3850
|
-
});
|
|
3851
|
-
return formData;
|
|
3852
|
-
}
|
|
3853
|
-
/**
|
|
3854
|
-
* Serializes detail grid rows (entity refs, numeric coercion, identity cleanup).
|
|
3855
|
-
* Pure function — operates on an array of row records.
|
|
3856
|
-
*/
|
|
3857
|
-
function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adapter = HydraAdapter) {
|
|
3858
|
-
const details = JSON.parse(JSON.stringify(rows));
|
|
3859
|
-
detailFields.forEach((field) => {
|
|
3860
|
-
const typeModule = getFieldTypeModule(field.type);
|
|
3861
|
-
details.forEach((detail) => {
|
|
3862
|
-
applySerializedValue(detail, field, typeModule.serializeDetailValue(field, detail[field.name], adapter));
|
|
3863
|
-
});
|
|
3864
|
-
});
|
|
3865
|
-
details.forEach((detail) => {
|
|
3866
|
-
if (!isEditMode) delete detail[detailIdField];
|
|
3867
|
-
else if (typeof detail[detailIdField] === "string") delete detail[detailIdField];
|
|
3868
|
-
});
|
|
3869
|
-
return details;
|
|
3870
|
-
}
|
|
3871
|
-
//#endregion
|
|
3872
4345
|
//#region packages/crud/form/useFormSubmit.ts
|
|
3873
4346
|
/**
|
|
3874
4347
|
* Returns helpers for form serialization and HTTP submit/delete operations.
|
|
@@ -5276,7 +5749,11 @@ function usePermissions(resource, supportedOperations = []) {
|
|
|
5276
5749
|
canExport: resolve(p?.canExport, false),
|
|
5277
5750
|
canBulkDelete: resolve(p?.canBulkDelete, false)
|
|
5278
5751
|
};
|
|
5279
|
-
}, [
|
|
5752
|
+
}, [
|
|
5753
|
+
resource.id,
|
|
5754
|
+
opsKey,
|
|
5755
|
+
resource.permissions
|
|
5756
|
+
]);
|
|
5280
5757
|
}
|
|
5281
5758
|
//#endregion
|
|
5282
5759
|
//#region packages/crud/crud/useSelectionState.ts
|
|
@@ -6125,7 +6602,7 @@ function buildRoutingFilterRules(fields, initialFilters) {
|
|
|
6125
6602
|
* Public export — wraps CrudPageInner in its own DialogStoreProvider so that
|
|
6126
6603
|
* existing pages do not need to add a provider themselves.
|
|
6127
6604
|
*/
|
|
6128
|
-
const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, onOperationChange }) => /* @__PURE__ */ jsx(DialogStoreProvider, { children: /* @__PURE__ */ jsx(CrudPageInner, {
|
|
6605
|
+
const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, computedValues, initialFilters, onFiltersChange, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid, onOperationChange }) => /* @__PURE__ */ jsx(DialogStoreProvider, { children: /* @__PURE__ */ jsx(CrudPageInner, {
|
|
6129
6606
|
resource,
|
|
6130
6607
|
onFormDataChange,
|
|
6131
6608
|
initialRecordId,
|
|
@@ -6138,9 +6615,10 @@ const CrudPage = ({ resource, onFormDataChange, initialRecordId, initialIsNew, c
|
|
|
6138
6615
|
editDisabled,
|
|
6139
6616
|
deleteDisabled,
|
|
6140
6617
|
gridRef,
|
|
6618
|
+
aboveGrid,
|
|
6141
6619
|
onOperationChange
|
|
6142
6620
|
}) });
|
|
6143
|
-
const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, initialIsNew = false, computedValues = EMPTY_COMPUTED_VALUES, initialFilters = EMPTY_INITIAL_FILTERS, onFiltersChange, formRef: externalFormRef, onSelectionChanged: externalOnSelectionChanged, editDisabled, deleteDisabled, gridRef: externalGridRef, onOperationChange }) => {
|
|
6621
|
+
const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, initialIsNew = false, computedValues = EMPTY_COMPUTED_VALUES, initialFilters = EMPTY_INITIAL_FILTERS, onFiltersChange, formRef: externalFormRef, onSelectionChanged: externalOnSelectionChanged, editDisabled, deleteDisabled, gridRef: externalGridRef, aboveGrid: aboveGridOverride, onOperationChange }) => {
|
|
6144
6622
|
const { t } = useCoreTranslation();
|
|
6145
6623
|
const { events, resource: resolvedResource, fields, formFields, formRef, permissions, selectionState, presetState } = useCrudPage(useMemo(() => resolveCrudResource(resource), [resource]), externalFormRef);
|
|
6146
6624
|
const datagridFields = useMemo(() => fields.filter((field) => field.isIdentity || field.visible !== false), [fields]);
|
|
@@ -6403,6 +6881,16 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
|
|
|
6403
6881
|
resolvedResource.rowActions,
|
|
6404
6882
|
t
|
|
6405
6883
|
]);
|
|
6884
|
+
const aboveGridSlot = aboveGridOverride ?? resolvedResource.aboveGrid;
|
|
6885
|
+
const aboveGridContent = (() => {
|
|
6886
|
+
if (!aboveGridSlot) return;
|
|
6887
|
+
if (typeof aboveGridSlot === "function") return aboveGridSlot({
|
|
6888
|
+
resource: resolvedResource,
|
|
6889
|
+
gridRef,
|
|
6890
|
+
refresh: () => gridRef.current?.refresh()
|
|
6891
|
+
});
|
|
6892
|
+
return aboveGridSlot;
|
|
6893
|
+
})();
|
|
6406
6894
|
/** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
|
|
6407
6895
|
const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ jsx(ColumnPresetSelector, {
|
|
6408
6896
|
resourceId: resolvedResource.id,
|
|
@@ -6465,6 +6953,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
|
|
|
6465
6953
|
editMode: resolvedResource.editMode,
|
|
6466
6954
|
visibleColumns: presetState.visibleColumns,
|
|
6467
6955
|
beforeToolbar: renderPresetSelector,
|
|
6956
|
+
aboveGrid: aboveGridContent,
|
|
6468
6957
|
detailUrl: gridDetail?.url,
|
|
6469
6958
|
detailFields: gridDetail?.fields,
|
|
6470
6959
|
onFilterChange: onFiltersChange,
|
|
@@ -7586,7 +8075,7 @@ function formatRuntimeErrorMessage(error) {
|
|
|
7586
8075
|
*
|
|
7587
8076
|
* URL deep-linking is wired via `initialRecordId` / `initialIsNew` props on CrudPage.
|
|
7588
8077
|
*/
|
|
7589
|
-
function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef }) {
|
|
8078
|
+
function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged, editDisabled, deleteDisabled, gridRef, aboveGrid }) {
|
|
7590
8079
|
const queryClient = useQueryClient();
|
|
7591
8080
|
const internalGridRef = useRef(null);
|
|
7592
8081
|
const effectiveGridRef = gridRef ?? internalGridRef;
|
|
@@ -7671,6 +8160,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
|
|
|
7671
8160
|
editDisabled,
|
|
7672
8161
|
deleteDisabled,
|
|
7673
8162
|
gridRef: effectiveGridRef,
|
|
8163
|
+
aboveGrid: aboveGrid ?? resource.aboveGrid,
|
|
7674
8164
|
onOperationChange: (operation) => {
|
|
7675
8165
|
if (operation === "create") {
|
|
7676
8166
|
startCreate();
|