@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.3

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.
@@ -1,22 +1,43 @@
1
- import { Button, Group } from "@mantine/core";
1
+ import { Button, Group, Text } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
3
  import type { ReactNode } from "react";
4
4
  import { useTMDataGridContext } from "../TMDataGridContext";
5
5
 
6
6
  /**
7
- * How many rows are waiting to be saved: dirty edits, entry rows (an add is
8
- * pending whether or not it was touched), and deletion marks.
7
+ * How many rows the draft store holds - committed edits, committed entry
8
+ * rows and deletion marks. This is what Save sends, so it is what Save
9
+ * counts: a row the user is still typing into is not in here.
9
10
  */
10
- function usePendingCount(): number {
11
+ function useDraftCount(): number {
12
+ const { edit } = useTMDataGridContext();
13
+ return useSelector(
14
+ edit.store,
15
+ (state) =>
16
+ state.committedRowIds.length +
17
+ state.newRows.filter((newRow) => newRow.committed).length +
18
+ state.deletedRowIds.length,
19
+ );
20
+ }
21
+
22
+ /**
23
+ * How many rows are still open - edited or entered but not committed, so not
24
+ * part of the save. Surfaced beside Save so an uncommitted row is visible
25
+ * rather than silently left behind.
26
+ */
27
+ function useOpenCount(): number {
11
28
  const { edit } = useTMDataGridContext();
12
29
  return useSelector(
13
30
  edit.store,
14
31
  (state) =>
15
32
  state.openRowIds.filter(
16
33
  (rowId) =>
17
- state.newRows.some((newRow) => newRow.tempId === rowId) ||
18
- (state.rows[rowId]?.dirtyFields.length ?? 0) > 0,
19
- ).length + state.deletedRowIds.length,
34
+ !state.committedRowIds.includes(rowId) &&
35
+ !state.newRows.some(
36
+ (newRow) => newRow.tempId === rowId && newRow.committed,
37
+ ) &&
38
+ (state.newRows.some((newRow) => newRow.tempId === rowId) ||
39
+ (state.rows[rowId]?.dirtyFields.length ?? 0) > 0),
40
+ ).length,
20
41
  );
21
42
  }
22
43
 
@@ -28,7 +49,7 @@ function usePendingCount(): number {
28
49
 
29
50
  function EditSaveButton() {
30
51
  const { edit, labels } = useTMDataGridContext();
31
- const pendingCount = usePendingCount();
52
+ const draftCount = useDraftCount();
32
53
  const isSubmitting = useSelector(edit.store, (state) =>
33
54
  state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
34
55
  );
@@ -36,19 +57,39 @@ function EditSaveButton() {
36
57
  return (
37
58
  <Button
38
59
  size="compact-sm"
39
- disabled={pendingCount === 0}
60
+ disabled={draftCount === 0}
40
61
  loading={isSubmitting}
41
62
  data-dg-part="save-all"
42
- onClick={() => void edit.submitAll()}
63
+ data-draft-count={draftCount}
64
+ onClick={() => void edit.saveDrafts()}
43
65
  >
44
- {labels.saveAllEdits(pendingCount)}
66
+ {labels.saveAllEdits(draftCount)}
45
67
  </Button>
46
68
  );
47
69
  }
48
70
 
71
+ /** The count of rows left open, or nothing while every row is decided. */
72
+ function EditOpenRowsNote() {
73
+ const { labels } = useTMDataGridContext();
74
+ const openCount = useOpenCount();
75
+ if (openCount === 0) return null;
76
+ return (
77
+ <Text
78
+ size="xs"
79
+ c="dimmed"
80
+ data-dg-part="open-rows-note"
81
+ data-open-count={openCount}
82
+ >
83
+ {labels.editRowsStillOpen(openCount)}
84
+ </Text>
85
+ );
86
+ }
87
+
49
88
  function EditDiscardButton() {
50
89
  const { edit, labels } = useTMDataGridContext();
51
- const pendingCount = usePendingCount();
90
+ // Discard drops everything the grid is holding, open rows included, so it
91
+ // stays live while anything is uncommitted.
92
+ const pendingCount = useDraftCount() + useOpenCount();
52
93
 
53
94
  return (
54
95
  <Button
@@ -67,11 +108,25 @@ function EditDiscardButton() {
67
108
  const EDIT_ACTIONS_CONTROLS: TMDataGridEditActionsControls = {
68
109
  Save: EditSaveButton,
69
110
  Discard: EditDiscardButton,
111
+ OpenRowsNote: EditOpenRowsNote,
70
112
  };
71
113
 
72
114
  /** What the edit chrome is showing. */
73
115
  export type TMDataGridEditActionsState = {
74
- /** Rows with unsaved work: dirty edits, entry rows, and deletion marks. */
116
+ /**
117
+ * Rows in the draft store, which is what Save sends: committed edits,
118
+ * committed entry rows and deletion marks.
119
+ */
120
+ draftCount: number;
121
+ /**
122
+ * Rows still open - edited or entered but not committed, so not part of
123
+ * the save. They stay open across a save.
124
+ */
125
+ openCount: number;
126
+ /**
127
+ * @deprecated Was "everything uncommitted", which Save no longer sends.
128
+ * Reads as `draftCount + openCount`; use whichever you meant.
129
+ */
75
130
  pendingCount: number;
76
131
  /** Whether a submit is in flight. */
77
132
  isSubmitting: boolean;
@@ -79,18 +134,22 @@ export type TMDataGridEditActionsState = {
79
134
 
80
135
  /** What the edit chrome can do. */
81
136
  export type TMDataGridEditActionsActions = {
82
- /** Commits every open row. Resolves `false` when a row stayed open. */
137
+ /** Saves the draft store. Open rows are left alone. */
83
138
  save: () => Promise<boolean>;
84
- /** Drops every draft. */
139
+ /** Submits every open row, committing the ones that validate. */
140
+ commitAll: () => Promise<boolean>;
141
+ /** Drops everything - open form state and the draft store alike. */
85
142
  discard: () => void;
86
143
  };
87
144
 
88
145
  /** The pre-bound pieces of the built-in edit chrome. */
89
146
  export type TMDataGridEditActionsControls = {
90
- /** Save, with the pending count, disabled while nothing is pending. */
147
+ /** Save, with the draft count, disabled while the draft store is empty. */
91
148
  Save: () => ReactNode;
92
149
  /** Discard, disabled while nothing is pending. */
93
150
  Discard: () => ReactNode;
151
+ /** The "N rows still being edited" note, or nothing while there are none. */
152
+ OpenRowsNote: () => ReactNode;
94
153
  };
95
154
 
96
155
  /** What {@link TMDataGridEditActionsProps.renderActions} is handed. */
@@ -108,7 +167,8 @@ export type TMDataGridEditActionsProps = {
108
167
  * <TMDataGrid.EditActions
109
168
  * renderActions={({ state, Controls }) => (
110
169
  * <Group>
111
- * {state.pendingCount > 0 && <Badge>{state.pendingCount}</Badge>}
170
+ * {state.draftCount > 0 && <Badge>{state.draftCount}</Badge>}
171
+ * <Controls.OpenRowsNote />
112
172
  * <Controls.Save />
113
173
  * <Controls.Discard />
114
174
  * </Group>
@@ -120,11 +180,11 @@ export type TMDataGridEditActionsProps = {
120
180
  };
121
181
 
122
182
  /**
123
- * Draft mode's toolbar chrome: Save with the dirty-row count, and Discard.
124
- * Both read the edit store, so they grey out while nothing is dirty and the
125
- * Save spins while a submit is in flight. Works under any `editing.mode` - a
126
- * cellConfirm grid accumulating drafts can offer the same pair - and renders
127
- * nothing while editing is off.
183
+ * Draft mode's toolbar chrome: Save with the draft-store count, Discard, and
184
+ * a note counting rows still open. Save sends the draft store and leaves open
185
+ * rows alone, so it greys out while nothing is committed however much is
186
+ * being typed - the note is what makes those rows visible. Works under any
187
+ * `editing.mode` and renders nothing while editing is off.
128
188
  *
129
189
  * ```tsx
130
190
  * <TMDataGrid.Toolbar>
@@ -138,7 +198,8 @@ export function TMDataGridEditActions({
138
198
  renderActions,
139
199
  }: TMDataGridEditActionsProps = {}) {
140
200
  const { edit, features } = useTMDataGridContext();
141
- const pendingCount = usePendingCount();
201
+ const draftCount = useDraftCount();
202
+ const openCount = useOpenCount();
142
203
  const isSubmitting = useSelector(edit.store, (state) =>
143
204
  state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
144
205
  );
@@ -147,14 +208,24 @@ export function TMDataGridEditActions({
147
208
 
148
209
  if (renderActions) {
149
210
  return renderActions({
150
- state: { pendingCount, isSubmitting },
151
- actions: { save: () => edit.submitAll(), discard: () => edit.cancelAll() },
211
+ state: {
212
+ draftCount,
213
+ openCount,
214
+ pendingCount: draftCount + openCount,
215
+ isSubmitting,
216
+ },
217
+ actions: {
218
+ save: () => edit.saveDrafts(),
219
+ commitAll: () => edit.commitAll(),
220
+ discard: () => edit.cancelAll(),
221
+ },
152
222
  Controls: EDIT_ACTIONS_CONTROLS,
153
223
  });
154
224
  }
155
225
 
156
226
  return (
157
227
  <Group gap="xs" wrap="nowrap">
228
+ <EditOpenRowsNote />
158
229
  <EditSaveButton />
159
230
  <EditDiscardButton />
160
231
  </Group>
@@ -82,7 +82,7 @@ function EditLaneCell<TData extends RowData>({
82
82
  const isConfirmedNew = useSelector(
83
83
  edit.store,
84
84
  (state) =>
85
- state.newRows.find((newRow) => newRow.tempId === rowId)?.confirmed ===
85
+ state.newRows.find((newRow) => newRow.tempId === rowId)?.committed ===
86
86
  true,
87
87
  );
88
88
  const isMarkedDeleted = useSelector(edit.store, (state) =>
@@ -67,12 +67,12 @@ export function TMDataGridEntryRows({
67
67
  }) {
68
68
  const { table, edit, features } = useTMDataGridContext();
69
69
  const newRows = useSelector(edit.store, (state) => state.newRows);
70
- // The reopen gesture: `begin` on a confirmed entry row flips it back to
70
+ // The reopen gesture: `begin` on a committed entry row flips it back to
71
71
  // editors and names the cell double-clicked - where the caret goes.
72
72
  const activeEntry = useSelector(edit.store, (state) =>
73
73
  state.active !== null &&
74
74
  state.newRows.some(
75
- (newRow) => newRow.tempId === state.active?.rowId && !newRow.confirmed,
75
+ (newRow) => newRow.tempId === state.active?.rowId && !newRow.committed,
76
76
  )
77
77
  ? state.active
78
78
  : null,
@@ -109,7 +109,7 @@ export function TMDataGridEntryRows({
109
109
  });
110
110
 
111
111
  /**
112
- * The caret for a reopen: `begin` on a confirmed row re-arms its editors
112
+ * The caret for a reopen: `begin` on a committed row re-arms its editors
113
113
  * and the double-clicked cell's editor should hold the caret, not the
114
114
  * row's first. Keyed separately from the add effect above - the row was
115
115
  * already focused once when it was added.
@@ -134,16 +134,16 @@ export function TMDataGridEntryRows({
134
134
  focusEditorContent(editor);
135
135
  });
136
136
 
137
- // Unconfirmed rows keep the seed values frozen at addRow - the live values
137
+ // Open (uncommitted) rows keep the seed values frozen at addRow - the live values
138
138
  // belong to the forms, which the editors read directly, and this table only
139
- // provides row and cell identity. A confirmed row's cells render values, so
139
+ // provides row and cell identity. A committed row's cells render values, so
140
140
  // there the draft itself is the row; the memo recomputes on every
141
- // confirm/reopen because either flips `newRows`' identity.
141
+ // commit/reopen because either flips `newRows`' identity.
142
142
  const data = useMemo(
143
143
  () =>
144
144
  newRows.map(
145
- ({ tempId, confirmed }) =>
146
- ((confirmed
145
+ ({ tempId, committed }) =>
146
+ ((committed
147
147
  ? edit.getForm(tempId)?.state.values
148
148
  : edit.getForm(tempId)?.options.defaultValues) ??
149
149
  {}) as TMDataGridRowData,
@@ -161,20 +161,20 @@ export function TMDataGridEntryRows({
161
161
  if (newRows.length === 0) return null;
162
162
 
163
163
  const entryRows = entryTable.getCoreRowModel().rows;
164
- const confirmedById = new Map(
165
- newRows.map((newRow) => [newRow.tempId, newRow.confirmed]),
164
+ const committedById = new Map(
165
+ newRows.map((newRow) => [newRow.tempId, newRow.committed]),
166
166
  );
167
167
  // A row being *typed* into is always sticky - it exists nowhere else to
168
- // scroll back to. A confirmed row joins the scrolling flow unless
168
+ // scroll back to. A committed row joins the scrolling flow unless
169
169
  // `newRowsSticky` keeps it pinned, so entering many rows cannot fill the
170
170
  // viewport with sticky chrome.
171
171
  const stickyRows = entryRows.filter(
172
172
  (entryRow) =>
173
- features.editNewRowsSticky || confirmedById.get(entryRow.id) !== true,
173
+ features.editNewRowsSticky || committedById.get(entryRow.id) !== true,
174
174
  );
175
175
  const flowRows = features.editNewRowsSticky
176
176
  ? []
177
- : entryRows.filter((entryRow) => confirmedById.get(entryRow.id) === true);
177
+ : entryRows.filter((entryRow) => committedById.get(entryRow.id) === true);
178
178
 
179
179
  const renderEntryRow = (
180
180
  entryRow: (typeof entryRows)[number],
@@ -183,7 +183,7 @@ export function TMDataGridEntryRows({
183
183
  const cellsById = new Map(
184
184
  entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
185
185
  );
186
- const confirmed = confirmedById.get(entryRow.id) === true;
186
+ const committed = committedById.get(entryRow.id) === true;
187
187
  return (
188
188
  <div
189
189
  key={entryRow.id}
@@ -191,7 +191,7 @@ export function TMDataGridEntryRows({
191
191
  data-dg-part="entry-row"
192
192
  data-row-id={entryRow.id}
193
193
  data-new
194
- data-confirmed={confirmed}
194
+ data-committed={committed}
195
195
  className={classes.entryRow}
196
196
  >
197
197
  {orderedColumns.map((column) => {
@@ -206,10 +206,10 @@ export function TMDataGridEntryRows({
206
206
  data-column-id={column.id}
207
207
  data-align={getColumnAlign(column)}
208
208
  data-control-column={isControlColumn(column.id)}
209
- // A confirmed row re-opens where it is double-clicked, the
209
+ // A committed row re-opens where it is double-clicked, the
210
210
  // same gesture a body cell answers.
211
211
  onDoubleClick={
212
- confirmed && !isControlColumn(column.id)
212
+ committed && !isControlColumn(column.id)
213
213
  ? () =>
214
214
  edit.begin({
215
215
  rowId: entryRow.id,
@@ -239,7 +239,7 @@ export function TMDataGridEntryRows({
239
239
  {cell !== undefined && column.id === EDIT_COLUMN_ID ? (
240
240
  // The lane's cell - the entry row's controls.
241
241
  flexRender(cell.column.columnDef.cell, cell.getContext())
242
- ) : confirmed && cell !== undefined ? (
242
+ ) : committed && cell !== undefined ? (
243
243
  // Entered, awaiting Save all: a value row through the
244
244
  // columns' own renderers, over the draft the memo above
245
245
  // fed this table.