@open-mercato/ui 0.10.1-develop.7699.1.5236fabda0 → 0.10.1-develop.7702.1.f991ecf392
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7702.1.f991ecf392",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.10.1-develop.7702.1.f991ecf392",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.10.1-develop.7702.1.f991ecf392",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.1",
|
|
168
168
|
"@testing-library/react": "^16.3.3",
|
|
@@ -3593,17 +3593,17 @@ function DataTableImpl<T extends RowData>({
|
|
|
3593
3593
|
const tableScrollWrapperClassName = embedded ? 'isolate' : 'isolate overflow-auto'
|
|
3594
3594
|
|
|
3595
3595
|
const virtualScrollRef = React.useRef<HTMLDivElement>(null)
|
|
3596
|
-
// Measure the horizontal scroll viewport so the empty
|
|
3596
|
+
// Measure the horizontal scroll viewport so the empty and error states can center within
|
|
3597
3597
|
// the visible area instead of within the (often wider, overflowing) table.
|
|
3598
3598
|
const [tableScrollEl, setTableScrollEl] = React.useState<HTMLDivElement | null>(null)
|
|
3599
|
-
const [
|
|
3599
|
+
const [scrollViewportWidth, setScrollViewportWidth] = React.useState<number | null>(null)
|
|
3600
3600
|
const setTableScrollWrapperRef = React.useCallback((node: HTMLDivElement | null) => {
|
|
3601
3601
|
setTableScrollEl(node)
|
|
3602
3602
|
virtualScrollRef.current = node
|
|
3603
3603
|
}, [])
|
|
3604
3604
|
React.useEffect(() => {
|
|
3605
3605
|
if (!tableScrollEl || typeof ResizeObserver === 'undefined') return
|
|
3606
|
-
const update = () =>
|
|
3606
|
+
const update = () => setScrollViewportWidth(tableScrollEl.clientWidth)
|
|
3607
3607
|
update()
|
|
3608
3608
|
const observer = new ResizeObserver(update)
|
|
3609
3609
|
observer.observe(tableScrollEl)
|
|
@@ -3841,8 +3841,13 @@ function DataTableImpl<T extends RowData>({
|
|
|
3841
3841
|
</TableRow>
|
|
3842
3842
|
) : error ? (
|
|
3843
3843
|
<TableRow>
|
|
3844
|
-
<TableCell colSpan={mergedColumns.length + (rowActions || injectedRowActions.length > 0 ? 1 : 0) + (hasInjectedBulkActions ? 1 : 0)} className="
|
|
3845
|
-
|
|
3844
|
+
<TableCell colSpan={mergedColumns.length + (rowActions || injectedRowActions.length > 0 ? 1 : 0) + (hasInjectedBulkActions ? 1 : 0)} className="p-0">
|
|
3845
|
+
<div
|
|
3846
|
+
className={cn('sticky left-0 flex min-h-24 items-center justify-center px-4 text-center text-destructive', scrollViewportWidth ? '' : 'w-fit')}
|
|
3847
|
+
style={scrollViewportWidth ? { width: scrollViewportWidth } : undefined}
|
|
3848
|
+
>
|
|
3849
|
+
{error}
|
|
3850
|
+
</div>
|
|
3846
3851
|
</TableCell>
|
|
3847
3852
|
</TableRow>
|
|
3848
3853
|
) : allRows.length ? (
|
|
@@ -4018,8 +4023,8 @@ function DataTableImpl<T extends RowData>({
|
|
|
4018
4023
|
<TableRow>
|
|
4019
4024
|
<TableCell colSpan={mergedColumns.length + (rowActions || injectedRowActions.length > 0 ? 1 : 0) + (hasInjectedBulkActions ? 1 : 0)} className="p-0">
|
|
4020
4025
|
<div
|
|
4021
|
-
className={cn('sticky left-0 flex justify-center py-6',
|
|
4022
|
-
style={
|
|
4026
|
+
className={cn('sticky left-0 flex justify-center py-6', scrollViewportWidth ? '' : 'w-fit')}
|
|
4027
|
+
style={scrollViewportWidth ? { width: scrollViewportWidth } : undefined}
|
|
4023
4028
|
>
|
|
4024
4029
|
{filterAwareEmptyState?.active ? (
|
|
4025
4030
|
<FilteredEmptyResults
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { DataTable } from '../DataTable'
|
|
4
|
+
import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
|
|
5
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
6
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
8
|
+
import { Button } from '../../primitives/button'
|
|
9
|
+
|
|
10
|
+
jest.mock('next/navigation', () => ({
|
|
11
|
+
useRouter: () => ({ push: jest.fn(), replace: jest.fn(), prefetch: jest.fn() }),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
15
|
+
useInjectionDataWidgets: () => ({ widgets: [], isLoading: false }),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
type Row = { id: string; code: string; region: string; owner: string }
|
|
19
|
+
|
|
20
|
+
const columns: ColumnDef<Row>[] = [
|
|
21
|
+
{ accessorKey: 'code', header: 'Code' },
|
|
22
|
+
{ accessorKey: 'region', header: 'Region' },
|
|
23
|
+
{ accessorKey: 'owner', header: 'Owner' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const tokensOf = (el: Element | null): string[] =>
|
|
27
|
+
(el?.getAttribute('class') ?? '').split(/\s+/).filter(Boolean)
|
|
28
|
+
|
|
29
|
+
type TableProps = {
|
|
30
|
+
data?: Row[]
|
|
31
|
+
error?: React.ReactNode | string | null
|
|
32
|
+
isLoading?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderTable({ data = [], error = null, isLoading = false }: TableProps) {
|
|
36
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 0 } } })
|
|
37
|
+
const result = render(
|
|
38
|
+
<QueryClientProvider client={queryClient}>
|
|
39
|
+
<I18nProvider locale="en" dict={{}}>
|
|
40
|
+
<DataTable columns={columns} data={data} error={error} isLoading={isLoading} />
|
|
41
|
+
</I18nProvider>
|
|
42
|
+
</QueryClientProvider>,
|
|
43
|
+
)
|
|
44
|
+
return { ...result, queryClient }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A table wider than its scroll viewport used to centre the error message on the
|
|
48
|
+
// whole table, pushing it towards (or past) the right edge of the visible area.
|
|
49
|
+
// The error must sit in the same sticky, viewport-wide box as the empty state.
|
|
50
|
+
describe('DataTable error state centres within the visible area', () => {
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
jest.restoreAllMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders the error inside a sticky, left-pinned box with destructive text', () => {
|
|
56
|
+
const { queryClient } = renderTable({ error: 'Could not load records' })
|
|
57
|
+
try {
|
|
58
|
+
const box = screen.getByText('Could not load records')
|
|
59
|
+
const tokens = tokensOf(box)
|
|
60
|
+
expect(tokens).toEqual(
|
|
61
|
+
expect.arrayContaining(['sticky', 'left-0', 'flex', 'items-center', 'justify-center', 'text-destructive']),
|
|
62
|
+
)
|
|
63
|
+
expect(tokensOf(box.closest('td'))).toContain('p-0')
|
|
64
|
+
// A block box with a fixed height would clip a wrapped message; on the cell it was a minimum.
|
|
65
|
+
expect(tokens).toContain('min-h-24')
|
|
66
|
+
expect(tokens).not.toContain('h-24')
|
|
67
|
+
} finally {
|
|
68
|
+
queryClient.clear()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('sizes the error box to the measured scroll viewport width', () => {
|
|
73
|
+
jest.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(640)
|
|
74
|
+
const { queryClient } = renderTable({ error: 'Could not load records' })
|
|
75
|
+
try {
|
|
76
|
+
const box = screen.getByText('Could not load records')
|
|
77
|
+
expect(box.style.width).toBe('640px')
|
|
78
|
+
expect(tokensOf(box)).not.toContain('w-fit')
|
|
79
|
+
} finally {
|
|
80
|
+
queryClient.clear()
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('falls back to w-fit when the viewport width is not measurable', () => {
|
|
85
|
+
jest.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(0)
|
|
86
|
+
const { queryClient } = renderTable({ error: 'Could not load records' })
|
|
87
|
+
try {
|
|
88
|
+
const box = screen.getByText('Could not load records')
|
|
89
|
+
expect(box.style.width).toBe('')
|
|
90
|
+
expect(tokensOf(box)).toContain('w-fit')
|
|
91
|
+
} finally {
|
|
92
|
+
queryClient.clear()
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('renders a node error with a working action button', () => {
|
|
97
|
+
const onRetry = jest.fn()
|
|
98
|
+
const { queryClient } = renderTable({
|
|
99
|
+
error: (
|
|
100
|
+
<span>
|
|
101
|
+
Could not load records.{' '}
|
|
102
|
+
<Button type="button" variant="outline" onClick={onRetry}>
|
|
103
|
+
Try again
|
|
104
|
+
</Button>
|
|
105
|
+
</span>
|
|
106
|
+
),
|
|
107
|
+
})
|
|
108
|
+
try {
|
|
109
|
+
expect(screen.getByText(/Could not load records\./)).toBeInTheDocument()
|
|
110
|
+
fireEvent.click(screen.getByRole('button', { name: 'Try again' }))
|
|
111
|
+
expect(onRetry).toHaveBeenCalledTimes(1)
|
|
112
|
+
} finally {
|
|
113
|
+
queryClient.clear()
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('shows the loading state instead of the error while loading', () => {
|
|
118
|
+
const { queryClient } = renderTable({ error: 'Could not load records', isLoading: true })
|
|
119
|
+
try {
|
|
120
|
+
expect(screen.queryByText('Could not load records')).not.toBeInTheDocument()
|
|
121
|
+
expect(screen.getByText('Loading data...')).toBeInTheDocument()
|
|
122
|
+
} finally {
|
|
123
|
+
queryClient.clear()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('renders rows when there is no error', () => {
|
|
128
|
+
const { queryClient } = renderTable({
|
|
129
|
+
data: [{ id: '1', code: 'AX-100', region: 'North', owner: 'Team Blue' }],
|
|
130
|
+
})
|
|
131
|
+
try {
|
|
132
|
+
expect(screen.getByText('AX-100')).toBeInTheDocument()
|
|
133
|
+
expect(screen.queryByText('Could not load records')).not.toBeInTheDocument()
|
|
134
|
+
} finally {
|
|
135
|
+
queryClient.clear()
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
})
|