@proteinjs/db-ui 1.9.3 → 1.10.0
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/CHANGELOG.md +20 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/src/form/RecordForm.d.ts.map +1 -1
- package/dist/src/form/RecordForm.js +98 -19
- package/dist/src/form/RecordForm.js.map +1 -1
- package/dist/src/form/customizations/MigrationRecordFormCustomization.d.ts +5 -0
- package/dist/src/form/customizations/MigrationRecordFormCustomization.d.ts.map +1 -1
- package/dist/src/form/customizations/MigrationRecordFormCustomization.js +72 -0
- package/dist/src/form/customizations/MigrationRecordFormCustomization.js.map +1 -1
- package/dist/src/table/RecordTable.d.ts.map +1 -1
- package/dist/src/table/RecordTable.js +7 -1
- package/dist/src/table/RecordTable.js.map +1 -1
- package/dist/src/tableDisplayName.d.ts +9 -0
- package/dist/src/tableDisplayName.d.ts.map +1 -0
- package/dist/src/tableDisplayName.js +31 -0
- package/dist/src/tableDisplayName.js.map +1 -0
- package/dist/test/recordFormInteractions.test.d.ts +5 -0
- package/dist/test/recordFormInteractions.test.d.ts.map +1 -0
- package/dist/test/recordFormInteractions.test.js +370 -0
- package/dist/test/recordFormInteractions.test.js.map +1 -0
- package/dist/test/recordTableDelete.test.d.ts +5 -0
- package/dist/test/recordTableDelete.test.d.ts.map +1 -0
- package/dist/test/recordTableDelete.test.js +254 -0
- package/dist/test/recordTableDelete.test.js.map +1 -0
- package/dist/test/tableDisplayName.test.d.ts +2 -0
- package/dist/test/tableDisplayName.test.d.ts.map +1 -0
- package/dist/test/tableDisplayName.test.js +50 -0
- package/dist/test/tableDisplayName.test.js.map +1 -0
- package/generated/index.ts +1 -1
- package/index.ts +2 -0
- package/jest.config.js +8 -1
- package/package.json +5 -4
- package/src/form/RecordForm.tsx +109 -22
- package/src/form/customizations/MigrationRecordFormCustomization.tsx +53 -1
- package/src/table/RecordTable.tsx +9 -1
- package/src/tableDisplayName.ts +28 -0
- package/test/recordFormInteractions.test.tsx +259 -0
- package/test/recordTableDelete.test.tsx +144 -0
- package/test/tableDisplayName.test.ts +40 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* RecordTable functional gaps (task #53 part 2):
|
|
5
|
+
* - item 2: bulk-delete routes through the confirmation dialog — the db delete only runs after
|
|
6
|
+
* the user confirms (the immediate bulk-delete repro).
|
|
7
|
+
* - item 6: the table titles as the plural human name ('Users'), not '<Name> Table'.
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { createRoot, Root } from 'react-dom/client';
|
|
11
|
+
import { act } from 'react-dom/test-utils';
|
|
12
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
13
|
+
import { QueryClient, QueryClientProvider } from 'react-query';
|
|
14
|
+
import { StaticTableLoader } from '@proteinjs/ui';
|
|
15
|
+
import { Record, StringColumn, Table, withRecordColumns } from '@proteinjs/db';
|
|
16
|
+
import { RecordTable } from '../src/table/RecordTable';
|
|
17
|
+
|
|
18
|
+
const mockDb = {
|
|
19
|
+
delete: jest.fn(async () => 1),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
jest.mock('@proteinjs/db', () => ({
|
|
23
|
+
...jest.requireActual('@proteinjs/db'),
|
|
24
|
+
getDb: () => mockDb,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
declare global {
|
|
28
|
+
// eslint-disable-next-line no-var
|
|
29
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
30
|
+
}
|
|
31
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
32
|
+
|
|
33
|
+
// jsdom has no IntersectionObserver (the table's infinite-scroll sentinel); a quiet stub is
|
|
34
|
+
// enough — these tests never page.
|
|
35
|
+
class StubIntersectionObserver {
|
|
36
|
+
observe() {}
|
|
37
|
+
unobserve() {}
|
|
38
|
+
disconnect() {}
|
|
39
|
+
}
|
|
40
|
+
(globalThis as any).IntersectionObserver = StubIntersectionObserver;
|
|
41
|
+
|
|
42
|
+
interface User extends Record {
|
|
43
|
+
email: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class UserTable extends Table<User> {
|
|
47
|
+
public name = 'user';
|
|
48
|
+
public columns = withRecordColumns<User>({
|
|
49
|
+
email: new StringColumn('email'),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rows: User[] = [{ id: 'u-1', email: 'a@n3xa.io' } as User, { id: 'u-2', email: 'b@n3xa.io' } as User];
|
|
54
|
+
|
|
55
|
+
describe('RecordTable', () => {
|
|
56
|
+
let container: HTMLDivElement;
|
|
57
|
+
let root: Root;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
jest.clearAllMocks();
|
|
61
|
+
container = document.createElement('div');
|
|
62
|
+
document.body.appendChild(container);
|
|
63
|
+
root = createRoot(container);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
act(() => {
|
|
68
|
+
root.unmount();
|
|
69
|
+
});
|
|
70
|
+
container.remove();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const mount = async () => {
|
|
74
|
+
const client = new QueryClient({ defaultOptions: { queries: { retry: false, cacheTime: 0 } } });
|
|
75
|
+
await act(async () => {
|
|
76
|
+
root.render(
|
|
77
|
+
<QueryClientProvider client={client}>
|
|
78
|
+
<MemoryRouter>
|
|
79
|
+
<RecordTable
|
|
80
|
+
table={new UserTable()}
|
|
81
|
+
tableLoader={new StaticTableLoader(rows, undefined as any)}
|
|
82
|
+
columns={['email']}
|
|
83
|
+
/>
|
|
84
|
+
</MemoryRouter>
|
|
85
|
+
</QueryClientProvider>
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
// Let react-query resolve the first page
|
|
89
|
+
for (let i = 0; i < 5 && !document.body.textContent?.includes('a@n3xa.io'); i++) {
|
|
90
|
+
await act(async () => {
|
|
91
|
+
await Promise.resolve();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
expect(document.body.textContent).toContain('a@n3xa.io');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const click = async (element: Element) => {
|
|
98
|
+
await act(async () => {
|
|
99
|
+
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const dialog = () => document.querySelector('[role="dialog"]');
|
|
104
|
+
|
|
105
|
+
it('titles as the plural human name, not "<Name> Table" (item 6)', async () => {
|
|
106
|
+
await mount();
|
|
107
|
+
|
|
108
|
+
expect(document.body.textContent).toContain('Users');
|
|
109
|
+
expect(document.body.textContent).not.toContain('User Table');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('bulk-delete acts only after the dialog confirms (the immediate bulk-delete repro, item 2)', async () => {
|
|
113
|
+
await mount();
|
|
114
|
+
|
|
115
|
+
const rowCheckbox = document.querySelector('input[aria-label="Select row"]') as HTMLInputElement;
|
|
116
|
+
await click(rowCheckbox);
|
|
117
|
+
|
|
118
|
+
const deleteButton = document.querySelector('button[aria-label="Delete selected rows"]');
|
|
119
|
+
expect(deleteButton).not.toBeNull();
|
|
120
|
+
await click(deleteButton!);
|
|
121
|
+
|
|
122
|
+
expect(mockDb.delete).not.toHaveBeenCalled();
|
|
123
|
+
expect(dialog()).not.toBeNull();
|
|
124
|
+
expect(dialog()!.textContent).toContain('Delete 1 row?');
|
|
125
|
+
expect(dialog()!.textContent).toContain('Users');
|
|
126
|
+
|
|
127
|
+
const confirm = Array.from(dialog()!.querySelectorAll('button')).find((b) => b.textContent === 'Delete')!;
|
|
128
|
+
await click(confirm);
|
|
129
|
+
|
|
130
|
+
expect(mockDb.delete).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('cancel leaves the rows alone', async () => {
|
|
134
|
+
await mount();
|
|
135
|
+
|
|
136
|
+
await click(document.querySelector('input[aria-label="Select row"]')!);
|
|
137
|
+
await click(document.querySelector('button[aria-label="Delete selected rows"]')!);
|
|
138
|
+
const cancel = Array.from(dialog()!.querySelectorAll('button')).find((b) => b.textContent === 'Cancel')!;
|
|
139
|
+
await click(cancel);
|
|
140
|
+
|
|
141
|
+
expect(mockDb.delete).not.toHaveBeenCalled();
|
|
142
|
+
expect(dialog()).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Record, StringColumn, Table, withRecordColumns } from '@proteinjs/db';
|
|
2
|
+
import { tableDisplayName } from '../src/tableDisplayName';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Admin table titles (task #53 part 2, item 6): 'user' titles as 'Users', not 'User Table'.
|
|
6
|
+
* Table carries no display metadata, so the title derives from the table name: humanize, then
|
|
7
|
+
* pluralize the last word with boring s/es/ies rules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Doc extends Record {
|
|
11
|
+
title: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stubTable(name: string): Table<Doc> {
|
|
15
|
+
return new (class extends Table<Doc> {
|
|
16
|
+
public name = name;
|
|
17
|
+
public columns = withRecordColumns<Doc>({ title: new StringColumn('title') });
|
|
18
|
+
})();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('tableDisplayName', () => {
|
|
22
|
+
it('pluralizes single-word table names', () => {
|
|
23
|
+
expect(tableDisplayName(stubTable('user'))).toBe('Users');
|
|
24
|
+
expect(tableDisplayName(stubTable('session'))).toBe('Sessions');
|
|
25
|
+
expect(tableDisplayName(stubTable('migration'))).toBe('Migrations');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('humanizes snake_case and pluralizes only the last word', () => {
|
|
29
|
+
expect(tableDisplayName(stubTable('access_grant'))).toBe('Access grants');
|
|
30
|
+
expect(tableDisplayName(stubTable('cached_thought'))).toBe('Cached thoughts');
|
|
31
|
+
expect(tableDisplayName(stubTable('usage_event'))).toBe('Usage events');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('applies es/ies rules', () => {
|
|
35
|
+
expect(tableDisplayName(stubTable('mailbox'))).toBe('Mailboxes');
|
|
36
|
+
expect(tableDisplayName(stubTable('address'))).toBe('Addresses');
|
|
37
|
+
expect(tableDisplayName(stubTable('category'))).toBe('Categories');
|
|
38
|
+
expect(tableDisplayName(stubTable('day'))).toBe('Days');
|
|
39
|
+
});
|
|
40
|
+
});
|