@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.7699.1.5236fabda0",
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.7699.1.5236fabda0",
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.7699.1.5236fabda0",
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 state can center within
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 [emptyStateViewportWidth, setEmptyStateViewportWidth] = React.useState<number | null>(null)
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 = () => setEmptyStateViewportWidth(tableScrollEl.clientWidth)
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="h-24 text-center text-destructive">
3845
- {error}
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', emptyStateViewportWidth ? '' : 'w-fit')}
4022
- style={emptyStateViewportWidth ? { width: emptyStateViewportWidth } : undefined}
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
+ })