@contractspec/example.crm-pipeline 3.7.7 → 3.7.12
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/.turbo/turbo-build.log +45 -42
- package/CHANGELOG.md +72 -0
- package/README.md +2 -1
- package/dist/browser/docs/crm-pipeline.docblock.js +1 -1
- package/dist/browser/docs/index.js +1 -1
- package/dist/browser/handlers/crm.handlers.js +13 -2
- package/dist/browser/handlers/index.js +13 -2
- package/dist/browser/index.js +392 -159
- package/dist/browser/ui/CrmDashboard.js +366 -144
- package/dist/browser/ui/hooks/index.js +19 -8
- package/dist/browser/ui/hooks/useDealList.js +19 -8
- package/dist/browser/ui/index.js +391 -158
- package/dist/browser/ui/renderers/index.js +32 -10
- package/dist/browser/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/browser/ui/renderers/pipeline.renderer.js +19 -8
- package/dist/browser/ui/tables/DealListTab.js +390 -0
- package/dist/docs/crm-pipeline.docblock.js +1 -1
- package/dist/docs/index.js +1 -1
- package/dist/handlers/crm.handlers.d.ts +2 -0
- package/dist/handlers/crm.handlers.js +13 -2
- package/dist/handlers/index.js +13 -2
- package/dist/index.js +392 -159
- package/dist/node/docs/crm-pipeline.docblock.js +1 -1
- package/dist/node/docs/index.js +1 -1
- package/dist/node/handlers/crm.handlers.js +13 -2
- package/dist/node/handlers/index.js +13 -2
- package/dist/node/index.js +392 -159
- package/dist/node/ui/CrmDashboard.js +366 -144
- package/dist/node/ui/hooks/index.js +19 -8
- package/dist/node/ui/hooks/useDealList.js +19 -8
- package/dist/node/ui/index.js +391 -158
- package/dist/node/ui/renderers/index.js +32 -10
- package/dist/node/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/node/ui/renderers/pipeline.renderer.js +19 -8
- package/dist/node/ui/tables/DealListTab.js +390 -0
- package/dist/ui/CrmDashboard.js +366 -144
- package/dist/ui/hooks/index.js +19 -8
- package/dist/ui/hooks/useDealList.d.ts +8 -2
- package/dist/ui/hooks/useDealList.js +19 -8
- package/dist/ui/index.js +391 -158
- package/dist/ui/renderers/index.js +32 -10
- package/dist/ui/renderers/pipeline.markdown.d.ts +1 -1
- package/dist/ui/renderers/pipeline.markdown.js +13 -2
- package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
- package/dist/ui/renderers/pipeline.renderer.js +19 -8
- package/dist/ui/tables/DealListTab.d.ts +20 -0
- package/dist/ui/tables/DealListTab.js +391 -0
- package/dist/ui/tables/DealListTab.smoke.test.d.ts +1 -0
- package/package.json +29 -13
- package/src/docs/crm-pipeline.docblock.ts +1 -1
- package/src/handlers/crm.handlers.ts +18 -1
- package/src/ui/CrmDashboard.tsx +2 -71
- package/src/ui/hooks/useDealList.ts +36 -8
- package/src/ui/renderers/pipeline.markdown.ts +1 -1
- package/src/ui/renderers/pipeline.renderer.tsx +1 -1
- package/src/ui/tables/DealListTab.smoke.test.tsx +149 -0
- package/src/ui/tables/DealListTab.tsx +276 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/example.crm-pipeline",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.12",
|
|
4
4
|
"description": "CRM Pipeline - Contacts, Companies, Deals, Tasks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -312,6 +312,13 @@
|
|
|
312
312
|
"bun": "./dist/ui/renderers/pipeline.renderer.js",
|
|
313
313
|
"node": "./dist/node/ui/renderers/pipeline.renderer.js",
|
|
314
314
|
"default": "./dist/ui/renderers/pipeline.renderer.js"
|
|
315
|
+
},
|
|
316
|
+
"./ui/tables/DealListTab": {
|
|
317
|
+
"types": "./dist/ui/tables/DealListTab.d.ts",
|
|
318
|
+
"browser": "./dist/browser/ui/tables/DealListTab.js",
|
|
319
|
+
"bun": "./dist/ui/tables/DealListTab.js",
|
|
320
|
+
"node": "./dist/node/ui/tables/DealListTab.js",
|
|
321
|
+
"default": "./dist/ui/tables/DealListTab.js"
|
|
315
322
|
}
|
|
316
323
|
},
|
|
317
324
|
"scripts": {
|
|
@@ -331,24 +338,26 @@
|
|
|
331
338
|
"typecheck": "tsc --noEmit"
|
|
332
339
|
},
|
|
333
340
|
"dependencies": {
|
|
334
|
-
"@contractspec/lib.contracts-spec": "
|
|
335
|
-
"@contractspec/lib.design-system": "3.8.
|
|
336
|
-
"@contractspec/lib.example-shared-ui": "6.0.
|
|
337
|
-
"@contractspec/lib.identity-rbac": "3.7.
|
|
338
|
-
"@contractspec/lib.runtime-sandbox": "2.7.
|
|
339
|
-
"@contractspec/lib.schema": "3.7.
|
|
340
|
-
"@contractspec/lib.ui-kit-web": "3.
|
|
341
|
-
"@contractspec/module.audit-trail": "3.7.
|
|
342
|
-
"@contractspec/module.notifications": "3.7.
|
|
341
|
+
"@contractspec/lib.contracts-spec": "5.0.0",
|
|
342
|
+
"@contractspec/lib.design-system": "3.8.5",
|
|
343
|
+
"@contractspec/lib.example-shared-ui": "6.0.12",
|
|
344
|
+
"@contractspec/lib.identity-rbac": "3.7.12",
|
|
345
|
+
"@contractspec/lib.runtime-sandbox": "2.7.10",
|
|
346
|
+
"@contractspec/lib.schema": "3.7.10",
|
|
347
|
+
"@contractspec/lib.ui-kit-web": "3.9.4",
|
|
348
|
+
"@contractspec/module.audit-trail": "3.7.12",
|
|
349
|
+
"@contractspec/module.notifications": "3.7.12",
|
|
343
350
|
"react": "19.2.0",
|
|
344
|
-
"react-dom": "19.2.0"
|
|
351
|
+
"react-dom": "19.2.0",
|
|
352
|
+
"@contractspec/lib.presentation-runtime-core": "3.9.0"
|
|
345
353
|
},
|
|
346
354
|
"devDependencies": {
|
|
347
|
-
"@contractspec/tool.typescript": "3.7.
|
|
355
|
+
"@contractspec/tool.typescript": "3.7.9",
|
|
348
356
|
"typescript": "^5.9.3",
|
|
349
357
|
"@types/react": "^19.2.14",
|
|
350
358
|
"@types/react-dom": "^19.2.2",
|
|
351
|
-
"@contractspec/tool.bun": "3.7.
|
|
359
|
+
"@contractspec/tool.bun": "3.7.9",
|
|
360
|
+
"happy-dom": "^20.8.4"
|
|
352
361
|
},
|
|
353
362
|
"publishConfig": {
|
|
354
363
|
"exports": {
|
|
@@ -659,6 +668,13 @@
|
|
|
659
668
|
"bun": "./dist/ui/renderers/pipeline.renderer.js",
|
|
660
669
|
"node": "./dist/node/ui/renderers/pipeline.renderer.js",
|
|
661
670
|
"default": "./dist/ui/renderers/pipeline.renderer.js"
|
|
671
|
+
},
|
|
672
|
+
"./ui/tables/DealListTab": {
|
|
673
|
+
"types": "./dist/ui/tables/DealListTab.d.ts",
|
|
674
|
+
"browser": "./dist/browser/ui/tables/DealListTab.js",
|
|
675
|
+
"bun": "./dist/ui/tables/DealListTab.js",
|
|
676
|
+
"node": "./dist/node/ui/tables/DealListTab.js",
|
|
677
|
+
"default": "./dist/ui/tables/DealListTab.js"
|
|
662
678
|
}
|
|
663
679
|
},
|
|
664
680
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -84,7 +84,7 @@ const crmPipelineDocBlocks: DocBlock[] = [
|
|
|
84
84
|
- deal.created, stage.moved, task.completed, contact.updated.
|
|
85
85
|
|
|
86
86
|
## Presentations
|
|
87
|
-
- Pipelines/kanban, deal detail, contact/company profiles, task lists.
|
|
87
|
+
- Pipelines/kanban, deal detail, contact/company profiles, task lists, and a server-mode shared table for the deal list.
|
|
88
88
|
|
|
89
89
|
## Notes
|
|
90
90
|
- Stage definitions should be declarative; enforce via spec and regeneration.
|
|
@@ -76,6 +76,8 @@ export interface ListDealsInput {
|
|
|
76
76
|
search?: string;
|
|
77
77
|
limit?: number;
|
|
78
78
|
offset?: number;
|
|
79
|
+
sortBy?: 'name' | 'value' | 'status' | 'expectedCloseDate' | 'updatedAt';
|
|
80
|
+
sortDirection?: 'asc' | 'desc';
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export interface ListDealsOutput {
|
|
@@ -142,6 +144,17 @@ function rowToDeal(row: DealRow): Deal {
|
|
|
142
144
|
|
|
143
145
|
// ============ Handler Factory ============
|
|
144
146
|
|
|
147
|
+
const DEAL_SORT_COLUMNS: Record<
|
|
148
|
+
NonNullable<ListDealsInput['sortBy']>,
|
|
149
|
+
string
|
|
150
|
+
> = {
|
|
151
|
+
name: 'name',
|
|
152
|
+
value: 'value',
|
|
153
|
+
status: 'status',
|
|
154
|
+
expectedCloseDate: 'expectedCloseDate',
|
|
155
|
+
updatedAt: 'updatedAt',
|
|
156
|
+
};
|
|
157
|
+
|
|
145
158
|
export function createCrmHandlers(db: DatabasePort) {
|
|
146
159
|
/**
|
|
147
160
|
* List deals with filtering
|
|
@@ -156,6 +169,8 @@ export function createCrmHandlers(db: DatabasePort) {
|
|
|
156
169
|
search,
|
|
157
170
|
limit = 20,
|
|
158
171
|
offset = 0,
|
|
172
|
+
sortBy = 'value',
|
|
173
|
+
sortDirection = 'desc',
|
|
159
174
|
} = input;
|
|
160
175
|
|
|
161
176
|
let whereClause = 'WHERE projectId = ?';
|
|
@@ -205,9 +220,11 @@ export function createCrmHandlers(db: DatabasePort) {
|
|
|
205
220
|
const totalValue = (valueResult[0]?.total as number) ?? 0;
|
|
206
221
|
|
|
207
222
|
// Get paginated deals
|
|
223
|
+
const orderByColumn = DEAL_SORT_COLUMNS[sortBy] ?? DEAL_SORT_COLUMNS.value;
|
|
224
|
+
const orderByDirection = sortDirection === 'asc' ? 'ASC' : 'DESC';
|
|
208
225
|
const dealRows = (
|
|
209
226
|
await db.query(
|
|
210
|
-
`SELECT * FROM crm_deal ${whereClause} ORDER BY
|
|
227
|
+
`SELECT * FROM crm_deal ${whereClause} ORDER BY ${orderByColumn} ${orderByDirection} LIMIT ? OFFSET ?`,
|
|
211
228
|
[...params, limit, offset]
|
|
212
229
|
)
|
|
213
230
|
).rows as unknown as DealRow[];
|
package/src/ui/CrmDashboard.tsx
CHANGED
|
@@ -31,6 +31,7 @@ import { type Deal, useDealList } from './hooks/useDealList';
|
|
|
31
31
|
import { useDealMutations } from './hooks/useDealMutations';
|
|
32
32
|
import { CreateDealModal } from './modals/CreateDealModal';
|
|
33
33
|
import { DealActionsModal } from './modals/DealActionsModal';
|
|
34
|
+
import { DealListTab } from './tables/DealListTab';
|
|
34
35
|
|
|
35
36
|
// type Tab = 'pipeline' | 'list' | 'metrics';
|
|
36
37
|
|
|
@@ -155,7 +156,7 @@ export function CrmDashboard() {
|
|
|
155
156
|
</TabsContent>
|
|
156
157
|
|
|
157
158
|
<TabsContent value="list" className="min-h-[400px]">
|
|
158
|
-
<DealListTab
|
|
159
|
+
<DealListTab onDealClick={handleDealClick} />
|
|
159
160
|
</TabsContent>
|
|
160
161
|
|
|
161
162
|
<TabsContent value="metrics" className="min-h-[400px]">
|
|
@@ -199,76 +200,6 @@ export function CrmDashboard() {
|
|
|
199
200
|
);
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
interface DealListTabProps {
|
|
203
|
-
data: ReturnType<typeof useDealList>['data'];
|
|
204
|
-
onDealClick?: (dealId: string) => void;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function DealListTab({ data, onDealClick }: DealListTabProps) {
|
|
208
|
-
if (!data?.deals.length) {
|
|
209
|
-
return (
|
|
210
|
-
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
|
211
|
-
No deals found
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return (
|
|
217
|
-
<div className="rounded-lg border border-border">
|
|
218
|
-
<table className="w-full">
|
|
219
|
-
<thead className="border-border border-b bg-muted/30">
|
|
220
|
-
<tr>
|
|
221
|
-
<th className="px-4 py-3 text-left font-medium text-sm">Deal</th>
|
|
222
|
-
<th className="px-4 py-3 text-left font-medium text-sm">Value</th>
|
|
223
|
-
<th className="px-4 py-3 text-left font-medium text-sm">Status</th>
|
|
224
|
-
<th className="px-4 py-3 text-left font-medium text-sm">
|
|
225
|
-
Expected Close
|
|
226
|
-
</th>
|
|
227
|
-
<th className="px-4 py-3 text-left font-medium text-sm">Actions</th>
|
|
228
|
-
</tr>
|
|
229
|
-
</thead>
|
|
230
|
-
<tbody className="divide-y divide-border">
|
|
231
|
-
{data.deals.map((deal: Deal) => (
|
|
232
|
-
<tr key={deal.id} className="hover:bg-muted/50">
|
|
233
|
-
<td className="px-4 py-3">
|
|
234
|
-
<div className="font-medium">{deal.name}</div>
|
|
235
|
-
</td>
|
|
236
|
-
<td className="px-4 py-3 font-mono">
|
|
237
|
-
{formatCurrency(deal.value, deal.currency)}
|
|
238
|
-
</td>
|
|
239
|
-
<td className="px-4 py-3">
|
|
240
|
-
<span
|
|
241
|
-
className={`inline-flex rounded-full px-2 py-0.5 font-medium text-xs ${
|
|
242
|
-
deal.status === 'WON'
|
|
243
|
-
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
244
|
-
: deal.status === 'LOST'
|
|
245
|
-
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
246
|
-
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
|
247
|
-
}`}
|
|
248
|
-
>
|
|
249
|
-
{deal.status}
|
|
250
|
-
</span>
|
|
251
|
-
</td>
|
|
252
|
-
<td className="px-4 py-3 text-muted-foreground">
|
|
253
|
-
{deal.expectedCloseDate?.toLocaleDateString() ?? '-'}
|
|
254
|
-
</td>
|
|
255
|
-
<td className="px-4 py-3">
|
|
256
|
-
<Button
|
|
257
|
-
variant="ghost"
|
|
258
|
-
size="sm"
|
|
259
|
-
onPress={() => onDealClick?.(deal.id)}
|
|
260
|
-
>
|
|
261
|
-
Actions
|
|
262
|
-
</Button>
|
|
263
|
-
</td>
|
|
264
|
-
</tr>
|
|
265
|
-
))}
|
|
266
|
-
</tbody>
|
|
267
|
-
</table>
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
203
|
function MetricsTab({
|
|
273
204
|
stats,
|
|
274
205
|
}: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
4
|
+
import type { ContractTableSort } from '@contractspec/lib.presentation-runtime-core';
|
|
4
5
|
/**
|
|
5
6
|
* Hook for fetching and managing deal list data
|
|
6
7
|
*
|
|
@@ -24,6 +25,9 @@ export interface UseDealListOptions {
|
|
|
24
25
|
status?: 'OPEN' | 'WON' | 'LOST' | 'all';
|
|
25
26
|
search?: string;
|
|
26
27
|
limit?: number;
|
|
28
|
+
pageIndex?: number;
|
|
29
|
+
pageSize?: number;
|
|
30
|
+
sorting?: ContractTableSort[];
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export function useDealList(options: UseDealListOptions = {}) {
|
|
@@ -35,9 +39,14 @@ export function useDealList(options: UseDealListOptions = {}) {
|
|
|
35
39
|
const [stages, setStages] = useState<Stage[]>([]);
|
|
36
40
|
const [loading, setLoading] = useState(true);
|
|
37
41
|
const [error, setError] = useState<Error | null>(null);
|
|
38
|
-
const [
|
|
42
|
+
const [internalPage, setInternalPage] = useState(0);
|
|
39
43
|
|
|
40
44
|
const pipelineId = options.pipelineId ?? 'pipeline-1';
|
|
45
|
+
const pageIndex = options.pageIndex ?? internalPage;
|
|
46
|
+
const pageSize = options.pageSize ?? options.limit ?? 50;
|
|
47
|
+
const [sort] = options.sorting ?? [];
|
|
48
|
+
const sortBy = sort?.id;
|
|
49
|
+
const sortDirection = sort ? (sort.desc ? 'desc' : 'asc') : undefined;
|
|
41
50
|
|
|
42
51
|
const fetchData = useCallback(async () => {
|
|
43
52
|
setLoading(true);
|
|
@@ -51,8 +60,17 @@ export function useDealList(options: UseDealListOptions = {}) {
|
|
|
51
60
|
stageId: options.stageId,
|
|
52
61
|
status: options.status === 'all' ? undefined : options.status,
|
|
53
62
|
search: options.search,
|
|
54
|
-
limit:
|
|
55
|
-
offset:
|
|
63
|
+
limit: pageSize,
|
|
64
|
+
offset: pageIndex * pageSize,
|
|
65
|
+
sortBy:
|
|
66
|
+
sortBy === 'name' ||
|
|
67
|
+
sortBy === 'value' ||
|
|
68
|
+
sortBy === 'status' ||
|
|
69
|
+
sortBy === 'expectedCloseDate' ||
|
|
70
|
+
sortBy === 'updatedAt'
|
|
71
|
+
? sortBy
|
|
72
|
+
: undefined,
|
|
73
|
+
sortDirection,
|
|
56
74
|
}),
|
|
57
75
|
crm.getDealsByStage({ projectId, pipelineId }),
|
|
58
76
|
crm.getPipelineStages({ pipelineId }),
|
|
@@ -72,8 +90,10 @@ export function useDealList(options: UseDealListOptions = {}) {
|
|
|
72
90
|
options.stageId,
|
|
73
91
|
options.status,
|
|
74
92
|
options.search,
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
pageIndex,
|
|
94
|
+
pageSize,
|
|
95
|
+
sortBy,
|
|
96
|
+
sortDirection,
|
|
77
97
|
]);
|
|
78
98
|
|
|
79
99
|
useEffect(() => {
|
|
@@ -105,9 +125,17 @@ export function useDealList(options: UseDealListOptions = {}) {
|
|
|
105
125
|
loading,
|
|
106
126
|
error,
|
|
107
127
|
stats,
|
|
108
|
-
page,
|
|
128
|
+
page: pageIndex + 1,
|
|
129
|
+
pageIndex,
|
|
130
|
+
pageSize,
|
|
109
131
|
refetch: fetchData,
|
|
110
|
-
nextPage:
|
|
111
|
-
|
|
132
|
+
nextPage:
|
|
133
|
+
options.pageIndex === undefined
|
|
134
|
+
? () => setInternalPage((page) => page + 1)
|
|
135
|
+
: undefined,
|
|
136
|
+
prevPage:
|
|
137
|
+
options.pageIndex === undefined
|
|
138
|
+
? () => pageIndex > 0 && setInternalPage((page) => page - 1)
|
|
139
|
+
: undefined,
|
|
112
140
|
};
|
|
113
141
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Imports handlers from the hooks module to ensure correct build order.
|
|
5
5
|
*/
|
|
6
|
-
import type { PresentationRenderer } from '@contractspec/lib.
|
|
6
|
+
import type { PresentationRenderer } from '@contractspec/lib.presentation-runtime-core/transform-engine';
|
|
7
7
|
import {
|
|
8
8
|
mockGetPipelineStagesHandler,
|
|
9
9
|
mockListDealsHandler,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Data is fetched via the CrmPipelineBoard component's internal hooks.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { PresentationRenderer } from '@contractspec/lib.
|
|
8
|
+
import type { PresentationRenderer } from '@contractspec/lib.presentation-runtime-core/transform-engine';
|
|
9
9
|
import * as React from 'react';
|
|
10
10
|
import { CrmPipelineBoard } from '../CrmPipelineBoard';
|
|
11
11
|
import { useDealList } from '../hooks/useDealList';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import Window from 'happy-dom/lib/window/Window.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { act } from 'react';
|
|
5
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
6
|
+
import { MOCK_DEALS } from '../../handlers/mock-data';
|
|
7
|
+
import type { Deal } from '../hooks/useDealList';
|
|
8
|
+
import { DealListDataTable } from './DealListTab';
|
|
9
|
+
|
|
10
|
+
const TEST_DEALS: Deal[] = MOCK_DEALS.map((deal) => ({
|
|
11
|
+
...deal,
|
|
12
|
+
projectId: 'project-1',
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
const windowInstance = new Window({
|
|
17
|
+
url: 'https://sandbox.contractspec.local/sandbox?template=crm-pipeline',
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(windowInstance, 'SyntaxError', {
|
|
20
|
+
value: SyntaxError,
|
|
21
|
+
configurable: true,
|
|
22
|
+
});
|
|
23
|
+
Object.assign(globalThis, {
|
|
24
|
+
window: windowInstance,
|
|
25
|
+
document: windowInstance.document,
|
|
26
|
+
navigator: windowInstance.navigator,
|
|
27
|
+
HTMLElement: windowInstance.HTMLElement,
|
|
28
|
+
HTMLButtonElement: windowInstance.HTMLButtonElement,
|
|
29
|
+
Node: windowInstance.Node,
|
|
30
|
+
Event: windowInstance.Event,
|
|
31
|
+
MouseEvent: windowInstance.MouseEvent,
|
|
32
|
+
MutationObserver: windowInstance.MutationObserver,
|
|
33
|
+
getComputedStyle: windowInstance.getComputedStyle.bind(windowInstance),
|
|
34
|
+
requestAnimationFrame: (callback: FrameRequestCallback) =>
|
|
35
|
+
setTimeout(() => callback(Date.now()), 0),
|
|
36
|
+
cancelAnimationFrame: (id: number) => clearTimeout(id),
|
|
37
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
document.body.innerHTML = '';
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function sortDeals(
|
|
46
|
+
pageIndex: number,
|
|
47
|
+
pageSize: number,
|
|
48
|
+
sorting: { id: string; desc: boolean }[]
|
|
49
|
+
) {
|
|
50
|
+
const [sort] = sorting;
|
|
51
|
+
const sorted = [...TEST_DEALS].sort((left, right) => {
|
|
52
|
+
const leftValue =
|
|
53
|
+
sort?.id === 'deal'
|
|
54
|
+
? left.name
|
|
55
|
+
: sort?.id === 'status'
|
|
56
|
+
? left.status
|
|
57
|
+
: sort?.id === 'expectedCloseDate'
|
|
58
|
+
? (left.expectedCloseDate?.toISOString() ?? '')
|
|
59
|
+
: sort?.id === 'updatedAt'
|
|
60
|
+
? left.updatedAt.toISOString()
|
|
61
|
+
: left.value;
|
|
62
|
+
const rightValue =
|
|
63
|
+
sort?.id === 'deal'
|
|
64
|
+
? right.name
|
|
65
|
+
: sort?.id === 'status'
|
|
66
|
+
? right.status
|
|
67
|
+
: sort?.id === 'expectedCloseDate'
|
|
68
|
+
? (right.expectedCloseDate?.toISOString() ?? '')
|
|
69
|
+
: sort?.id === 'updatedAt'
|
|
70
|
+
? right.updatedAt.toISOString()
|
|
71
|
+
: right.value;
|
|
72
|
+
if (leftValue === rightValue) return 0;
|
|
73
|
+
const comparison = leftValue > rightValue ? 1 : -1;
|
|
74
|
+
return sort?.desc ? comparison * -1 : comparison;
|
|
75
|
+
});
|
|
76
|
+
return sorted.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Harness() {
|
|
80
|
+
const [sorting, setSorting] = React.useState([{ id: 'value', desc: true }]);
|
|
81
|
+
const [pagination, setPagination] = React.useState({
|
|
82
|
+
pageIndex: 0,
|
|
83
|
+
pageSize: 3,
|
|
84
|
+
});
|
|
85
|
+
return (
|
|
86
|
+
<DealListDataTable
|
|
87
|
+
deals={sortDeals(pagination.pageIndex, pagination.pageSize, sorting)}
|
|
88
|
+
totalItems={TEST_DEALS.length}
|
|
89
|
+
pageIndex={pagination.pageIndex}
|
|
90
|
+
pageSize={pagination.pageSize}
|
|
91
|
+
sorting={sorting}
|
|
92
|
+
onSortingChange={setSorting}
|
|
93
|
+
onPaginationChange={setPagination}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function renderTable() {
|
|
99
|
+
const container = document.createElement('div');
|
|
100
|
+
document.body.append(container);
|
|
101
|
+
const root: Root = createRoot(container);
|
|
102
|
+
|
|
103
|
+
await act(async () => {
|
|
104
|
+
root.render(<Harness />);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return { container, root };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function click(element: Element | null | undefined) {
|
|
111
|
+
if (!element) {
|
|
112
|
+
throw new Error('Expected clickable element.');
|
|
113
|
+
}
|
|
114
|
+
await act(async () => {
|
|
115
|
+
if ('click' in element && typeof element.click === 'function') {
|
|
116
|
+
element.click();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe('DealListDataTable', () => {
|
|
124
|
+
test('renders the shared table and supports selection plus expansion', async () => {
|
|
125
|
+
const { container, root } = await renderTable();
|
|
126
|
+
|
|
127
|
+
expect(container.textContent).toContain('All Deals');
|
|
128
|
+
expect(container.textContent).toContain('6 total deals');
|
|
129
|
+
|
|
130
|
+
await click(container.querySelector('[aria-label="Select row deal-5"]'));
|
|
131
|
+
expect(container.textContent).toContain('Selected 1');
|
|
132
|
+
|
|
133
|
+
await click(container.querySelector('[aria-label="Expand row deal-5"]'));
|
|
134
|
+
expect(container.textContent).toContain('user-1');
|
|
135
|
+
|
|
136
|
+
await click(
|
|
137
|
+
[...container.getElementsByTagName('button')].find(
|
|
138
|
+
(button) => button.textContent?.trim() === '2'
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
expect(container.textContent).toContain(
|
|
142
|
+
'Affichage de 4 à 6 sur 6 résultats'
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
root.unmount();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|