@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.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
- inlineActions?: boolean;
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 useInlineEdit({ mode, url, idField, adapter = HydraAdapter, httpClient, fields, onSaveSuccess, onSaveError }) {
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) => draftRowsRef.current.has(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 (await doSaveRow(key)) optsRef.current.onSaveSuccess?.();
2288
- }, [doSaveRow]),
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
- if ((await Promise.allSettled(keys.map((key) => doSaveRow(key)))).some((r) => r.status === "fulfilled" && r.value)) optsRef.current.onSaveSuccess?.();
2292
- }, [doSaveRow])
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
- //#endregion
2296
- //#region packages/crud/datagrid/InlineEditCell.tsx
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 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.
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: options.editMode === "batch" ? "batch" : "row",
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 && (canInlineEditMode || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
3298
+ const hasBuiltInRowActions = Boolean(options.allowEdit && (rowInlineMode && showRowInlineActions || options.onEdit || options.events?.EDIT) || options.allowView && options.onView || options.allowDelete && (options.onDelete || options.events?.DELETE));
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 dataTotal = visibleFields.reduce((sum, f) => sum + getColumnWidth(f, colWidths), 0);
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 extra = Math.max(0, containerWidth - dataTotal - fixedTotal);
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 = getColumnWidth(f, colWidths);
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
- options.editMode === "batch" && inlineEdit.draftRows.size > 0 && /* @__PURE__ */ jsxs("div", {
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: [/* @__PURE__ */ jsx("span", {
3446
+ children: /* @__PURE__ */ jsx("span", {
3241
3447
  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
- })]
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
- editing ? "nb-datagrid__row--editing" : "",
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 (!editing) selectRow(row);
3766
+ if (rowInRowEdit) return;
3767
+ selectRow(row);
3571
3768
  },
3572
3769
  onDoubleClick: () => {
3573
- if (editing) return;
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 (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);
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
- if (editing && canEditFieldInline(field)) return /* @__PURE__ */ jsx("td", {
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: "nb-datagrid__edit-cell",
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
- title: getCellText(field, row, filterRemoteOptions[field.name], t("common.yes"), t("common.no")),
3663
- children: renderCell(field, row, rowIndex, columnIndex, filterRemoteOptions[field.name], t("common.yes"), t("common.no"))
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: editing ? /* @__PURE__ */ jsxs("div", {
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,