@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/ai/AiChat.js +4 -2
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiChatSessions.js +30 -9
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/AiDock.js +6 -1
- package/dist/ai/AiDock.js.map +2 -2
- package/dist/ai/conversation-store.js +2 -1
- package/dist/ai/conversation-store.js.map +2 -2
- package/dist/ai/useAiChat.js +15 -4
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/charts/BarChart.js +19 -76
- package/dist/backend/charts/BarChart.js.map +2 -2
- package/dist/backend/charts/BarChartImpl.js +91 -0
- package/dist/backend/charts/BarChartImpl.js.map +7 -0
- package/dist/backend/charts/LineChart.js +18 -91
- package/dist/backend/charts/LineChart.js.map +2 -2
- package/dist/backend/charts/LineChartImpl.js +112 -0
- package/dist/backend/charts/LineChartImpl.js.map +7 -0
- package/dist/backend/charts/PieChart.js +17 -62
- package/dist/backend/charts/PieChart.js.map +2 -2
- package/dist/backend/charts/PieChartImpl.js +76 -0
- package/dist/backend/charts/PieChartImpl.js.map +7 -0
- package/dist/backend/dashboard/DashboardScreen.js +9 -1
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/dashboard/widgetRegistry.js +18 -2
- package/dist/backend/dashboard/widgetRegistry.js.map +2 -2
- package/dist/backend/injection/useInjectionDataWidgets.js +12 -2
- package/dist/backend/injection/useInjectionDataWidgets.js.map +2 -2
- package/package.json +3 -3
- package/src/ai/AiChat.tsx +10 -0
- package/src/ai/AiChatSessions.tsx +54 -9
- package/src/ai/AiDock.tsx +11 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +97 -0
- package/src/ai/__tests__/AiChatSessions.test.tsx +228 -0
- package/src/ai/__tests__/AiDock.test.tsx +31 -1
- package/src/ai/__tests__/conversation-store.test.ts +74 -1
- package/src/ai/conversation-store.ts +18 -2
- package/src/ai/useAiChat.ts +35 -4
- package/src/backend/__tests__/lazy-heavy-libraries.test.ts +53 -0
- package/src/backend/charts/BarChart.tsx +21 -72
- package/src/backend/charts/BarChartImpl.tsx +99 -0
- package/src/backend/charts/LineChart.tsx +23 -85
- package/src/backend/charts/LineChartImpl.tsx +120 -0
- package/src/backend/charts/PieChart.tsx +19 -60
- package/src/backend/charts/PieChartImpl.tsx +82 -0
- package/src/backend/dashboard/DashboardScreen.tsx +9 -1
- package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +45 -0
- package/src/backend/dashboard/__tests__/widgetRegistry.test.ts +60 -0
- package/src/backend/dashboard/widgetRegistry.ts +23 -5
- 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
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
})
|