@nubitio/crud 0.5.26 → 0.5.28
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 +283 -60
- package/dist/index.d.cts +27 -1
- package/dist/index.d.mts +27 -1
- package/dist/index.mjs +283 -60
- package/dist/style.css +154 -22
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -2183,10 +2183,20 @@ function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adap
|
|
|
2183
2183
|
function canEditFieldInline(field) {
|
|
2184
2184
|
return !field.isIdentity && !field.readonly && field.type !== "file" && field.visibleOnForm !== false;
|
|
2185
2185
|
}
|
|
2186
|
-
function
|
|
2186
|
+
function valuesEqual$1(a, b) {
|
|
2187
|
+
if (a === b) return true;
|
|
2188
|
+
if (a == null && b == null) return true;
|
|
2189
|
+
if (a == null || b == null) return false;
|
|
2190
|
+
const numA = Number(a);
|
|
2191
|
+
const numB = Number(b);
|
|
2192
|
+
if (!Number.isNaN(numA) && !Number.isNaN(numB)) return numA === numB;
|
|
2193
|
+
return String(a) === String(b);
|
|
2194
|
+
}
|
|
2195
|
+
function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError, onBatchSave }) {
|
|
2187
2196
|
const [draftRows, setDraftRows] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
2188
2197
|
const [savingRows, setSavingRows] = (0, react$1.useState)(/* @__PURE__ */ new Set());
|
|
2189
2198
|
const [rowErrors, setRowErrors] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
2199
|
+
const [activeCell, setActiveCell] = (0, react$1.useState)(null);
|
|
2190
2200
|
const draftRowsRef = (0, react$1.useRef)(draftRows);
|
|
2191
2201
|
draftRowsRef.current = draftRows;
|
|
2192
2202
|
const optsRef = (0, react$1.useRef)({
|
|
@@ -2196,7 +2206,8 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2196
2206
|
httpClient,
|
|
2197
2207
|
fields,
|
|
2198
2208
|
onSaveSuccess,
|
|
2199
|
-
onSaveError
|
|
2209
|
+
onSaveError,
|
|
2210
|
+
onBatchSave
|
|
2200
2211
|
});
|
|
2201
2212
|
optsRef.current = {
|
|
2202
2213
|
url,
|
|
@@ -2205,9 +2216,21 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2205
2216
|
httpClient,
|
|
2206
2217
|
fields,
|
|
2207
2218
|
onSaveSuccess,
|
|
2208
|
-
onSaveError
|
|
2219
|
+
onSaveError,
|
|
2220
|
+
onBatchSave
|
|
2209
2221
|
};
|
|
2210
|
-
const isEditing = (0, react$1.useCallback)((key) =>
|
|
2222
|
+
const isEditing = (0, react$1.useCallback)((key) => draftRows.has(key), [draftRows]);
|
|
2223
|
+
const isCellActive = (0, react$1.useCallback)((key, fieldName) => activeCell != null && activeCell.key === key && activeCell.fieldName === fieldName, [activeCell]);
|
|
2224
|
+
const isCellDirty = (0, react$1.useCallback)((key, fieldName, original) => {
|
|
2225
|
+
const draft = draftRows.get(key);
|
|
2226
|
+
if (!draft) return false;
|
|
2227
|
+
return !valuesEqual$1(draft[fieldName], original[fieldName]);
|
|
2228
|
+
}, [draftRows]);
|
|
2229
|
+
const hasDraftChanges = (0, react$1.useCallback)((key, original) => {
|
|
2230
|
+
const draft = draftRows.get(key);
|
|
2231
|
+
if (!draft) return false;
|
|
2232
|
+
return optsRef.current.fields.some((field) => canEditFieldInline(field) && !valuesEqual$1(draft[field.name], original[field.name]));
|
|
2233
|
+
}, [draftRows]);
|
|
2211
2234
|
const startEdit = (0, react$1.useCallback)((row) => {
|
|
2212
2235
|
const key = row[optsRef.current.idField];
|
|
2213
2236
|
setDraftRows((prev) => {
|
|
@@ -2219,7 +2242,28 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2219
2242
|
next.delete(key);
|
|
2220
2243
|
return next;
|
|
2221
2244
|
});
|
|
2245
|
+
setActiveCell(null);
|
|
2222
2246
|
}, [mode]);
|
|
2247
|
+
const startCellEdit = (0, react$1.useCallback)((row, fieldName) => {
|
|
2248
|
+
const key = row[optsRef.current.idField];
|
|
2249
|
+
setDraftRows((prev) => {
|
|
2250
|
+
const existing = prev.get(key);
|
|
2251
|
+
const withoutKey = (mode === "row" ? [] : Array.from(prev.entries())).filter(([entryKey]) => entryKey !== key);
|
|
2252
|
+
return new Map([...withoutKey, [key, existing ? { ...existing } : { ...row }]]);
|
|
2253
|
+
});
|
|
2254
|
+
setRowErrors((prev) => {
|
|
2255
|
+
const next = new Map(prev);
|
|
2256
|
+
next.delete(key);
|
|
2257
|
+
return next;
|
|
2258
|
+
});
|
|
2259
|
+
setActiveCell({
|
|
2260
|
+
key,
|
|
2261
|
+
fieldName
|
|
2262
|
+
});
|
|
2263
|
+
}, [mode]);
|
|
2264
|
+
const stopCellEdit = (0, react$1.useCallback)(() => {
|
|
2265
|
+
setActiveCell(null);
|
|
2266
|
+
}, []);
|
|
2223
2267
|
const cancelEdit = (0, react$1.useCallback)((key) => {
|
|
2224
2268
|
setDraftRows((prev) => {
|
|
2225
2269
|
const next = new Map(prev);
|
|
@@ -2231,10 +2275,12 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2231
2275
|
next.delete(key);
|
|
2232
2276
|
return next;
|
|
2233
2277
|
});
|
|
2278
|
+
setActiveCell((current) => current?.key === key ? null : current);
|
|
2234
2279
|
}, []);
|
|
2235
2280
|
const discardAll = (0, react$1.useCallback)(() => {
|
|
2236
2281
|
setDraftRows(/* @__PURE__ */ new Map());
|
|
2237
2282
|
setRowErrors(/* @__PURE__ */ new Map());
|
|
2283
|
+
setActiveCell(null);
|
|
2238
2284
|
}, []);
|
|
2239
2285
|
const updateDraft = (0, react$1.useCallback)((key, fieldName, value) => {
|
|
2240
2286
|
setDraftRows((prev) => {
|
|
@@ -2301,35 +2347,102 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2301
2347
|
});
|
|
2302
2348
|
}
|
|
2303
2349
|
}, []);
|
|
2350
|
+
const validateDraft = (0, react$1.useCallback)((key, draft, fs) => {
|
|
2351
|
+
const errors = {};
|
|
2352
|
+
fs.forEach((field) => {
|
|
2353
|
+
if (!canEditFieldInline(field)) return;
|
|
2354
|
+
if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
|
|
2355
|
+
});
|
|
2356
|
+
if (Object.keys(errors).length === 0) return true;
|
|
2357
|
+
setRowErrors((prev) => new Map(prev).set(key, errors));
|
|
2358
|
+
return false;
|
|
2359
|
+
}, []);
|
|
2360
|
+
const doSaveBatch = (0, react$1.useCallback)(async () => {
|
|
2361
|
+
const { fields: fs, onBatchSave: saveBatch, onSaveError: onErr } = optsRef.current;
|
|
2362
|
+
if (!saveBatch) return false;
|
|
2363
|
+
const entries = Array.from(draftRowsRef.current.entries());
|
|
2364
|
+
if (entries.length === 0) return false;
|
|
2365
|
+
if (!entries.every(([key, draft]) => validateDraft(key, draft, fs))) return false;
|
|
2366
|
+
const keys = entries.map(([key]) => key);
|
|
2367
|
+
setSavingRows((prev) => new Set([...prev, ...keys]));
|
|
2368
|
+
try {
|
|
2369
|
+
await saveBatch(entries.map(([, draft]) => ({ ...draft })));
|
|
2370
|
+
setDraftRows(/* @__PURE__ */ new Map());
|
|
2371
|
+
setRowErrors(/* @__PURE__ */ new Map());
|
|
2372
|
+
return true;
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
keys.forEach((key) => onErr?.(key, err));
|
|
2375
|
+
return false;
|
|
2376
|
+
} finally {
|
|
2377
|
+
setSavingRows((prev) => {
|
|
2378
|
+
const next = new Set(prev);
|
|
2379
|
+
keys.forEach((key) => next.delete(key));
|
|
2380
|
+
return next;
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
}, [validateDraft]);
|
|
2304
2384
|
return {
|
|
2305
2385
|
draftRows,
|
|
2306
2386
|
savingRows,
|
|
2307
2387
|
rowErrors,
|
|
2388
|
+
activeCell,
|
|
2308
2389
|
isEditing,
|
|
2390
|
+
isCellActive,
|
|
2391
|
+
isCellDirty,
|
|
2392
|
+
hasDraftChanges,
|
|
2309
2393
|
startEdit,
|
|
2394
|
+
startCellEdit,
|
|
2395
|
+
stopCellEdit,
|
|
2310
2396
|
cancelEdit,
|
|
2311
2397
|
discardAll,
|
|
2312
2398
|
updateDraft,
|
|
2313
2399
|
saveRow: (0, react$1.useCallback)(async (key) => {
|
|
2314
|
-
if (
|
|
2315
|
-
|
|
2400
|
+
if (optsRef.current.onBatchSave) {
|
|
2401
|
+
const ok = await doSaveBatch();
|
|
2402
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2403
|
+
return ok;
|
|
2404
|
+
}
|
|
2405
|
+
const ok = await doSaveRow(key);
|
|
2406
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2407
|
+
return ok;
|
|
2408
|
+
}, [doSaveBatch, doSaveRow]),
|
|
2316
2409
|
saveAll: (0, react$1.useCallback)(async () => {
|
|
2410
|
+
if (optsRef.current.onBatchSave) {
|
|
2411
|
+
const ok = await doSaveBatch();
|
|
2412
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2413
|
+
return ok;
|
|
2414
|
+
}
|
|
2317
2415
|
const keys = Array.from(draftRowsRef.current.keys());
|
|
2318
|
-
|
|
2319
|
-
|
|
2416
|
+
const anySuccess = (await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value);
|
|
2417
|
+
if (anySuccess) optsRef.current.onSaveSuccess?.();
|
|
2418
|
+
return anySuccess;
|
|
2419
|
+
}, [doSaveBatch, doSaveRow])
|
|
2320
2420
|
};
|
|
2321
2421
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2422
|
+
function focusInlineControl(container) {
|
|
2423
|
+
if (!container) return;
|
|
2424
|
+
const focusable = container.querySelector("input:not([type=\"hidden\"]):not([disabled]):not([readonly]), select:not([disabled]), textarea:not([disabled]), .nb-date-picker__input:not([readonly]), .nb-dropdown__trigger:not([disabled])");
|
|
2425
|
+
focusable?.focus();
|
|
2426
|
+
if (focusable instanceof HTMLInputElement && focusable.type !== "checkbox") focusable.select();
|
|
2427
|
+
}
|
|
2324
2428
|
/**
|
|
2325
|
-
* Renders a single cell as an editable control during inline
|
|
2326
|
-
* Uses the same FieldTypeModule.ControlRender path as the full form,
|
|
2327
|
-
*
|
|
2429
|
+
* Renders a single cell as an editable control during inline editing.
|
|
2430
|
+
* Uses the same FieldTypeModule.ControlRender path as the full form, with
|
|
2431
|
+
* compact styling so controls fit naturally inside a table cell.
|
|
2328
2432
|
*/
|
|
2329
|
-
function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t }) {
|
|
2433
|
+
function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t, autoFocus = true }) {
|
|
2434
|
+
const containerRef = (0, react.useRef)(null);
|
|
2330
2435
|
const typeModule = getFieldTypeModule(field.type);
|
|
2331
2436
|
const fieldError = errors?.[field.name];
|
|
2332
2437
|
const errorClass = fieldError ? " is-error" : "";
|
|
2438
|
+
(0, react.useEffect)(() => {
|
|
2439
|
+
if (!autoFocus) return;
|
|
2440
|
+
focusInlineControl(containerRef.current);
|
|
2441
|
+
}, [
|
|
2442
|
+
autoFocus,
|
|
2443
|
+
field.name,
|
|
2444
|
+
rowKey
|
|
2445
|
+
]);
|
|
2333
2446
|
const commonProps = {
|
|
2334
2447
|
className: `nb-inline-control${errorClass}`,
|
|
2335
2448
|
disabled,
|
|
@@ -2350,7 +2463,16 @@ function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = fal
|
|
|
2350
2463
|
upsertUploadedFile: () => {}
|
|
2351
2464
|
};
|
|
2352
2465
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2466
|
+
ref: containerRef,
|
|
2353
2467
|
className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
|
|
2468
|
+
onKeyDown: (event) => {
|
|
2469
|
+
if (event.key === "Escape") event.stopPropagation();
|
|
2470
|
+
},
|
|
2471
|
+
onMouseDown: (event) => {
|
|
2472
|
+
if (event.target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
|
|
2473
|
+
event.stopPropagation();
|
|
2474
|
+
},
|
|
2475
|
+
onClick: (event) => event.stopPropagation(),
|
|
2354
2476
|
children: typeModule.ControlRender({
|
|
2355
2477
|
field,
|
|
2356
2478
|
value: draft[field.name],
|
|
@@ -2479,6 +2601,25 @@ function normalizeIcon$1(icon) {
|
|
|
2479
2601
|
function isDateLikeField(field) {
|
|
2480
2602
|
return field.type === "date" || field.type === "datetime";
|
|
2481
2603
|
}
|
|
2604
|
+
function isCellEditMode(editMode) {
|
|
2605
|
+
return editMode === "cell" || editMode === "batch";
|
|
2606
|
+
}
|
|
2607
|
+
function resolveInlineEditToolbar(editMode, inlineEditToolbar) {
|
|
2608
|
+
if (inlineEditToolbar === false) return null;
|
|
2609
|
+
const defaultShow = isCellEditMode(editMode);
|
|
2610
|
+
if (inlineEditToolbar == null) return defaultShow ? {
|
|
2611
|
+
save: true,
|
|
2612
|
+
revert: true
|
|
2613
|
+
} : null;
|
|
2614
|
+
if (typeof inlineEditToolbar === "boolean") return inlineEditToolbar ? {
|
|
2615
|
+
save: true,
|
|
2616
|
+
revert: true
|
|
2617
|
+
} : null;
|
|
2618
|
+
return {
|
|
2619
|
+
save: inlineEditToolbar.save !== false,
|
|
2620
|
+
revert: inlineEditToolbar.revert !== false
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2482
2623
|
function getToolbarKey(action, index) {
|
|
2483
2624
|
return action.key ?? action.text ?? String(index);
|
|
2484
2625
|
}
|
|
@@ -2832,6 +2973,15 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2832
2973
|
fieldsRef.current = options.fields;
|
|
2833
2974
|
const loadSeqRef = (0, react.useRef)(0);
|
|
2834
2975
|
const loadRows = (0, react.useCallback)(async () => {
|
|
2976
|
+
if (options.data) {
|
|
2977
|
+
rowsRef.current = options.data;
|
|
2978
|
+
setRows(options.data);
|
|
2979
|
+
setTotalCount(options.data.length);
|
|
2980
|
+
setGridSummary(options.gridSummary ?? null);
|
|
2981
|
+
setIsGridLoading(false);
|
|
2982
|
+
onContentReadyRef.current?.();
|
|
2983
|
+
return options.data;
|
|
2984
|
+
}
|
|
2835
2985
|
if (options.manualLoad) return rowsRef.current;
|
|
2836
2986
|
setIsGridLoading(true);
|
|
2837
2987
|
const seq = ++loadSeqRef.current;
|
|
@@ -2855,6 +3005,8 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2855
3005
|
}, [
|
|
2856
3006
|
filterOperators,
|
|
2857
3007
|
filters,
|
|
3008
|
+
options.data,
|
|
3009
|
+
options.gridSummary,
|
|
2858
3010
|
options.manualLoad,
|
|
2859
3011
|
options.paging,
|
|
2860
3012
|
page,
|
|
@@ -2898,30 +3050,58 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2898
3050
|
resourceStoreFactory,
|
|
2899
3051
|
visibleFields
|
|
2900
3052
|
]);
|
|
2901
|
-
const canInlineEditMode = options.editMode === "row" || options.editMode === "batch";
|
|
3053
|
+
const canInlineEditMode = options.editMode === "row" || options.editMode === "cell" || options.editMode === "batch";
|
|
3054
|
+
const cellEditMode = isCellEditMode(options.editMode);
|
|
3055
|
+
const rowInlineMode = options.editMode === "row";
|
|
3056
|
+
const inlineEditToolbar = resolveInlineEditToolbar(options.editMode, options.inlineEditToolbar);
|
|
3057
|
+
const showRowInlineActions = options.inlineRowActions ?? rowInlineMode;
|
|
2902
3058
|
const inlineEdit = useInlineEdit({
|
|
2903
|
-
mode:
|
|
3059
|
+
mode: cellEditMode ? "batch" : "row",
|
|
2904
3060
|
url: options.url,
|
|
2905
3061
|
idField,
|
|
2906
3062
|
adapter: options.adapter,
|
|
2907
3063
|
httpClient,
|
|
2908
3064
|
fields: options.fields,
|
|
2909
3065
|
onSaveSuccess: () => void loadRows(),
|
|
2910
|
-
onSaveError: () => {}
|
|
3066
|
+
onSaveError: () => {},
|
|
3067
|
+
onBatchSave: options.onBatchSave
|
|
2911
3068
|
});
|
|
3069
|
+
const dirtyRowCount = (0, react.useMemo)(() => rows.filter((row) => {
|
|
3070
|
+
const key = row[idField] ?? row;
|
|
3071
|
+
return inlineEdit.draftRows.has(key) && inlineEdit.hasDraftChanges(key, row);
|
|
3072
|
+
}).length, [
|
|
3073
|
+
rows,
|
|
3074
|
+
idField,
|
|
3075
|
+
inlineEdit.draftRows,
|
|
3076
|
+
inlineEdit.hasDraftChanges
|
|
3077
|
+
]);
|
|
3078
|
+
const hasPendingInlineEdits = dirtyRowCount > 0;
|
|
3079
|
+
(0, react.useEffect)(() => {
|
|
3080
|
+
if (!inlineEdit.activeCell) return;
|
|
3081
|
+
const handler = (event) => {
|
|
3082
|
+
const target = event.target;
|
|
3083
|
+
if (target.closest(".nb-datagrid__edit-cell")) return;
|
|
3084
|
+
if (target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
|
|
3085
|
+
inlineEdit.stopCellEdit();
|
|
3086
|
+
};
|
|
3087
|
+
document.addEventListener("mousedown", handler);
|
|
3088
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
3089
|
+
}, [inlineEdit.activeCell, inlineEdit.stopCellEdit]);
|
|
2912
3090
|
const handleStateRef = (0, react.useRef)({
|
|
2913
3091
|
selectedKeys,
|
|
2914
3092
|
filters,
|
|
2915
3093
|
filterOperators,
|
|
2916
3094
|
loadRows,
|
|
2917
|
-
idField
|
|
3095
|
+
idField,
|
|
3096
|
+
inlineEdit
|
|
2918
3097
|
});
|
|
2919
3098
|
handleStateRef.current = {
|
|
2920
3099
|
selectedKeys,
|
|
2921
3100
|
filters,
|
|
2922
3101
|
filterOperators,
|
|
2923
3102
|
loadRows,
|
|
2924
|
-
idField
|
|
3103
|
+
idField,
|
|
3104
|
+
inlineEdit
|
|
2925
3105
|
};
|
|
2926
3106
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
2927
3107
|
showLoading: (message) => {
|
|
@@ -2961,7 +3141,9 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2961
3141
|
setFilterInputs(nextFilters);
|
|
2962
3142
|
setFilterOperators(nextOperators);
|
|
2963
3143
|
setPage(0);
|
|
2964
|
-
}
|
|
3144
|
+
},
|
|
3145
|
+
hasEditData: () => handleStateRef.current.inlineEdit.draftRows.size > 0,
|
|
3146
|
+
saveChanges: () => handleStateRef.current.inlineEdit.saveAll()
|
|
2965
3147
|
}), [options.fields, t]);
|
|
2966
3148
|
const selectedRows = rows.filter((row) => selectedKeys.includes(row[idField]));
|
|
2967
3149
|
const selectRow = (row) => {
|
|
@@ -2975,7 +3157,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
2975
3157
|
const rowEditable = (row) => options.canEditRow?.(row) !== false;
|
|
2976
3158
|
const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
|
|
2977
3159
|
const buildRowActions = (row) => [
|
|
2978
|
-
...options.allowEdit && rowEditable(row) && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
|
|
3160
|
+
...options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
|
|
2979
3161
|
text: t("grid.inlineEditRow"),
|
|
2980
3162
|
icon: "ph-pencil-simple",
|
|
2981
3163
|
disabled: options.editDisabled,
|
|
@@ -3005,7 +3187,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3005
3187
|
}] : []
|
|
3006
3188
|
];
|
|
3007
3189
|
const openRow = (row) => {
|
|
3008
|
-
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3190
|
+
if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
|
|
3009
3191
|
inlineEdit.startEdit(row);
|
|
3010
3192
|
return;
|
|
3011
3193
|
}
|
|
@@ -3140,7 +3322,7 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3140
3322
|
};
|
|
3141
3323
|
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
3142
3324
|
const hasCheckbox = options.selectionMode === "multiple";
|
|
3143
|
-
const hasBuiltInRowActions = Boolean(options.allowEdit && (
|
|
3325
|
+
const hasBuiltInRowActions = Boolean(options.allowEdit && (rowInlineMode && showRowInlineActions || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
|
|
3144
3326
|
const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
|
|
3145
3327
|
const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
|
|
3146
3328
|
const hasDetail = Boolean(options.detailFields);
|
|
@@ -3155,15 +3337,26 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3155
3337
|
actionsColWidth
|
|
3156
3338
|
});
|
|
3157
3339
|
const resolvedColWidths = (0, react.useMemo)(() => {
|
|
3158
|
-
if (visibleFields.length === 0) return colWidths;
|
|
3159
|
-
const
|
|
3340
|
+
if (visibleFields.length === 0 || containerWidth <= 0) return colWidths;
|
|
3341
|
+
const bases = visibleFields.map((f) => getColumnWidth(f, colWidths));
|
|
3342
|
+
const dataTotal = bases.reduce((sum, width) => sum + width, 0);
|
|
3160
3343
|
const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
|
|
3161
|
-
const
|
|
3344
|
+
const available = Math.max(0, containerWidth - fixedTotal);
|
|
3345
|
+
if (dataTotal > available && available > 0) {
|
|
3346
|
+
const scale = available / dataTotal;
|
|
3347
|
+
const result = {};
|
|
3348
|
+
visibleFields.forEach((field, index) => {
|
|
3349
|
+
const floor = field.minWidth ?? MIN_COL_WIDTH;
|
|
3350
|
+
result[field.name] = Math.max(floor, Math.floor(bases[index] * scale));
|
|
3351
|
+
});
|
|
3352
|
+
return result;
|
|
3353
|
+
}
|
|
3354
|
+
const extra = Math.max(0, available - dataTotal);
|
|
3162
3355
|
if (extra === 0 || dataTotal === 0) return colWidths;
|
|
3163
3356
|
let distributed = 0;
|
|
3164
3357
|
const result = {};
|
|
3165
3358
|
visibleFields.forEach((f, i) => {
|
|
3166
|
-
const base =
|
|
3359
|
+
const base = bases[i];
|
|
3167
3360
|
const share = i < visibleFields.length - 1 ? Math.round(extra * (base / dataTotal)) : extra - distributed;
|
|
3168
3361
|
result[f.name] = base + share;
|
|
3169
3362
|
distributed += share;
|
|
@@ -3175,7 +3368,8 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3175
3368
|
hasCheckbox,
|
|
3176
3369
|
hasDetail,
|
|
3177
3370
|
hasRowActions,
|
|
3178
|
-
containerWidth
|
|
3371
|
+
containerWidth,
|
|
3372
|
+
actionsColWidth
|
|
3179
3373
|
]);
|
|
3180
3374
|
const tableLayoutStyle = { "--nb-datagrid-layout-width": `${layoutWidth}px` };
|
|
3181
3375
|
const filterableFields = visibleFields.filter((field) => field.filterable);
|
|
@@ -3254,6 +3448,18 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3254
3448
|
children: activeFilterCount
|
|
3255
3449
|
})]
|
|
3256
3450
|
}),
|
|
3451
|
+
inlineEditToolbar?.revert && hasPendingInlineEdits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3452
|
+
className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--revert",
|
|
3453
|
+
icon: "ph ph-arrow-counter-clockwise",
|
|
3454
|
+
label: t("grid.inlineRevertChanges"),
|
|
3455
|
+
onClick: () => inlineEdit.discardAll()
|
|
3456
|
+
}),
|
|
3457
|
+
inlineEditToolbar?.save && hasPendingInlineEdits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3458
|
+
className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--save",
|
|
3459
|
+
icon: "ph ph-floppy-disk",
|
|
3460
|
+
label: t("grid.inlineSaveChanges"),
|
|
3461
|
+
onClick: () => void inlineEdit.saveAll()
|
|
3462
|
+
}),
|
|
3257
3463
|
toolbar.showRefresh !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
3258
3464
|
icon: "ph ph-arrow-clockwise",
|
|
3259
3465
|
label: t("grid.buttonRefresh"),
|
|
@@ -3261,26 +3467,13 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3261
3467
|
})
|
|
3262
3468
|
]
|
|
3263
3469
|
}),
|
|
3264
|
-
|
|
3265
|
-
className: "nb-datagrid__batch-bar",
|
|
3470
|
+
cellEditMode && hasPendingInlineEdits && !inlineEditToolbar && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3471
|
+
className: "nb-datagrid__batch-bar nb-datagrid__batch-bar--compact",
|
|
3266
3472
|
role: "status",
|
|
3267
|
-
children:
|
|
3473
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3268
3474
|
className: "nb-datagrid__batch-bar-label",
|
|
3269
|
-
children: t("grid.inlineUnsavedRows", { count:
|
|
3270
|
-
})
|
|
3271
|
-
className: "nb-datagrid__batch-bar-actions",
|
|
3272
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3273
|
-
type: "button",
|
|
3274
|
-
className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--discard",
|
|
3275
|
-
onClick: () => inlineEdit.discardAll(),
|
|
3276
|
-
children: t("grid.inlineDiscardAll")
|
|
3277
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3278
|
-
type: "button",
|
|
3279
|
-
className: "nb-datagrid__batch-bar-btn nb-datagrid__batch-bar-btn--save",
|
|
3280
|
-
onClick: () => void inlineEdit.saveAll(),
|
|
3281
|
-
children: t("grid.inlineSaveAll", { count: inlineEdit.draftRows.size })
|
|
3282
|
-
})]
|
|
3283
|
-
})]
|
|
3475
|
+
children: t("grid.inlineUnsavedRows", { count: dirtyRowCount })
|
|
3476
|
+
})
|
|
3284
3477
|
}),
|
|
3285
3478
|
options.aboveGrid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3286
3479
|
className: "nb-datagrid__above-grid",
|
|
@@ -3576,9 +3769,11 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3576
3769
|
const key = row[idField] ?? rowIndex;
|
|
3577
3770
|
const selected = selectedKeys.includes(key);
|
|
3578
3771
|
const editing = inlineEdit.draftRows.has(key);
|
|
3772
|
+
const rowHasChanges = editing && inlineEdit.hasDraftChanges(key, row);
|
|
3579
3773
|
const saving = inlineEdit.savingRows.has(key);
|
|
3580
3774
|
const rowDraft = inlineEdit.draftRows.get(key) ?? row;
|
|
3581
3775
|
const rowFieldErrors = inlineEdit.rowErrors.get(key);
|
|
3776
|
+
const rowInRowEdit = rowInlineMode && editing;
|
|
3582
3777
|
const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
|
|
3583
3778
|
const detailUrl = options.detailUrl?.replace("{id}", String(key));
|
|
3584
3779
|
const expanded = expandedKeys.has(key);
|
|
@@ -3588,17 +3783,19 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3588
3783
|
"nb-datagrid__row",
|
|
3589
3784
|
expanded ? "nb-datagrid__row--expanded" : "",
|
|
3590
3785
|
selected ? "nb-datagrid__row--selected" : "",
|
|
3591
|
-
|
|
3786
|
+
rowInRowEdit ? "nb-datagrid__row--editing" : "",
|
|
3787
|
+
rowHasChanges ? "nb-datagrid__row--dirty" : "",
|
|
3592
3788
|
saving ? "nb-datagrid__row--saving" : ""
|
|
3593
3789
|
].filter(Boolean).join(" "),
|
|
3594
3790
|
tabIndex: 0,
|
|
3595
3791
|
"aria-selected": selected,
|
|
3596
3792
|
onClick: () => {
|
|
3597
|
-
if (
|
|
3793
|
+
if (rowInRowEdit) return;
|
|
3794
|
+
selectRow(row);
|
|
3598
3795
|
},
|
|
3599
3796
|
onDoubleClick: () => {
|
|
3600
|
-
if (
|
|
3601
|
-
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3797
|
+
if (rowInRowEdit || inlineEdit.activeCell) return;
|
|
3798
|
+
if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
|
|
3602
3799
|
inlineEdit.startEdit(row);
|
|
3603
3800
|
return;
|
|
3604
3801
|
}
|
|
@@ -3610,9 +3807,10 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3610
3807
|
if (options.allowView && options.onView) options.onView(row);
|
|
3611
3808
|
},
|
|
3612
3809
|
onKeyDown: (event) => {
|
|
3613
|
-
if (
|
|
3614
|
-
else if (
|
|
3615
|
-
else if (
|
|
3810
|
+
if (inlineEdit.activeCell && event.key === "Escape") inlineEdit.stopCellEdit();
|
|
3811
|
+
else if (rowInRowEdit && event.key === "Escape") inlineEdit.cancelEdit(key);
|
|
3812
|
+
else if (rowInRowEdit && event.key === "Enter" && showRowInlineActions) inlineEdit.saveRow(key);
|
|
3813
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && rowInlineMode && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
|
|
3616
3814
|
else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
|
|
3617
3815
|
else emit(options.events.EDIT, { row });
|
|
3618
3816
|
else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
@@ -3662,13 +3860,29 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3662
3860
|
}),
|
|
3663
3861
|
visibleFields.map((field, columnIndex) => {
|
|
3664
3862
|
const width = getColumnWidth(field, resolvedColWidths);
|
|
3665
|
-
|
|
3863
|
+
const editable = options.allowEdit && !options.editDisabled && rowEditable(row) && canEditFieldInline(field);
|
|
3864
|
+
const cellActive = inlineEdit.isCellActive(key, field.name);
|
|
3865
|
+
const cellDirty = editing && inlineEdit.isCellDirty(key, field.name, row);
|
|
3866
|
+
const showCellEditor = editable && (rowInRowEdit && editing || cellEditMode && cellActive);
|
|
3867
|
+
const displayRow = editing ? rowDraft : row;
|
|
3868
|
+
const cellClassName = [
|
|
3869
|
+
showCellEditor ? "nb-datagrid__edit-cell" : "nb-datagrid__data-cell",
|
|
3870
|
+
editable && canInlineEditMode ? "nb-datagrid__cell--editable" : "",
|
|
3871
|
+
cellDirty ? "nb-datagrid__cell--dirty" : "",
|
|
3872
|
+
cellActive ? "nb-datagrid__cell--active" : ""
|
|
3873
|
+
].filter(Boolean).join(" ");
|
|
3874
|
+
const beginInlineEdit = () => {
|
|
3875
|
+
if (!editable) return;
|
|
3876
|
+
selectRow(row);
|
|
3877
|
+
if (cellEditMode) inlineEdit.startCellEdit(row, field.name);
|
|
3878
|
+
else if (rowInlineMode && !editing) inlineEdit.startEdit(row);
|
|
3879
|
+
};
|
|
3880
|
+
if (showCellEditor) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3666
3881
|
style: {
|
|
3667
3882
|
width,
|
|
3668
3883
|
textAlign: field.align
|
|
3669
3884
|
},
|
|
3670
|
-
className:
|
|
3671
|
-
onClick: (e) => e.stopPropagation(),
|
|
3885
|
+
className: cellClassName,
|
|
3672
3886
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineEditCell, {
|
|
3673
3887
|
field,
|
|
3674
3888
|
rowKey: key,
|
|
@@ -3678,7 +3892,8 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3678
3892
|
disabled: saving,
|
|
3679
3893
|
allRemoteOptions: filterRemoteOptions,
|
|
3680
3894
|
httpClient,
|
|
3681
|
-
t
|
|
3895
|
+
t,
|
|
3896
|
+
autoFocus: true
|
|
3682
3897
|
})
|
|
3683
3898
|
}, field.name);
|
|
3684
3899
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
@@ -3686,14 +3901,20 @@ const NativeDataGridView = (0, react.forwardRef)((options, ref) => {
|
|
|
3686
3901
|
width,
|
|
3687
3902
|
textAlign: field.align
|
|
3688
3903
|
},
|
|
3689
|
-
|
|
3690
|
-
|
|
3904
|
+
className: cellClassName,
|
|
3905
|
+
title: getCellText(field, displayRow, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
|
|
3906
|
+
onClick: (event) => {
|
|
3907
|
+
if (!editable || !canInlineEditMode) return;
|
|
3908
|
+
event.stopPropagation();
|
|
3909
|
+
beginInlineEdit();
|
|
3910
|
+
},
|
|
3911
|
+
children: renderCell(field, displayRow, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
|
|
3691
3912
|
}, field.name);
|
|
3692
3913
|
}),
|
|
3693
3914
|
hasRowActions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3694
3915
|
className: "nb-datagrid__actions-cell",
|
|
3695
3916
|
onClick: (e) => e.stopPropagation(),
|
|
3696
|
-
children:
|
|
3917
|
+
children: rowInRowEdit && showRowInlineActions ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3697
3918
|
className: "nb-datagrid__inline-actions",
|
|
3698
3919
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3699
3920
|
type: "button",
|
|
@@ -6978,6 +7199,8 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
|
|
|
6978
7199
|
selectionMode: hasMultipleSelection ? "multiple" : "single",
|
|
6979
7200
|
onSelectionChanged: handleSelectionChanged,
|
|
6980
7201
|
editMode: resolvedResource.editMode,
|
|
7202
|
+
inlineEditToolbar: resolvedResource.inlineEditToolbar,
|
|
7203
|
+
inlineRowActions: resolvedResource.inlineRowActions,
|
|
6981
7204
|
visibleColumns: presetState.visibleColumns,
|
|
6982
7205
|
beforeToolbar: renderPresetSelector,
|
|
6983
7206
|
aboveGrid: aboveGridContent,
|
package/dist/index.d.cts
CHANGED
|
@@ -23,6 +23,8 @@ interface GridHandle {
|
|
|
23
23
|
getSelectedRows: () => DataRecord$1[];
|
|
24
24
|
getFilter: () => unknown[];
|
|
25
25
|
filter: (filterRule: FilterRule | null) => void;
|
|
26
|
+
hasEditData: () => boolean;
|
|
27
|
+
saveChanges: () => Promise<boolean>;
|
|
26
28
|
}
|
|
27
29
|
//#endregion
|
|
28
30
|
//#region packages/crud/field/FieldType.d.ts
|
|
@@ -974,6 +976,13 @@ interface ResourceConfig<T extends DataRecord$1 = DataRecord$1> {
|
|
|
974
976
|
* - `'row'` | `'cell'` | `'batch'` enable inline editing when supported.
|
|
975
977
|
*/
|
|
976
978
|
editMode?: 'popup' | 'row' | 'cell' | 'batch';
|
|
979
|
+
/** Toolbar save/revert for inline edit. `false` hides them (use GridHandle instead). */
|
|
980
|
+
inlineEditToolbar?: boolean | {
|
|
981
|
+
save?: boolean;
|
|
982
|
+
revert?: boolean;
|
|
983
|
+
};
|
|
984
|
+
/** Per-row save/cancel in the actions column. Defaults by edit mode. */
|
|
985
|
+
inlineRowActions?: boolean;
|
|
977
986
|
/** Optional layout descriptor for the dialog form: tabs or collapsible sections. */
|
|
978
987
|
formLayout?: FormLayout;
|
|
979
988
|
/** Named column visibility presets. When defined, a preset selector is shown in the toolbar. */
|
|
@@ -1543,6 +1552,8 @@ interface DataGridViewOptions {
|
|
|
1543
1552
|
id: string;
|
|
1544
1553
|
title: string;
|
|
1545
1554
|
url: string;
|
|
1555
|
+
/** Controlled rows. When provided, the grid renders this data instead of loading from url. */
|
|
1556
|
+
data?: DataRecord$1[];
|
|
1546
1557
|
detailUrl?: string;
|
|
1547
1558
|
fields: Field[];
|
|
1548
1559
|
detailFields?: Field[] | ((parentRow: DataRecord$1) => Field[]);
|
|
@@ -1581,7 +1592,20 @@ interface DataGridViewOptions {
|
|
|
1581
1592
|
stateStoringEnabled?: boolean;
|
|
1582
1593
|
toolbarVisible?: boolean;
|
|
1583
1594
|
selectionMode?: 'single' | 'multiple';
|
|
1584
|
-
|
|
1595
|
+
/**
|
|
1596
|
+
* Save / revert buttons shown in the toolbar when inline edits are pending.
|
|
1597
|
+
* Set to `false` to hide and handle persistence via `GridHandle.saveChanges()`.
|
|
1598
|
+
* Defaults to `true` for `cell` and `batch` edit modes.
|
|
1599
|
+
*/
|
|
1600
|
+
inlineEditToolbar?: boolean | {
|
|
1601
|
+
save?: boolean;
|
|
1602
|
+
revert?: boolean;
|
|
1603
|
+
};
|
|
1604
|
+
/**
|
|
1605
|
+
* Per-row save / cancel icons in the actions column while a row is being edited.
|
|
1606
|
+
* Defaults to `true` for `row` mode and `false` for `cell` / `batch`.
|
|
1607
|
+
*/
|
|
1608
|
+
inlineRowActions?: boolean;
|
|
1585
1609
|
onBack?: () => void;
|
|
1586
1610
|
onAdd?: () => void;
|
|
1587
1611
|
onEdit?: (row: DataRecord$1) => void;
|
|
@@ -1591,6 +1615,8 @@ interface DataGridViewOptions {
|
|
|
1591
1615
|
onRowPrepared?: (event: unknown) => void;
|
|
1592
1616
|
onContentReady?: () => void;
|
|
1593
1617
|
onFilterChange?: (filters: Record<string, string>) => void;
|
|
1618
|
+
/** Custom batch persistence. Defaults to PATCHing each row to url/id when omitted. */
|
|
1619
|
+
onBatchSave?: (rows: DataRecord$1[]) => void | Promise<void>;
|
|
1594
1620
|
filterRow?: boolean;
|
|
1595
1621
|
headerFilter?: boolean;
|
|
1596
1622
|
manualLoad?: boolean;
|