@open-mercato/ui 0.6.4-develop.3944.1.4100aa7fbe → 0.6.4-develop.3962.1.70f30e284c

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.
Files changed (51) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/ai/AiChat.js +4 -2
  3. package/dist/ai/AiChat.js.map +2 -2
  4. package/dist/ai/AiChatSessions.js +30 -9
  5. package/dist/ai/AiChatSessions.js.map +2 -2
  6. package/dist/ai/AiDock.js +6 -1
  7. package/dist/ai/AiDock.js.map +2 -2
  8. package/dist/ai/conversation-store.js +2 -1
  9. package/dist/ai/conversation-store.js.map +2 -2
  10. package/dist/ai/useAiChat.js +15 -4
  11. package/dist/ai/useAiChat.js.map +2 -2
  12. package/dist/backend/charts/BarChart.js +19 -76
  13. package/dist/backend/charts/BarChart.js.map +2 -2
  14. package/dist/backend/charts/BarChartImpl.js +91 -0
  15. package/dist/backend/charts/BarChartImpl.js.map +7 -0
  16. package/dist/backend/charts/LineChart.js +18 -91
  17. package/dist/backend/charts/LineChart.js.map +2 -2
  18. package/dist/backend/charts/LineChartImpl.js +112 -0
  19. package/dist/backend/charts/LineChartImpl.js.map +7 -0
  20. package/dist/backend/charts/PieChart.js +17 -62
  21. package/dist/backend/charts/PieChart.js.map +2 -2
  22. package/dist/backend/charts/PieChartImpl.js +76 -0
  23. package/dist/backend/charts/PieChartImpl.js.map +7 -0
  24. package/dist/backend/dashboard/DashboardScreen.js +9 -1
  25. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  26. package/dist/backend/dashboard/widgetRegistry.js +18 -2
  27. package/dist/backend/dashboard/widgetRegistry.js.map +2 -2
  28. package/dist/backend/injection/useInjectionDataWidgets.js +12 -2
  29. package/dist/backend/injection/useInjectionDataWidgets.js.map +2 -2
  30. package/package.json +3 -3
  31. package/src/ai/AiChat.tsx +10 -0
  32. package/src/ai/AiChatSessions.tsx +54 -9
  33. package/src/ai/AiDock.tsx +11 -0
  34. package/src/ai/__tests__/AiChat.conversation.test.tsx +97 -0
  35. package/src/ai/__tests__/AiChatSessions.test.tsx +228 -0
  36. package/src/ai/__tests__/AiDock.test.tsx +31 -1
  37. package/src/ai/__tests__/conversation-store.test.ts +74 -1
  38. package/src/ai/conversation-store.ts +18 -2
  39. package/src/ai/useAiChat.ts +35 -4
  40. package/src/backend/__tests__/lazy-heavy-libraries.test.ts +53 -0
  41. package/src/backend/charts/BarChart.tsx +21 -72
  42. package/src/backend/charts/BarChartImpl.tsx +99 -0
  43. package/src/backend/charts/LineChart.tsx +23 -85
  44. package/src/backend/charts/LineChartImpl.tsx +120 -0
  45. package/src/backend/charts/PieChart.tsx +19 -60
  46. package/src/backend/charts/PieChartImpl.tsx +82 -0
  47. package/src/backend/dashboard/DashboardScreen.tsx +9 -1
  48. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +45 -0
  49. package/src/backend/dashboard/__tests__/widgetRegistry.test.ts +60 -0
  50. package/src/backend/dashboard/widgetRegistry.ts +23 -5
  51. package/src/backend/injection/useInjectionDataWidgets.ts +18 -2
@@ -0,0 +1,99 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ BarChart as RechartsBarChart,
6
+ Bar,
7
+ XAxis,
8
+ YAxis,
9
+ CartesianGrid,
10
+ Tooltip,
11
+ Legend,
12
+ ResponsiveContainer,
13
+ } from 'recharts'
14
+ import { ChartTooltipContent, resolveChartColor } from './ChartUtils'
15
+ import type { BarChartDataItem, BarChartProps } from './BarChart'
16
+
17
+ type ImplProps = Pick<
18
+ BarChartProps,
19
+ | 'data'
20
+ | 'index'
21
+ | 'categories'
22
+ | 'colors'
23
+ | 'layout'
24
+ | 'valueFormatter'
25
+ | 'showLegend'
26
+ | 'showGridLines'
27
+ | 'categoryLabels'
28
+ > & {
29
+ valueFormatter: (value: number) => string
30
+ }
31
+
32
+ export default function BarChartImpl({
33
+ data,
34
+ index,
35
+ categories,
36
+ colors,
37
+ layout = 'vertical',
38
+ valueFormatter,
39
+ showLegend = true,
40
+ showGridLines = true,
41
+ categoryLabels,
42
+ }: ImplProps) {
43
+ const isHorizontal = layout === 'horizontal'
44
+ const chartHeight = isHorizontal ? Math.max(200, data.length * 28) : 200
45
+ const getBarColor = (idx: number): string => resolveChartColor(colors?.[idx], idx)
46
+
47
+ return (
48
+ <ResponsiveContainer width="100%" height={chartHeight}>
49
+ <RechartsBarChart
50
+ data={data}
51
+ layout={isHorizontal ? 'vertical' : 'horizontal'}
52
+ margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
53
+ >
54
+ {showGridLines && (
55
+ <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
56
+ )}
57
+ <XAxis
58
+ type={isHorizontal ? 'number' : 'category'}
59
+ dataKey={isHorizontal ? undefined : index}
60
+ tickFormatter={isHorizontal ? valueFormatter : undefined}
61
+ tick={{ fontSize: 11 }}
62
+ />
63
+ <YAxis
64
+ type={isHorizontal ? 'category' : 'number'}
65
+ dataKey={isHorizontal ? index : undefined}
66
+ tickFormatter={isHorizontal ? undefined : valueFormatter}
67
+ width={isHorizontal ? 90 : 50}
68
+ interval={0}
69
+ tick={{ fontSize: 10 }}
70
+ />
71
+ <Tooltip
72
+ content={
73
+ <ChartTooltipContent
74
+ valueFormatter={valueFormatter}
75
+ categoryLabels={categoryLabels}
76
+ labelFormatter={(label, payload) => {
77
+ const entry = payload?.[0] as { payload?: BarChartDataItem } | undefined
78
+ const item = entry?.payload
79
+ return item?.[index] ? String(item[index]) : label
80
+ }}
81
+ />
82
+ }
83
+ cursor={{ fill: 'hsl(var(--muted))', opacity: 0.2 }}
84
+ />
85
+ {showLegend && categories.length > 1 && (
86
+ <Legend verticalAlign="top" height={36} />
87
+ )}
88
+ {categories.map((category, idx) => (
89
+ <Bar
90
+ key={category}
91
+ dataKey={category}
92
+ fill={getBarColor(idx)}
93
+ radius={isHorizontal ? [0, 4, 4, 0] : [4, 4, 0, 0]}
94
+ />
95
+ ))}
96
+ </RechartsBarChart>
97
+ </ResponsiveContainer>
98
+ )
99
+ }
@@ -1,20 +1,8 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import {
5
- LineChart as RechartsLineChart,
6
- AreaChart as RechartsAreaChart,
7
- Line,
8
- Area,
9
- XAxis,
10
- YAxis,
11
- CartesianGrid,
12
- Tooltip,
13
- ResponsiveContainer,
14
- Legend,
15
- } from 'recharts'
4
+ import dynamic from 'next/dynamic'
16
5
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
17
- import { ChartTooltipContent, resolveChartColor } from './ChartUtils'
18
6
 
19
7
  export type LineChartDataItem = Record<string, string | number | null | undefined>
20
8
 
@@ -47,6 +35,15 @@ function defaultValueFormatter(value: number): string {
47
35
  return value.toLocaleString(undefined, { maximumFractionDigits: 2 })
48
36
  }
49
37
 
38
+ const LineChartImpl = dynamic(() => import('./LineChartImpl'), {
39
+ ssr: false,
40
+ loading: () => (
41
+ <div className="flex h-48 items-center justify-center">
42
+ <Spinner className="h-6 w-6 text-muted-foreground" />
43
+ </div>
44
+ ),
45
+ })
46
+
50
47
  export function LineChart({
51
48
  title,
52
49
  data,
@@ -65,10 +62,6 @@ export function LineChart({
65
62
  emptyMessage = 'No data available',
66
63
  categoryLabels,
67
64
  }: LineChartProps) {
68
- const getLineColor = (idx: number): string => {
69
- return resolveChartColor(colors?.[idx], idx)
70
- }
71
-
72
65
  if (error) {
73
66
  return (
74
67
  <div className={`rounded-lg border bg-card p-4 ${className}`}>
@@ -102,78 +95,23 @@ export function LineChart({
102
95
  )
103
96
  }
104
97
 
105
- const ChartComponent = showArea ? RechartsAreaChart : RechartsLineChart
106
-
107
98
  return (
108
99
  <div className={`rounded-lg border bg-card p-4 ${className}`}>
109
100
  {title && <h3 className="mb-4 text-base font-medium text-card-foreground">{title}</h3>}
110
101
  <div className="h-52 w-full">
111
- <ResponsiveContainer width="100%" height="100%">
112
- <ChartComponent
113
- data={data}
114
- margin={{ top: 5, right: 10, left: 10, bottom: 5 }}
115
- >
116
- {showGridLines && (
117
- <CartesianGrid
118
- strokeDasharray="3 3"
119
- stroke="hsl(var(--border))"
120
- />
121
- )}
122
- <XAxis
123
- dataKey={index}
124
- tick={{ fontSize: 12 }}
125
- tickLine={false}
126
- axisLine={false}
127
- />
128
- <YAxis
129
- tickFormatter={valueFormatter}
130
- tick={{ fontSize: 12 }}
131
- tickLine={false}
132
- axisLine={false}
133
- width={56}
134
- />
135
- <Tooltip
136
- content={<ChartTooltipContent valueFormatter={valueFormatter} categoryLabels={categoryLabels} />}
137
- cursor={{ stroke: 'hsl(var(--muted-foreground))', strokeDasharray: '3 3' }}
138
- />
139
- {showLegend && categories.length > 1 && (
140
- <Legend
141
- verticalAlign="top"
142
- height={36}
143
- formatter={(value) => (
144
- <span style={{ color: 'hsl(var(--muted-foreground))', fontSize: '12px' }}>{value}</span>
145
- )}
146
- />
147
- )}
148
- {showArea
149
- ? categories.map((category, idx) => (
150
- <Area
151
- key={category}
152
- type={curveType}
153
- dataKey={category}
154
- stroke={getLineColor(idx)}
155
- fill={getLineColor(idx)}
156
- fillOpacity={0.2}
157
- strokeWidth={2}
158
- connectNulls={connectNulls}
159
- dot={false}
160
- activeDot={{ r: 4, strokeWidth: 0 }}
161
- />
162
- ))
163
- : categories.map((category, idx) => (
164
- <Line
165
- key={category}
166
- type={curveType}
167
- dataKey={category}
168
- stroke={getLineColor(idx)}
169
- strokeWidth={2}
170
- connectNulls={connectNulls}
171
- dot={false}
172
- activeDot={{ r: 4, strokeWidth: 0 }}
173
- />
174
- ))}
175
- </ChartComponent>
176
- </ResponsiveContainer>
102
+ <LineChartImpl
103
+ data={data}
104
+ index={index}
105
+ categories={categories}
106
+ colors={colors}
107
+ showArea={showArea}
108
+ valueFormatter={valueFormatter}
109
+ showLegend={showLegend}
110
+ showGridLines={showGridLines}
111
+ curveType={curveType}
112
+ connectNulls={connectNulls}
113
+ categoryLabels={categoryLabels}
114
+ />
177
115
  </div>
178
116
  </div>
179
117
  )
@@ -0,0 +1,120 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ LineChart as RechartsLineChart,
6
+ AreaChart as RechartsAreaChart,
7
+ Line,
8
+ Area,
9
+ XAxis,
10
+ YAxis,
11
+ CartesianGrid,
12
+ Tooltip,
13
+ ResponsiveContainer,
14
+ Legend,
15
+ } from 'recharts'
16
+ import { ChartTooltipContent, resolveChartColor } from './ChartUtils'
17
+ import type { LineChartProps } from './LineChart'
18
+
19
+ type ImplProps = Pick<
20
+ LineChartProps,
21
+ | 'data'
22
+ | 'index'
23
+ | 'categories'
24
+ | 'colors'
25
+ | 'showArea'
26
+ | 'valueFormatter'
27
+ | 'showLegend'
28
+ | 'showGridLines'
29
+ | 'curveType'
30
+ | 'connectNulls'
31
+ | 'categoryLabels'
32
+ > & {
33
+ valueFormatter: (value: number) => string
34
+ }
35
+
36
+ export default function LineChartImpl({
37
+ data,
38
+ index,
39
+ categories,
40
+ colors,
41
+ showArea = false,
42
+ valueFormatter,
43
+ showLegend = true,
44
+ showGridLines = true,
45
+ curveType = 'monotone',
46
+ connectNulls = true,
47
+ categoryLabels,
48
+ }: ImplProps) {
49
+ const getLineColor = (idx: number): string => resolveChartColor(colors?.[idx], idx)
50
+ const ChartComponent = showArea ? RechartsAreaChart : RechartsLineChart
51
+
52
+ return (
53
+ <ResponsiveContainer width="100%" height="100%">
54
+ <ChartComponent
55
+ data={data}
56
+ margin={{ top: 5, right: 10, left: 10, bottom: 5 }}
57
+ >
58
+ {showGridLines && (
59
+ <CartesianGrid
60
+ strokeDasharray="3 3"
61
+ stroke="hsl(var(--border))"
62
+ />
63
+ )}
64
+ <XAxis
65
+ dataKey={index}
66
+ tick={{ fontSize: 12 }}
67
+ tickLine={false}
68
+ axisLine={false}
69
+ />
70
+ <YAxis
71
+ tickFormatter={valueFormatter}
72
+ tick={{ fontSize: 12 }}
73
+ tickLine={false}
74
+ axisLine={false}
75
+ width={56}
76
+ />
77
+ <Tooltip
78
+ content={<ChartTooltipContent valueFormatter={valueFormatter} categoryLabels={categoryLabels} />}
79
+ cursor={{ stroke: 'hsl(var(--muted-foreground))', strokeDasharray: '3 3' }}
80
+ />
81
+ {showLegend && categories.length > 1 && (
82
+ <Legend
83
+ verticalAlign="top"
84
+ height={36}
85
+ formatter={(value) => (
86
+ <span style={{ color: 'hsl(var(--muted-foreground))', fontSize: '12px' }}>{value}</span>
87
+ )}
88
+ />
89
+ )}
90
+ {showArea
91
+ ? categories.map((category, idx) => (
92
+ <Area
93
+ key={category}
94
+ type={curveType}
95
+ dataKey={category}
96
+ stroke={getLineColor(idx)}
97
+ fill={getLineColor(idx)}
98
+ fillOpacity={0.2}
99
+ strokeWidth={2}
100
+ connectNulls={connectNulls}
101
+ dot={false}
102
+ activeDot={{ r: 4, strokeWidth: 0 }}
103
+ />
104
+ ))
105
+ : categories.map((category, idx) => (
106
+ <Line
107
+ key={category}
108
+ type={curveType}
109
+ dataKey={category}
110
+ stroke={getLineColor(idx)}
111
+ strokeWidth={2}
112
+ connectNulls={connectNulls}
113
+ dot={false}
114
+ activeDot={{ r: 4, strokeWidth: 0 }}
115
+ />
116
+ ))}
117
+ </ChartComponent>
118
+ </ResponsiveContainer>
119
+ )
120
+ }
@@ -1,17 +1,8 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import {
5
- PieChart as RechartsPieChart,
6
- Pie,
7
- Cell,
8
- Tooltip,
9
- ResponsiveContainer,
10
- Legend,
11
- Label,
12
- } from 'recharts'
4
+ import dynamic from 'next/dynamic'
13
5
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
14
- import { ChartTooltipContent, resolveChartColor } from './ChartUtils'
15
6
 
16
7
  export type PieChartDataItem = {
17
8
  name: string
@@ -42,6 +33,15 @@ function defaultValueFormatter(value: number): string {
42
33
  return value.toLocaleString(undefined, { maximumFractionDigits: 2 })
43
34
  }
44
35
 
36
+ const PieChartImpl = dynamic(() => import('./PieChartImpl'), {
37
+ ssr: false,
38
+ loading: () => (
39
+ <div className="flex h-48 items-center justify-center">
40
+ <Spinner className="h-6 w-6 text-muted-foreground" />
41
+ </div>
42
+ ),
43
+ })
44
+
45
45
  export function PieChart({
46
46
  title,
47
47
  data,
@@ -55,10 +55,6 @@ export function PieChart({
55
55
  className = '',
56
56
  emptyMessage = 'No data available',
57
57
  }: PieChartProps) {
58
- const getSliceColor = (idx: number): string => {
59
- return resolveChartColor(colors?.[idx], idx)
60
- }
61
-
62
58
  const total = React.useMemo(() => {
63
59
  return data.reduce((sum, item) => sum + item.value, 0)
64
60
  }, [data])
@@ -99,56 +95,19 @@ export function PieChart({
99
95
  )
100
96
  }
101
97
 
102
- const innerRadius = variant === 'donut' ? '60%' : 0
103
- const outerRadius = '80%'
104
-
105
98
  return (
106
99
  <div className={wrapperClass}>
107
100
  {title && <h3 className="mb-4 text-base font-medium text-card-foreground">{title}</h3>}
108
101
  <div className="h-52 w-full">
109
- <ResponsiveContainer width="100%" height="100%">
110
- <RechartsPieChart>
111
- <Pie
112
- data={data}
113
- dataKey="value"
114
- nameKey="name"
115
- cx="50%"
116
- cy="40%"
117
- innerRadius={innerRadius}
118
- outerRadius={outerRadius}
119
- paddingAngle={2}
120
- strokeWidth={0}
121
- >
122
- {data.map((_, idx) => (
123
- <Cell key={`cell-${idx}`} fill={getSliceColor(idx)} />
124
- ))}
125
- {showLabel && variant === 'donut' && (
126
- <Label
127
- value={valueFormatter(total)}
128
- position="center"
129
- className="fill-foreground text-2xl font-bold"
130
- />
131
- )}
132
- </Pie>
133
- {showTooltip && (
134
- <Tooltip
135
- content={
136
- <ChartTooltipContent
137
- valueFormatter={valueFormatter}
138
- hideLabel
139
- />
140
- }
141
- />
142
- )}
143
- <Legend
144
- verticalAlign="bottom"
145
- height={36}
146
- formatter={(value) => (
147
- <span style={{ color: 'hsl(var(--muted-foreground))', fontSize: '12px' }}>{value}</span>
148
- )}
149
- />
150
- </RechartsPieChart>
151
- </ResponsiveContainer>
102
+ <PieChartImpl
103
+ data={data}
104
+ colors={colors}
105
+ variant={variant}
106
+ valueFormatter={valueFormatter}
107
+ showLabel={showLabel}
108
+ showTooltip={showTooltip}
109
+ total={total}
110
+ />
152
111
  </div>
153
112
  </div>
154
113
  )
@@ -0,0 +1,82 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ PieChart as RechartsPieChart,
6
+ Pie,
7
+ Cell,
8
+ Tooltip,
9
+ ResponsiveContainer,
10
+ Legend,
11
+ Label,
12
+ } from 'recharts'
13
+ import { ChartTooltipContent, resolveChartColor } from './ChartUtils'
14
+ import type { PieChartProps } from './PieChart'
15
+
16
+ type ImplProps = Pick<
17
+ PieChartProps,
18
+ 'data' | 'colors' | 'variant' | 'valueFormatter' | 'showLabel' | 'showTooltip'
19
+ > & {
20
+ valueFormatter: (value: number) => string
21
+ total: number
22
+ }
23
+
24
+ export default function PieChartImpl({
25
+ data,
26
+ colors,
27
+ variant = 'donut',
28
+ valueFormatter,
29
+ showLabel = true,
30
+ showTooltip = true,
31
+ total,
32
+ }: ImplProps) {
33
+ const getSliceColor = (idx: number): string => resolveChartColor(colors?.[idx], idx)
34
+ const innerRadius = variant === 'donut' ? '60%' : 0
35
+ const outerRadius = '80%'
36
+
37
+ return (
38
+ <ResponsiveContainer width="100%" height="100%">
39
+ <RechartsPieChart>
40
+ <Pie
41
+ data={data}
42
+ dataKey="value"
43
+ nameKey="name"
44
+ cx="50%"
45
+ cy="40%"
46
+ innerRadius={innerRadius}
47
+ outerRadius={outerRadius}
48
+ paddingAngle={2}
49
+ strokeWidth={0}
50
+ >
51
+ {data.map((_, idx) => (
52
+ <Cell key={`cell-${idx}`} fill={getSliceColor(idx)} />
53
+ ))}
54
+ {showLabel && variant === 'donut' && (
55
+ <Label
56
+ value={valueFormatter(total)}
57
+ position="center"
58
+ className="fill-foreground text-2xl font-bold"
59
+ />
60
+ )}
61
+ </Pie>
62
+ {showTooltip && (
63
+ <Tooltip
64
+ content={
65
+ <ChartTooltipContent
66
+ valueFormatter={valueFormatter}
67
+ hideLabel
68
+ />
69
+ }
70
+ />
71
+ )}
72
+ <Legend
73
+ verticalAlign="bottom"
74
+ height={36}
75
+ formatter={(value) => (
76
+ <span style={{ color: 'hsl(var(--muted-foreground))', fontSize: '12px' }}>{value}</span>
77
+ )}
78
+ />
79
+ </RechartsPieChart>
80
+ </ResponsiveContainer>
81
+ )
82
+ }
@@ -12,6 +12,7 @@ import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboa
12
12
  import { cn } from '@open-mercato/shared/lib/utils'
13
13
  import { GripVertical, Info, Plus, RefreshCw, Settings2, Trash2, X, Loader2 } from 'lucide-react'
14
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
15
16
  import { InjectionSpot } from '../injection/InjectionSpot'
16
17
 
17
18
  type DashboardWidgetSize = 'sm' | 'md' | 'lg'
@@ -92,6 +93,7 @@ function generateId(): string {
92
93
 
93
94
  export function DashboardScreen() {
94
95
  const t = useT()
96
+ const organizationScopeVersion = useOrganizationScopeVersion()
95
97
  const [loading, setLoading] = React.useState(true)
96
98
  const [error, setError] = React.useState<string | null>(null)
97
99
  const [hasRegisteredWidgets, setHasRegisteredWidgets] = React.useState(true)
@@ -165,7 +167,7 @@ export function DashboardScreen() {
165
167
 
166
168
  React.useEffect(() => {
167
169
  load()
168
- }, [load])
170
+ }, [load, organizationScopeVersion])
169
171
 
170
172
  const metaById = React.useMemo(() => {
171
173
  const map = new Map<string, WidgetMeta>()
@@ -560,6 +562,12 @@ function DashboardWidgetCard({
560
562
  loadDashboardWidgetModule(meta.loaderKey)
561
563
  .then((loaded) => {
562
564
  if (cancelled) return
565
+ if (!loaded) {
566
+ setModule(null)
567
+ setLoadError(t('dashboard.widget.loadError'))
568
+ setLoading(false)
569
+ return
570
+ }
563
571
  setModule(loaded)
564
572
  setLoading(false)
565
573
  })
@@ -20,6 +20,12 @@ jest.mock('../widgetRegistry', () => ({
20
20
  loadDashboardWidgetModule: jest.fn(),
21
21
  }))
22
22
 
23
+ let mockOrganizationScopeVersion = 0
24
+
25
+ jest.mock('@open-mercato/shared/lib/frontend/useOrganizationScope', () => ({
26
+ useOrganizationScopeVersion: () => mockOrganizationScopeVersion,
27
+ }))
28
+
23
29
  jest.mock('next/navigation', () => ({
24
30
  useRouter: () => ({
25
31
  refresh: jest.fn(),
@@ -32,6 +38,7 @@ const createMockResponse = (status: number): Response => ({ status } as Response
32
38
 
33
39
  const dict = {
34
40
  'dashboard.loadError': 'Failed to load dashboard',
41
+ 'dashboard.widget.loadError': 'Unable to load widget.',
35
42
  'dashboard.empty.noWidgets.title': 'No dashboard widgets yet',
36
43
  'dashboard.empty.noWidgets.description': 'Dashboard widgets will appear here after you add a module.',
37
44
  'dashboard.widgets.foo.title': 'Widget Foo',
@@ -74,6 +81,7 @@ function MockWidget() {
74
81
  describe('DashboardScreen', () => {
75
82
  beforeEach(() => {
76
83
  jest.resetAllMocks()
84
+ mockOrganizationScopeVersion = 0
77
85
  ;(getDashboardWidgets as jest.Mock).mockReturnValue([{ key: 'foo.loader', loader: jest.fn() }])
78
86
  ;(loadDashboardWidgetModule as jest.Mock).mockResolvedValue({
79
87
  Widget: MockWidget,
@@ -98,6 +106,27 @@ describe('DashboardScreen', () => {
98
106
  expect(await screen.findByText('Widget body')).toBeInTheDocument()
99
107
  })
100
108
 
109
+ it('reloads the dashboard when the organization scope changes', async () => {
110
+ ;(apiCall as jest.Mock).mockResolvedValue({
111
+ ok: true,
112
+ status: 200,
113
+ result: widgetResponse,
114
+ response: createMockResponse(200),
115
+ })
116
+
117
+ const { rerender } = renderWithProviders(<DashboardScreen />, { dict })
118
+
119
+ expect(await screen.findByText('Widget body')).toBeInTheDocument()
120
+ expect(apiCall).toHaveBeenCalledTimes(1)
121
+
122
+ mockOrganizationScopeVersion = 1
123
+ rerender(<DashboardScreen />)
124
+
125
+ await waitFor(() => {
126
+ expect(apiCall).toHaveBeenCalledTimes(2)
127
+ })
128
+ })
129
+
101
130
  it('shows an error when the layout request fails', async () => {
102
131
  const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
103
132
  ;(apiCall as jest.Mock).mockResolvedValue({
@@ -134,4 +163,20 @@ describe('DashboardScreen', () => {
134
163
 
135
164
  errorSpy.mockRestore()
136
165
  })
166
+
167
+ it('shows a widget load error when the registered module cannot be found', async () => {
168
+ ;(apiCall as jest.Mock).mockResolvedValue({
169
+ ok: true,
170
+ status: 200,
171
+ result: widgetResponse,
172
+ response: createMockResponse(200),
173
+ })
174
+ ;(loadDashboardWidgetModule as jest.Mock).mockResolvedValue(null)
175
+
176
+ renderWithProviders(<DashboardScreen />, { dict })
177
+
178
+ expect(await screen.findByText('Widget Foo')).toBeInTheDocument()
179
+ expect(await screen.findByText('Unable to load widget.')).toBeInTheDocument()
180
+ expect(screen.queryByText('Widget body')).not.toBeInTheDocument()
181
+ })
137
182
  })