Skip to content

Commit d47aaf4

Browse files
Copilothotlong
andcommitted
Add multi-cell and batch save support for inline editing
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a449851 commit d47aaf4

5 files changed

Lines changed: 490 additions & 21 deletions

File tree

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi } from 'vitest';
10+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
11+
import React from 'react';
12+
import type { DataTableSchema } from '@object-ui/types';
13+
14+
// Import the component
15+
import '../data-table';
16+
import { ComponentRegistry } from '@object-ui/core';
17+
18+
describe('Data Table - Batch Editing', () => {
19+
const mockData = [
20+
{ id: 1, name: 'John Doe', email: 'john@example.com', age: 30 },
21+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 25 },
22+
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', age: 35 },
23+
];
24+
25+
const mockColumns = [
26+
{ header: 'ID', accessorKey: 'id', editable: false },
27+
{ header: 'Name', accessorKey: 'name' },
28+
{ header: 'Email', accessorKey: 'email' },
29+
{ header: 'Age', accessorKey: 'age' },
30+
];
31+
32+
it('should track pending changes across multiple cells', async () => {
33+
const onRowSave = vi.fn();
34+
35+
const schema: DataTableSchema = {
36+
type: 'data-table',
37+
columns: mockColumns,
38+
data: mockData,
39+
editable: true,
40+
pagination: false,
41+
searchable: false,
42+
rowActions: true,
43+
onRowSave,
44+
};
45+
46+
const DataTableRenderer = ComponentRegistry.get('data-table');
47+
if (!DataTableRenderer) throw new Error('DataTableRenderer not found');
48+
49+
const { container } = render(<DataTableRenderer schema={schema} />);
50+
51+
// Edit first cell in row
52+
const nameCell = screen.getByText('John Doe').closest('td');
53+
if (nameCell) {
54+
fireEvent.doubleClick(nameCell);
55+
56+
await waitFor(() => {
57+
const input = nameCell.querySelector('input');
58+
expect(input).toBeInTheDocument();
59+
});
60+
61+
const input = nameCell.querySelector('input');
62+
if (input) {
63+
fireEvent.change(input, { target: { value: 'John Smith' } });
64+
fireEvent.keyDown(input, { key: 'Enter' });
65+
}
66+
}
67+
68+
// Wait for the edit to be saved to pending changes
69+
await waitFor(() => {
70+
const modifiedIndicator = screen.getByText(/1 row modified/i);
71+
expect(modifiedIndicator).toBeInTheDocument();
72+
});
73+
74+
// Edit second cell in same row - now the name shows as 'John Smith'
75+
const emailCell = container.querySelector('td:has-text("john@example.com")') ||
76+
Array.from(container.querySelectorAll('td')).find(el =>
77+
el.textContent?.includes('john@example.com')
78+
);
79+
80+
expect(emailCell).toBeInTheDocument();
81+
if (emailCell) {
82+
fireEvent.doubleClick(emailCell);
83+
84+
await waitFor(() => {
85+
const input = emailCell.querySelector('input');
86+
expect(input).toBeInTheDocument();
87+
});
88+
89+
const input = emailCell.querySelector('input');
90+
if (input) {
91+
fireEvent.change(input, { target: { value: 'johnsmith@example.com' } });
92+
fireEvent.keyDown(input, { key: 'Enter' });
93+
}
94+
}
95+
96+
// Row should still show as modified (still just 1 row)
97+
await waitFor(() => {
98+
const modifiedIndicator = screen.getByText(/1 row modified/i);
99+
expect(modifiedIndicator).toBeInTheDocument();
100+
});
101+
});
102+
103+
it('should save a single row with multiple changes', async () => {
104+
const onRowSave = vi.fn().mockResolvedValue(undefined);
105+
106+
const schema: DataTableSchema = {
107+
type: 'data-table',
108+
columns: mockColumns,
109+
data: mockData,
110+
editable: true,
111+
pagination: false,
112+
searchable: false,
113+
rowActions: true,
114+
onRowSave,
115+
};
116+
117+
const DataTableRenderer = ComponentRegistry.get('data-table');
118+
if (!DataTableRenderer) throw new Error('DataTableRenderer not found');
119+
120+
render(<DataTableRenderer schema={schema} />);
121+
122+
// Edit name
123+
const nameCell = screen.getByText('John Doe').closest('td');
124+
if (nameCell) {
125+
fireEvent.doubleClick(nameCell);
126+
await waitFor(() => {
127+
const input = nameCell.querySelector('input');
128+
expect(input).toBeInTheDocument();
129+
});
130+
131+
const input = nameCell.querySelector('input');
132+
if (input) {
133+
fireEvent.change(input, { target: { value: 'John Smith' } });
134+
fireEvent.keyDown(input, { key: 'Enter' });
135+
}
136+
}
137+
138+
// Find and click save button for row
139+
await waitFor(() => {
140+
const saveButtons = screen.getAllByTitle('Save row');
141+
expect(saveButtons.length).toBeGreaterThan(0);
142+
fireEvent.click(saveButtons[0]);
143+
});
144+
145+
// Verify callback was called with correct data
146+
await waitFor(() => {
147+
expect(onRowSave).toHaveBeenCalledWith(
148+
0,
149+
{ name: 'John Smith' },
150+
mockData[0]
151+
);
152+
});
153+
});
154+
155+
it('should save all modified rows with batch save', async () => {
156+
const onBatchSave = vi.fn().mockResolvedValue(undefined);
157+
158+
const schema: DataTableSchema = {
159+
type: 'data-table',
160+
columns: mockColumns,
161+
data: mockData,
162+
editable: true,
163+
pagination: false,
164+
searchable: false,
165+
onBatchSave,
166+
};
167+
168+
const DataTableRenderer = ComponentRegistry.get('data-table');
169+
if (!DataTableRenderer) throw new Error('DataTableRenderer not found');
170+
171+
render(<DataTableRenderer schema={schema} />);
172+
173+
// Edit row 1
174+
const nameCell1 = screen.getByText('John Doe').closest('td');
175+
if (nameCell1) {
176+
fireEvent.doubleClick(nameCell1);
177+
await waitFor(() => {
178+
const input = nameCell1.querySelector('input');
179+
expect(input).toBeInTheDocument();
180+
});
181+
182+
const input = nameCell1.querySelector('input');
183+
if (input) {
184+
fireEvent.change(input, { target: { value: 'John Smith' } });
185+
fireEvent.keyDown(input, { key: 'Enter' });
186+
}
187+
}
188+
189+
// Edit row 2
190+
await waitFor(() => {
191+
const nameCell2 = screen.getByText('Jane Smith').closest('td');
192+
expect(nameCell2).toBeInTheDocument();
193+
});
194+
195+
const nameCell2 = screen.getByText('Jane Smith').closest('td');
196+
if (nameCell2) {
197+
fireEvent.doubleClick(nameCell2);
198+
await waitFor(() => {
199+
const input = nameCell2.querySelector('input');
200+
expect(input).toBeInTheDocument();
201+
});
202+
203+
const input = nameCell2.querySelector('input');
204+
if (input) {
205+
fireEvent.change(input, { target: { value: 'Jane Doe' } });
206+
fireEvent.keyDown(input, { key: 'Enter' });
207+
}
208+
}
209+
210+
// Click save all button
211+
await waitFor(() => {
212+
const saveAllButton = screen.getByText(/Save All \(2\)/i);
213+
expect(saveAllButton).toBeInTheDocument();
214+
fireEvent.click(saveAllButton);
215+
});
216+
217+
// Verify callback was called
218+
await waitFor(() => {
219+
expect(onBatchSave).toHaveBeenCalledWith([
220+
{ rowIndex: 0, changes: { name: 'John Smith' }, row: mockData[0] },
221+
{ rowIndex: 1, changes: { name: 'Jane Doe' }, row: mockData[1] },
222+
]);
223+
});
224+
});
225+
226+
it('should cancel all changes', async () => {
227+
const onBatchSave = vi.fn();
228+
229+
const schema: DataTableSchema = {
230+
type: 'data-table',
231+
columns: mockColumns,
232+
data: mockData,
233+
editable: true,
234+
pagination: false,
235+
searchable: false,
236+
onBatchSave,
237+
};
238+
239+
const DataTableRenderer = ComponentRegistry.get('data-table');
240+
if (!DataTableRenderer) throw new Error('DataTableRenderer not found');
241+
242+
render(<DataTableRenderer schema={schema} />);
243+
244+
// Edit a cell
245+
const nameCell = screen.getByText('John Doe').closest('td');
246+
if (nameCell) {
247+
fireEvent.doubleClick(nameCell);
248+
await waitFor(() => {
249+
const input = nameCell.querySelector('input');
250+
expect(input).toBeInTheDocument();
251+
});
252+
253+
const input = nameCell.querySelector('input');
254+
if (input) {
255+
fireEvent.change(input, { target: { value: 'John Smith' } });
256+
fireEvent.keyDown(input, { key: 'Enter' });
257+
}
258+
}
259+
260+
// Click cancel all button
261+
await waitFor(() => {
262+
const cancelButton = screen.getByText(/Cancel All/i);
263+
expect(cancelButton).toBeInTheDocument();
264+
fireEvent.click(cancelButton);
265+
});
266+
267+
// Verify changes indicator is gone
268+
await waitFor(() => {
269+
const modifiedIndicator = screen.queryByText(/row modified/i);
270+
expect(modifiedIndicator).not.toBeInTheDocument();
271+
});
272+
273+
// Original value should be restored
274+
expect(screen.getByText('John Doe')).toBeInTheDocument();
275+
});
276+
});

0 commit comments

Comments
 (0)