@nubitio/crud 0.5.26 → 0.5.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +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 +138 -19
- package/package.json +3 -3
package/dist/index.d.mts
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;
|
package/dist/index.mjs
CHANGED
|
@@ -2156,10 +2156,20 @@ function serializeDetailRows(rows, detailFields, detailIdField, isEditMode, adap
|
|
|
2156
2156
|
function canEditFieldInline(field) {
|
|
2157
2157
|
return !field.isIdentity && !field.readonly && field.type !== "file" && field.visibleOnForm !== false;
|
|
2158
2158
|
}
|
|
2159
|
-
function
|
|
2159
|
+
function valuesEqual$1(a, b) {
|
|
2160
|
+
if (a === b) return true;
|
|
2161
|
+
if (a == null && b == null) return true;
|
|
2162
|
+
if (a == null || b == null) return false;
|
|
2163
|
+
const numA = Number(a);
|
|
2164
|
+
const numB = Number(b);
|
|
2165
|
+
if (!Number.isNaN(numA) && !Number.isNaN(numB)) return numA === numB;
|
|
2166
|
+
return String(a) === String(b);
|
|
2167
|
+
}
|
|
2168
|
+
function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError, onBatchSave }) {
|
|
2160
2169
|
const [draftRows, setDraftRows] = useState(/* @__PURE__ */ new Map());
|
|
2161
2170
|
const [savingRows, setSavingRows] = useState(/* @__PURE__ */ new Set());
|
|
2162
2171
|
const [rowErrors, setRowErrors] = useState(/* @__PURE__ */ new Map());
|
|
2172
|
+
const [activeCell, setActiveCell] = useState(null);
|
|
2163
2173
|
const draftRowsRef = useRef(draftRows);
|
|
2164
2174
|
draftRowsRef.current = draftRows;
|
|
2165
2175
|
const optsRef = useRef({
|
|
@@ -2169,7 +2179,8 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2169
2179
|
httpClient,
|
|
2170
2180
|
fields,
|
|
2171
2181
|
onSaveSuccess,
|
|
2172
|
-
onSaveError
|
|
2182
|
+
onSaveError,
|
|
2183
|
+
onBatchSave
|
|
2173
2184
|
});
|
|
2174
2185
|
optsRef.current = {
|
|
2175
2186
|
url,
|
|
@@ -2178,9 +2189,21 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2178
2189
|
httpClient,
|
|
2179
2190
|
fields,
|
|
2180
2191
|
onSaveSuccess,
|
|
2181
|
-
onSaveError
|
|
2192
|
+
onSaveError,
|
|
2193
|
+
onBatchSave
|
|
2182
2194
|
};
|
|
2183
|
-
const isEditing = useCallback((key) =>
|
|
2195
|
+
const isEditing = useCallback((key) => draftRows.has(key), [draftRows]);
|
|
2196
|
+
const isCellActive = useCallback((key, fieldName) => activeCell != null && activeCell.key === key && activeCell.fieldName === fieldName, [activeCell]);
|
|
2197
|
+
const isCellDirty = useCallback((key, fieldName, original) => {
|
|
2198
|
+
const draft = draftRows.get(key);
|
|
2199
|
+
if (!draft) return false;
|
|
2200
|
+
return !valuesEqual$1(draft[fieldName], original[fieldName]);
|
|
2201
|
+
}, [draftRows]);
|
|
2202
|
+
const hasDraftChanges = useCallback((key, original) => {
|
|
2203
|
+
const draft = draftRows.get(key);
|
|
2204
|
+
if (!draft) return false;
|
|
2205
|
+
return optsRef.current.fields.some((field) => canEditFieldInline(field) && !valuesEqual$1(draft[field.name], original[field.name]));
|
|
2206
|
+
}, [draftRows]);
|
|
2184
2207
|
const startEdit = useCallback((row) => {
|
|
2185
2208
|
const key = row[optsRef.current.idField];
|
|
2186
2209
|
setDraftRows((prev) => {
|
|
@@ -2192,7 +2215,28 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2192
2215
|
next.delete(key);
|
|
2193
2216
|
return next;
|
|
2194
2217
|
});
|
|
2218
|
+
setActiveCell(null);
|
|
2195
2219
|
}, [mode]);
|
|
2220
|
+
const startCellEdit = useCallback((row, fieldName) => {
|
|
2221
|
+
const key = row[optsRef.current.idField];
|
|
2222
|
+
setDraftRows((prev) => {
|
|
2223
|
+
const existing = prev.get(key);
|
|
2224
|
+
const withoutKey = (mode === "row" ? [] : Array.from(prev.entries())).filter(([entryKey]) => entryKey !== key);
|
|
2225
|
+
return new Map([...withoutKey, [key, existing ? { ...existing } : { ...row }]]);
|
|
2226
|
+
});
|
|
2227
|
+
setRowErrors((prev) => {
|
|
2228
|
+
const next = new Map(prev);
|
|
2229
|
+
next.delete(key);
|
|
2230
|
+
return next;
|
|
2231
|
+
});
|
|
2232
|
+
setActiveCell({
|
|
2233
|
+
key,
|
|
2234
|
+
fieldName
|
|
2235
|
+
});
|
|
2236
|
+
}, [mode]);
|
|
2237
|
+
const stopCellEdit = useCallback(() => {
|
|
2238
|
+
setActiveCell(null);
|
|
2239
|
+
}, []);
|
|
2196
2240
|
const cancelEdit = useCallback((key) => {
|
|
2197
2241
|
setDraftRows((prev) => {
|
|
2198
2242
|
const next = new Map(prev);
|
|
@@ -2204,10 +2248,12 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2204
2248
|
next.delete(key);
|
|
2205
2249
|
return next;
|
|
2206
2250
|
});
|
|
2251
|
+
setActiveCell((current) => current?.key === key ? null : current);
|
|
2207
2252
|
}, []);
|
|
2208
2253
|
const discardAll = useCallback(() => {
|
|
2209
2254
|
setDraftRows(/* @__PURE__ */ new Map());
|
|
2210
2255
|
setRowErrors(/* @__PURE__ */ new Map());
|
|
2256
|
+
setActiveCell(null);
|
|
2211
2257
|
}, []);
|
|
2212
2258
|
const updateDraft = useCallback((key, fieldName, value) => {
|
|
2213
2259
|
setDraftRows((prev) => {
|
|
@@ -2274,35 +2320,102 @@ function useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient,
|
|
|
2274
2320
|
});
|
|
2275
2321
|
}
|
|
2276
2322
|
}, []);
|
|
2323
|
+
const validateDraft = useCallback((key, draft, fs) => {
|
|
2324
|
+
const errors = {};
|
|
2325
|
+
fs.forEach((field) => {
|
|
2326
|
+
if (!canEditFieldInline(field)) return;
|
|
2327
|
+
if (field.required && (draft[field.name] == null || draft[field.name] === "")) errors[field.name] = "required";
|
|
2328
|
+
});
|
|
2329
|
+
if (Object.keys(errors).length === 0) return true;
|
|
2330
|
+
setRowErrors((prev) => new Map(prev).set(key, errors));
|
|
2331
|
+
return false;
|
|
2332
|
+
}, []);
|
|
2333
|
+
const doSaveBatch = useCallback(async () => {
|
|
2334
|
+
const { fields: fs, onBatchSave: saveBatch, onSaveError: onErr } = optsRef.current;
|
|
2335
|
+
if (!saveBatch) return false;
|
|
2336
|
+
const entries = Array.from(draftRowsRef.current.entries());
|
|
2337
|
+
if (entries.length === 0) return false;
|
|
2338
|
+
if (!entries.every(([key, draft]) => validateDraft(key, draft, fs))) return false;
|
|
2339
|
+
const keys = entries.map(([key]) => key);
|
|
2340
|
+
setSavingRows((prev) => new Set([...prev, ...keys]));
|
|
2341
|
+
try {
|
|
2342
|
+
await saveBatch(entries.map(([, draft]) => ({ ...draft })));
|
|
2343
|
+
setDraftRows(/* @__PURE__ */ new Map());
|
|
2344
|
+
setRowErrors(/* @__PURE__ */ new Map());
|
|
2345
|
+
return true;
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
keys.forEach((key) => onErr?.(key, err));
|
|
2348
|
+
return false;
|
|
2349
|
+
} finally {
|
|
2350
|
+
setSavingRows((prev) => {
|
|
2351
|
+
const next = new Set(prev);
|
|
2352
|
+
keys.forEach((key) => next.delete(key));
|
|
2353
|
+
return next;
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
}, [validateDraft]);
|
|
2277
2357
|
return {
|
|
2278
2358
|
draftRows,
|
|
2279
2359
|
savingRows,
|
|
2280
2360
|
rowErrors,
|
|
2361
|
+
activeCell,
|
|
2281
2362
|
isEditing,
|
|
2363
|
+
isCellActive,
|
|
2364
|
+
isCellDirty,
|
|
2365
|
+
hasDraftChanges,
|
|
2282
2366
|
startEdit,
|
|
2367
|
+
startCellEdit,
|
|
2368
|
+
stopCellEdit,
|
|
2283
2369
|
cancelEdit,
|
|
2284
2370
|
discardAll,
|
|
2285
2371
|
updateDraft,
|
|
2286
2372
|
saveRow: useCallback(async (key) => {
|
|
2287
|
-
if (
|
|
2288
|
-
|
|
2373
|
+
if (optsRef.current.onBatchSave) {
|
|
2374
|
+
const ok = await doSaveBatch();
|
|
2375
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2376
|
+
return ok;
|
|
2377
|
+
}
|
|
2378
|
+
const ok = await doSaveRow(key);
|
|
2379
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2380
|
+
return ok;
|
|
2381
|
+
}, [doSaveBatch, doSaveRow]),
|
|
2289
2382
|
saveAll: useCallback(async () => {
|
|
2383
|
+
if (optsRef.current.onBatchSave) {
|
|
2384
|
+
const ok = await doSaveBatch();
|
|
2385
|
+
if (ok) optsRef.current.onSaveSuccess?.();
|
|
2386
|
+
return ok;
|
|
2387
|
+
}
|
|
2290
2388
|
const keys = Array.from(draftRowsRef.current.keys());
|
|
2291
|
-
|
|
2292
|
-
|
|
2389
|
+
const anySuccess = (await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value);
|
|
2390
|
+
if (anySuccess) optsRef.current.onSaveSuccess?.();
|
|
2391
|
+
return anySuccess;
|
|
2392
|
+
}, [doSaveBatch, doSaveRow])
|
|
2293
2393
|
};
|
|
2294
2394
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2395
|
+
function focusInlineControl(container) {
|
|
2396
|
+
if (!container) return;
|
|
2397
|
+
const focusable = container.querySelector("input:not([type=\"hidden\"]):not([disabled]):not([readonly]), select:not([disabled]), textarea:not([disabled]), .nb-date-picker__input:not([readonly]), .nb-dropdown__trigger:not([disabled])");
|
|
2398
|
+
focusable?.focus();
|
|
2399
|
+
if (focusable instanceof HTMLInputElement && focusable.type !== "checkbox") focusable.select();
|
|
2400
|
+
}
|
|
2297
2401
|
/**
|
|
2298
|
-
* Renders a single cell as an editable control during inline
|
|
2299
|
-
* Uses the same FieldTypeModule.ControlRender path as the full form,
|
|
2300
|
-
*
|
|
2402
|
+
* Renders a single cell as an editable control during inline editing.
|
|
2403
|
+
* Uses the same FieldTypeModule.ControlRender path as the full form, with
|
|
2404
|
+
* compact styling so controls fit naturally inside a table cell.
|
|
2301
2405
|
*/
|
|
2302
|
-
function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t }) {
|
|
2406
|
+
function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = false, allRemoteOptions, httpClient, t, autoFocus = true }) {
|
|
2407
|
+
const containerRef = useRef(null);
|
|
2303
2408
|
const typeModule = getFieldTypeModule(field.type);
|
|
2304
2409
|
const fieldError = errors?.[field.name];
|
|
2305
2410
|
const errorClass = fieldError ? " is-error" : "";
|
|
2411
|
+
useEffect(() => {
|
|
2412
|
+
if (!autoFocus) return;
|
|
2413
|
+
focusInlineControl(containerRef.current);
|
|
2414
|
+
}, [
|
|
2415
|
+
autoFocus,
|
|
2416
|
+
field.name,
|
|
2417
|
+
rowKey
|
|
2418
|
+
]);
|
|
2306
2419
|
const commonProps = {
|
|
2307
2420
|
className: `nb-inline-control${errorClass}`,
|
|
2308
2421
|
disabled,
|
|
@@ -2323,7 +2436,16 @@ function InlineEditCell({ field, rowKey, draft, onChange, errors, disabled = fal
|
|
|
2323
2436
|
upsertUploadedFile: () => {}
|
|
2324
2437
|
};
|
|
2325
2438
|
return /* @__PURE__ */ jsx("div", {
|
|
2439
|
+
ref: containerRef,
|
|
2326
2440
|
className: `nb-inline-cell${errorClass ? " nb-inline-cell--error" : ""}`,
|
|
2441
|
+
onKeyDown: (event) => {
|
|
2442
|
+
if (event.key === "Escape") event.stopPropagation();
|
|
2443
|
+
},
|
|
2444
|
+
onMouseDown: (event) => {
|
|
2445
|
+
if (event.target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
|
|
2446
|
+
event.stopPropagation();
|
|
2447
|
+
},
|
|
2448
|
+
onClick: (event) => event.stopPropagation(),
|
|
2327
2449
|
children: typeModule.ControlRender({
|
|
2328
2450
|
field,
|
|
2329
2451
|
value: draft[field.name],
|
|
@@ -2452,6 +2574,25 @@ function normalizeIcon$1(icon) {
|
|
|
2452
2574
|
function isDateLikeField(field) {
|
|
2453
2575
|
return field.type === "date" || field.type === "datetime";
|
|
2454
2576
|
}
|
|
2577
|
+
function isCellEditMode(editMode) {
|
|
2578
|
+
return editMode === "cell" || editMode === "batch";
|
|
2579
|
+
}
|
|
2580
|
+
function resolveInlineEditToolbar(editMode, inlineEditToolbar) {
|
|
2581
|
+
if (inlineEditToolbar === false) return null;
|
|
2582
|
+
const defaultShow = isCellEditMode(editMode);
|
|
2583
|
+
if (inlineEditToolbar == null) return defaultShow ? {
|
|
2584
|
+
save: true,
|
|
2585
|
+
revert: true
|
|
2586
|
+
} : null;
|
|
2587
|
+
if (typeof inlineEditToolbar === "boolean") return inlineEditToolbar ? {
|
|
2588
|
+
save: true,
|
|
2589
|
+
revert: true
|
|
2590
|
+
} : null;
|
|
2591
|
+
return {
|
|
2592
|
+
save: inlineEditToolbar.save !== false,
|
|
2593
|
+
revert: inlineEditToolbar.revert !== false
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2455
2596
|
function getToolbarKey(action, index) {
|
|
2456
2597
|
return action.key ?? action.text ?? String(index);
|
|
2457
2598
|
}
|
|
@@ -2805,6 +2946,15 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2805
2946
|
fieldsRef.current = options.fields;
|
|
2806
2947
|
const loadSeqRef = useRef(0);
|
|
2807
2948
|
const loadRows = useCallback(async () => {
|
|
2949
|
+
if (options.data) {
|
|
2950
|
+
rowsRef.current = options.data;
|
|
2951
|
+
setRows(options.data);
|
|
2952
|
+
setTotalCount(options.data.length);
|
|
2953
|
+
setGridSummary(options.gridSummary ?? null);
|
|
2954
|
+
setIsGridLoading(false);
|
|
2955
|
+
onContentReadyRef.current?.();
|
|
2956
|
+
return options.data;
|
|
2957
|
+
}
|
|
2808
2958
|
if (options.manualLoad) return rowsRef.current;
|
|
2809
2959
|
setIsGridLoading(true);
|
|
2810
2960
|
const seq = ++loadSeqRef.current;
|
|
@@ -2828,6 +2978,8 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2828
2978
|
}, [
|
|
2829
2979
|
filterOperators,
|
|
2830
2980
|
filters,
|
|
2981
|
+
options.data,
|
|
2982
|
+
options.gridSummary,
|
|
2831
2983
|
options.manualLoad,
|
|
2832
2984
|
options.paging,
|
|
2833
2985
|
page,
|
|
@@ -2871,30 +3023,58 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2871
3023
|
resourceStoreFactory,
|
|
2872
3024
|
visibleFields
|
|
2873
3025
|
]);
|
|
2874
|
-
const canInlineEditMode = options.editMode === "row" || options.editMode === "batch";
|
|
3026
|
+
const canInlineEditMode = options.editMode === "row" || options.editMode === "cell" || options.editMode === "batch";
|
|
3027
|
+
const cellEditMode = isCellEditMode(options.editMode);
|
|
3028
|
+
const rowInlineMode = options.editMode === "row";
|
|
3029
|
+
const inlineEditToolbar = resolveInlineEditToolbar(options.editMode, options.inlineEditToolbar);
|
|
3030
|
+
const showRowInlineActions = options.inlineRowActions ?? rowInlineMode;
|
|
2875
3031
|
const inlineEdit = useInlineEdit({
|
|
2876
|
-
mode:
|
|
3032
|
+
mode: cellEditMode ? "batch" : "row",
|
|
2877
3033
|
url: options.url,
|
|
2878
3034
|
idField,
|
|
2879
3035
|
adapter: options.adapter,
|
|
2880
3036
|
httpClient,
|
|
2881
3037
|
fields: options.fields,
|
|
2882
3038
|
onSaveSuccess: () => void loadRows(),
|
|
2883
|
-
onSaveError: () => {}
|
|
3039
|
+
onSaveError: () => {},
|
|
3040
|
+
onBatchSave: options.onBatchSave
|
|
2884
3041
|
});
|
|
3042
|
+
const dirtyRowCount = useMemo(() => rows.filter((row) => {
|
|
3043
|
+
const key = row[idField] ?? row;
|
|
3044
|
+
return inlineEdit.draftRows.has(key) && inlineEdit.hasDraftChanges(key, row);
|
|
3045
|
+
}).length, [
|
|
3046
|
+
rows,
|
|
3047
|
+
idField,
|
|
3048
|
+
inlineEdit.draftRows,
|
|
3049
|
+
inlineEdit.hasDraftChanges
|
|
3050
|
+
]);
|
|
3051
|
+
const hasPendingInlineEdits = dirtyRowCount > 0;
|
|
3052
|
+
useEffect(() => {
|
|
3053
|
+
if (!inlineEdit.activeCell) return;
|
|
3054
|
+
const handler = (event) => {
|
|
3055
|
+
const target = event.target;
|
|
3056
|
+
if (target.closest(".nb-datagrid__edit-cell")) return;
|
|
3057
|
+
if (target.closest(".nb-date-picker__panel, .nb-form__lookup-menu, .nb-dropdown__menu, .nb-datagrid__actions-popover")) return;
|
|
3058
|
+
inlineEdit.stopCellEdit();
|
|
3059
|
+
};
|
|
3060
|
+
document.addEventListener("mousedown", handler);
|
|
3061
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
3062
|
+
}, [inlineEdit.activeCell, inlineEdit.stopCellEdit]);
|
|
2885
3063
|
const handleStateRef = useRef({
|
|
2886
3064
|
selectedKeys,
|
|
2887
3065
|
filters,
|
|
2888
3066
|
filterOperators,
|
|
2889
3067
|
loadRows,
|
|
2890
|
-
idField
|
|
3068
|
+
idField,
|
|
3069
|
+
inlineEdit
|
|
2891
3070
|
});
|
|
2892
3071
|
handleStateRef.current = {
|
|
2893
3072
|
selectedKeys,
|
|
2894
3073
|
filters,
|
|
2895
3074
|
filterOperators,
|
|
2896
3075
|
loadRows,
|
|
2897
|
-
idField
|
|
3076
|
+
idField,
|
|
3077
|
+
inlineEdit
|
|
2898
3078
|
};
|
|
2899
3079
|
useImperativeHandle(ref, () => ({
|
|
2900
3080
|
showLoading: (message) => {
|
|
@@ -2934,7 +3114,9 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2934
3114
|
setFilterInputs(nextFilters);
|
|
2935
3115
|
setFilterOperators(nextOperators);
|
|
2936
3116
|
setPage(0);
|
|
2937
|
-
}
|
|
3117
|
+
},
|
|
3118
|
+
hasEditData: () => handleStateRef.current.inlineEdit.draftRows.size > 0,
|
|
3119
|
+
saveChanges: () => handleStateRef.current.inlineEdit.saveAll()
|
|
2938
3120
|
}), [options.fields, t]);
|
|
2939
3121
|
const selectedRows = rows.filter((row) => selectedKeys.includes(row[idField]));
|
|
2940
3122
|
const selectRow = (row) => {
|
|
@@ -2948,7 +3130,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2948
3130
|
const rowEditable = (row) => options.canEditRow?.(row) !== false;
|
|
2949
3131
|
const rowDeletable = (row) => options.canDeleteRow?.(row) !== false;
|
|
2950
3132
|
const buildRowActions = (row) => [
|
|
2951
|
-
...options.allowEdit && rowEditable(row) && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
|
|
3133
|
+
...options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode && !inlineEdit.isEditing(row[idField]) ? [{
|
|
2952
3134
|
text: t("grid.inlineEditRow"),
|
|
2953
3135
|
icon: "ph-pencil-simple",
|
|
2954
3136
|
disabled: options.editDisabled,
|
|
@@ -2978,7 +3160,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
2978
3160
|
}] : []
|
|
2979
3161
|
];
|
|
2980
3162
|
const openRow = (row) => {
|
|
2981
|
-
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3163
|
+
if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
|
|
2982
3164
|
inlineEdit.startEdit(row);
|
|
2983
3165
|
return;
|
|
2984
3166
|
}
|
|
@@ -3113,7 +3295,7 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3113
3295
|
};
|
|
3114
3296
|
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
3115
3297
|
const hasCheckbox = options.selectionMode === "multiple";
|
|
3116
|
-
const hasBuiltInRowActions = Boolean(options.allowEdit && (
|
|
3298
|
+
const hasBuiltInRowActions = Boolean(options.allowEdit && (rowInlineMode && showRowInlineActions || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
|
|
3117
3299
|
const hasRowActions = Boolean(options.mode !== "minimal" && (hasBuiltInRowActions || options.rowActions));
|
|
3118
3300
|
const colSpan = visibleFields.length + (hasCheckbox ? 1 : 0) + (options.detailFields ? 1 : 0) + (hasRowActions ? 1 : 0);
|
|
3119
3301
|
const hasDetail = Boolean(options.detailFields);
|
|
@@ -3128,15 +3310,26 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3128
3310
|
actionsColWidth
|
|
3129
3311
|
});
|
|
3130
3312
|
const resolvedColWidths = useMemo(() => {
|
|
3131
|
-
if (visibleFields.length === 0) return colWidths;
|
|
3132
|
-
const
|
|
3313
|
+
if (visibleFields.length === 0 || containerWidth <= 0) return colWidths;
|
|
3314
|
+
const bases = visibleFields.map((f) => getColumnWidth(f, colWidths));
|
|
3315
|
+
const dataTotal = bases.reduce((sum, width) => sum + width, 0);
|
|
3133
3316
|
const fixedTotal = (hasCheckbox ? CHECKBOX_COL_WIDTH : 0) + (hasDetail ? DETAIL_COL_WIDTH : 0) + (hasRowActions ? actionsColWidth : 0);
|
|
3134
|
-
const
|
|
3317
|
+
const available = Math.max(0, containerWidth - fixedTotal);
|
|
3318
|
+
if (dataTotal > available && available > 0) {
|
|
3319
|
+
const scale = available / dataTotal;
|
|
3320
|
+
const result = {};
|
|
3321
|
+
visibleFields.forEach((field, index) => {
|
|
3322
|
+
const floor = field.minWidth ?? MIN_COL_WIDTH;
|
|
3323
|
+
result[field.name] = Math.max(floor, Math.floor(bases[index] * scale));
|
|
3324
|
+
});
|
|
3325
|
+
return result;
|
|
3326
|
+
}
|
|
3327
|
+
const extra = Math.max(0, available - dataTotal);
|
|
3135
3328
|
if (extra === 0 || dataTotal === 0) return colWidths;
|
|
3136
3329
|
let distributed = 0;
|
|
3137
3330
|
const result = {};
|
|
3138
3331
|
visibleFields.forEach((f, i) => {
|
|
3139
|
-
const base =
|
|
3332
|
+
const base = bases[i];
|
|
3140
3333
|
const share = i < visibleFields.length - 1 ? Math.round(extra * (base / dataTotal)) : extra - distributed;
|
|
3141
3334
|
result[f.name] = base + share;
|
|
3142
3335
|
distributed += share;
|
|
@@ -3148,7 +3341,8 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3148
3341
|
hasCheckbox,
|
|
3149
3342
|
hasDetail,
|
|
3150
3343
|
hasRowActions,
|
|
3151
|
-
containerWidth
|
|
3344
|
+
containerWidth,
|
|
3345
|
+
actionsColWidth
|
|
3152
3346
|
]);
|
|
3153
3347
|
const tableLayoutStyle = { "--nb-datagrid-layout-width": `${layoutWidth}px` };
|
|
3154
3348
|
const filterableFields = visibleFields.filter((field) => field.filterable);
|
|
@@ -3227,6 +3421,18 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3227
3421
|
children: activeFilterCount
|
|
3228
3422
|
})]
|
|
3229
3423
|
}),
|
|
3424
|
+
inlineEditToolbar?.revert && hasPendingInlineEdits && /* @__PURE__ */ jsx(IconButton, {
|
|
3425
|
+
className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--revert",
|
|
3426
|
+
icon: "ph ph-arrow-counter-clockwise",
|
|
3427
|
+
label: t("grid.inlineRevertChanges"),
|
|
3428
|
+
onClick: () => inlineEdit.discardAll()
|
|
3429
|
+
}),
|
|
3430
|
+
inlineEditToolbar?.save && hasPendingInlineEdits && /* @__PURE__ */ jsx(IconButton, {
|
|
3431
|
+
className: "nb-datagrid__toolbar-icon-action nb-datagrid__toolbar-icon-action--save",
|
|
3432
|
+
icon: "ph ph-floppy-disk",
|
|
3433
|
+
label: t("grid.inlineSaveChanges"),
|
|
3434
|
+
onClick: () => void inlineEdit.saveAll()
|
|
3435
|
+
}),
|
|
3230
3436
|
toolbar.showRefresh !== false && /* @__PURE__ */ jsx(IconButton, {
|
|
3231
3437
|
icon: "ph ph-arrow-clockwise",
|
|
3232
3438
|
label: t("grid.buttonRefresh"),
|
|
@@ -3234,26 +3440,13 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3234
3440
|
})
|
|
3235
3441
|
]
|
|
3236
3442
|
}),
|
|
3237
|
-
|
|
3238
|
-
className: "nb-datagrid__batch-bar",
|
|
3443
|
+
cellEditMode && hasPendingInlineEdits && !inlineEditToolbar && /* @__PURE__ */ jsx("div", {
|
|
3444
|
+
className: "nb-datagrid__batch-bar nb-datagrid__batch-bar--compact",
|
|
3239
3445
|
role: "status",
|
|
3240
|
-
children:
|
|
3446
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
3241
3447
|
className: "nb-datagrid__batch-bar-label",
|
|
3242
|
-
children: t("grid.inlineUnsavedRows", { count:
|
|
3243
|
-
})
|
|
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
|
-
})]
|
|
3448
|
+
children: t("grid.inlineUnsavedRows", { count: dirtyRowCount })
|
|
3449
|
+
})
|
|
3257
3450
|
}),
|
|
3258
3451
|
options.aboveGrid ? /* @__PURE__ */ jsx("div", {
|
|
3259
3452
|
className: "nb-datagrid__above-grid",
|
|
@@ -3549,9 +3742,11 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3549
3742
|
const key = row[idField] ?? rowIndex;
|
|
3550
3743
|
const selected = selectedKeys.includes(key);
|
|
3551
3744
|
const editing = inlineEdit.draftRows.has(key);
|
|
3745
|
+
const rowHasChanges = editing && inlineEdit.hasDraftChanges(key, row);
|
|
3552
3746
|
const saving = inlineEdit.savingRows.has(key);
|
|
3553
3747
|
const rowDraft = inlineEdit.draftRows.get(key) ?? row;
|
|
3554
3748
|
const rowFieldErrors = inlineEdit.rowErrors.get(key);
|
|
3749
|
+
const rowInRowEdit = rowInlineMode && editing;
|
|
3555
3750
|
const detailFields = typeof options.detailFields === "function" ? options.detailFields(row) : options.detailFields;
|
|
3556
3751
|
const detailUrl = options.detailUrl?.replace("{id}", String(key));
|
|
3557
3752
|
const expanded = expandedKeys.has(key);
|
|
@@ -3561,17 +3756,19 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3561
3756
|
"nb-datagrid__row",
|
|
3562
3757
|
expanded ? "nb-datagrid__row--expanded" : "",
|
|
3563
3758
|
selected ? "nb-datagrid__row--selected" : "",
|
|
3564
|
-
|
|
3759
|
+
rowInRowEdit ? "nb-datagrid__row--editing" : "",
|
|
3760
|
+
rowHasChanges ? "nb-datagrid__row--dirty" : "",
|
|
3565
3761
|
saving ? "nb-datagrid__row--saving" : ""
|
|
3566
3762
|
].filter(Boolean).join(" "),
|
|
3567
3763
|
tabIndex: 0,
|
|
3568
3764
|
"aria-selected": selected,
|
|
3569
3765
|
onClick: () => {
|
|
3570
|
-
if (
|
|
3766
|
+
if (rowInRowEdit) return;
|
|
3767
|
+
selectRow(row);
|
|
3571
3768
|
},
|
|
3572
3769
|
onDoubleClick: () => {
|
|
3573
|
-
if (
|
|
3574
|
-
if (options.allowEdit && rowEditable(row) && canInlineEditMode) {
|
|
3770
|
+
if (rowInRowEdit || inlineEdit.activeCell) return;
|
|
3771
|
+
if (options.allowEdit && rowEditable(row) && rowInlineMode && canInlineEditMode) {
|
|
3575
3772
|
inlineEdit.startEdit(row);
|
|
3576
3773
|
return;
|
|
3577
3774
|
}
|
|
@@ -3583,9 +3780,10 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3583
3780
|
if (options.allowView && options.onView) options.onView(row);
|
|
3584
3781
|
},
|
|
3585
3782
|
onKeyDown: (event) => {
|
|
3586
|
-
if (
|
|
3587
|
-
else if (
|
|
3588
|
-
else if (
|
|
3783
|
+
if (inlineEdit.activeCell && event.key === "Escape") inlineEdit.stopCellEdit();
|
|
3784
|
+
else if (rowInRowEdit && event.key === "Escape") inlineEdit.cancelEdit(key);
|
|
3785
|
+
else if (rowInRowEdit && event.key === "Enter" && showRowInlineActions) inlineEdit.saveRow(key);
|
|
3786
|
+
else if (!editing && event.key === "Enter" && options.allowEdit && rowInlineMode && canInlineEditMode && rowEditable(row)) inlineEdit.startEdit(row);
|
|
3589
3787
|
else if (!editing && event.key === "Enter" && options.allowEdit && (options.onEdit || options.events?.EDIT)) if (options.onEdit) options.onEdit(row);
|
|
3590
3788
|
else emit(options.events.EDIT, { row });
|
|
3591
3789
|
else if (!editing && event.key === "Enter" && options.allowView && options.onView) options.onView(row);
|
|
@@ -3635,13 +3833,29 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3635
3833
|
}),
|
|
3636
3834
|
visibleFields.map((field, columnIndex) => {
|
|
3637
3835
|
const width = getColumnWidth(field, resolvedColWidths);
|
|
3638
|
-
|
|
3836
|
+
const editable = options.allowEdit && !options.editDisabled && rowEditable(row) && canEditFieldInline(field);
|
|
3837
|
+
const cellActive = inlineEdit.isCellActive(key, field.name);
|
|
3838
|
+
const cellDirty = editing && inlineEdit.isCellDirty(key, field.name, row);
|
|
3839
|
+
const showCellEditor = editable && (rowInRowEdit && editing || cellEditMode && cellActive);
|
|
3840
|
+
const displayRow = editing ? rowDraft : row;
|
|
3841
|
+
const cellClassName = [
|
|
3842
|
+
showCellEditor ? "nb-datagrid__edit-cell" : "nb-datagrid__data-cell",
|
|
3843
|
+
editable && canInlineEditMode ? "nb-datagrid__cell--editable" : "",
|
|
3844
|
+
cellDirty ? "nb-datagrid__cell--dirty" : "",
|
|
3845
|
+
cellActive ? "nb-datagrid__cell--active" : ""
|
|
3846
|
+
].filter(Boolean).join(" ");
|
|
3847
|
+
const beginInlineEdit = () => {
|
|
3848
|
+
if (!editable) return;
|
|
3849
|
+
selectRow(row);
|
|
3850
|
+
if (cellEditMode) inlineEdit.startCellEdit(row, field.name);
|
|
3851
|
+
else if (rowInlineMode && !editing) inlineEdit.startEdit(row);
|
|
3852
|
+
};
|
|
3853
|
+
if (showCellEditor) return /* @__PURE__ */ jsx("td", {
|
|
3639
3854
|
style: {
|
|
3640
3855
|
width,
|
|
3641
3856
|
textAlign: field.align
|
|
3642
3857
|
},
|
|
3643
|
-
className:
|
|
3644
|
-
onClick: (e) => e.stopPropagation(),
|
|
3858
|
+
className: cellClassName,
|
|
3645
3859
|
children: /* @__PURE__ */ jsx(InlineEditCell, {
|
|
3646
3860
|
field,
|
|
3647
3861
|
rowKey: key,
|
|
@@ -3651,7 +3865,8 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3651
3865
|
disabled: saving,
|
|
3652
3866
|
allRemoteOptions: filterRemoteOptions,
|
|
3653
3867
|
httpClient,
|
|
3654
|
-
t
|
|
3868
|
+
t,
|
|
3869
|
+
autoFocus: true
|
|
3655
3870
|
})
|
|
3656
3871
|
}, field.name);
|
|
3657
3872
|
return /* @__PURE__ */ jsx("td", {
|
|
@@ -3659,14 +3874,20 @@ const NativeDataGridView = forwardRef((options, ref) => {
|
|
|
3659
3874
|
width,
|
|
3660
3875
|
textAlign: field.align
|
|
3661
3876
|
},
|
|
3662
|
-
|
|
3663
|
-
|
|
3877
|
+
className: cellClassName,
|
|
3878
|
+
title: getCellText(field, displayRow, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
|
|
3879
|
+
onClick: (event) => {
|
|
3880
|
+
if (!editable || !canInlineEditMode) return;
|
|
3881
|
+
event.stopPropagation();
|
|
3882
|
+
beginInlineEdit();
|
|
3883
|
+
},
|
|
3884
|
+
children: renderCell(field, displayRow, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
|
|
3664
3885
|
}, field.name);
|
|
3665
3886
|
}),
|
|
3666
3887
|
hasRowActions && /* @__PURE__ */ jsx("td", {
|
|
3667
3888
|
className: "nb-datagrid__actions-cell",
|
|
3668
3889
|
onClick: (e) => e.stopPropagation(),
|
|
3669
|
-
children:
|
|
3890
|
+
children: rowInRowEdit && showRowInlineActions ? /* @__PURE__ */ jsxs("div", {
|
|
3670
3891
|
className: "nb-datagrid__inline-actions",
|
|
3671
3892
|
children: [/* @__PURE__ */ jsx("button", {
|
|
3672
3893
|
type: "button",
|
|
@@ -6951,6 +7172,8 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
|
|
|
6951
7172
|
selectionMode: hasMultipleSelection ? "multiple" : "single",
|
|
6952
7173
|
onSelectionChanged: handleSelectionChanged,
|
|
6953
7174
|
editMode: resolvedResource.editMode,
|
|
7175
|
+
inlineEditToolbar: resolvedResource.inlineEditToolbar,
|
|
7176
|
+
inlineRowActions: resolvedResource.inlineRowActions,
|
|
6954
7177
|
visibleColumns: presetState.visibleColumns,
|
|
6955
7178
|
beforeToolbar: renderPresetSelector,
|
|
6956
7179
|
aboveGrid: aboveGridContent,
|