@open-mercato/ui 0.6.3-develop.3894.1.352abf4240 → 0.6.3

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 (67) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/ai/AiAssistantLauncher.js +61 -8
  3. package/dist/ai/AiAssistantLauncher.js.map +2 -2
  4. package/dist/ai/AiChat.js +29 -6
  5. package/dist/ai/AiChat.js.map +2 -2
  6. package/dist/ai/AiChatSessions.js +31 -9
  7. package/dist/ai/AiChatSessions.js.map +2 -2
  8. package/dist/ai/AiDock.js +8 -1
  9. package/dist/ai/AiDock.js.map +2 -2
  10. package/dist/ai/ConversationShareButton.js +40 -0
  11. package/dist/ai/ConversationShareButton.js.map +7 -0
  12. package/dist/ai/ConversationShareDialog.js +234 -0
  13. package/dist/ai/ConversationShareDialog.js.map +7 -0
  14. package/dist/ai/conversation-store.js +4 -2
  15. package/dist/ai/conversation-store.js.map +2 -2
  16. package/dist/ai/index.js +4 -0
  17. package/dist/ai/index.js.map +2 -2
  18. package/dist/ai/useAiChat.js +20 -4
  19. package/dist/ai/useAiChat.js.map +2 -2
  20. package/dist/backend/charts/BarChart.js +19 -76
  21. package/dist/backend/charts/BarChart.js.map +2 -2
  22. package/dist/backend/charts/BarChartImpl.js +91 -0
  23. package/dist/backend/charts/BarChartImpl.js.map +7 -0
  24. package/dist/backend/charts/LineChart.js +18 -91
  25. package/dist/backend/charts/LineChart.js.map +2 -2
  26. package/dist/backend/charts/LineChartImpl.js +112 -0
  27. package/dist/backend/charts/LineChartImpl.js.map +7 -0
  28. package/dist/backend/charts/PieChart.js +17 -62
  29. package/dist/backend/charts/PieChart.js.map +2 -2
  30. package/dist/backend/charts/PieChartImpl.js +76 -0
  31. package/dist/backend/charts/PieChartImpl.js.map +7 -0
  32. package/dist/backend/dashboard/DashboardScreen.js +9 -1
  33. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  34. package/dist/backend/dashboard/widgetRegistry.js +18 -2
  35. package/dist/backend/dashboard/widgetRegistry.js.map +2 -2
  36. package/dist/backend/icons/lucideRegistry.generated.js +2 -0
  37. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  38. package/dist/backend/injection/useInjectionDataWidgets.js +12 -2
  39. package/dist/backend/injection/useInjectionDataWidgets.js.map +2 -2
  40. package/package.json +4 -5
  41. package/src/ai/AiAssistantLauncher.tsx +81 -5
  42. package/src/ai/AiChat.tsx +42 -2
  43. package/src/ai/AiChatSessions.tsx +55 -9
  44. package/src/ai/AiDock.tsx +13 -0
  45. package/src/ai/ConversationShareButton.tsx +39 -0
  46. package/src/ai/ConversationShareDialog.tsx +299 -0
  47. package/src/ai/__tests__/AiAssistantLauncher.test.tsx +6 -0
  48. package/src/ai/__tests__/AiChat.conversation.test.tsx +97 -0
  49. package/src/ai/__tests__/AiChatSessions.test.tsx +228 -0
  50. package/src/ai/__tests__/AiDock.test.tsx +31 -1
  51. package/src/ai/__tests__/conversation-store.test.ts +74 -1
  52. package/src/ai/conversation-store.ts +22 -2
  53. package/src/ai/index.ts +2 -0
  54. package/src/ai/useAiChat.ts +47 -4
  55. package/src/backend/__tests__/lazy-heavy-libraries.test.ts +53 -0
  56. package/src/backend/charts/BarChart.tsx +21 -72
  57. package/src/backend/charts/BarChartImpl.tsx +99 -0
  58. package/src/backend/charts/LineChart.tsx +23 -85
  59. package/src/backend/charts/LineChartImpl.tsx +120 -0
  60. package/src/backend/charts/PieChart.tsx +19 -60
  61. package/src/backend/charts/PieChartImpl.tsx +82 -0
  62. package/src/backend/dashboard/DashboardScreen.tsx +9 -1
  63. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +45 -0
  64. package/src/backend/dashboard/__tests__/widgetRegistry.test.ts +60 -0
  65. package/src/backend/dashboard/widgetRegistry.ts +23 -5
  66. package/src/backend/icons/lucideRegistry.generated.tsx +2 -0
  67. package/src/backend/injection/useInjectionDataWidgets.ts +18 -2
@@ -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
  })
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import type { ModuleDashboardWidgetEntry } from '@open-mercato/shared/modules/registry'
6
+
7
+ function createEntry(loader = jest.fn().mockResolvedValue({ Widget: () => null })): ModuleDashboardWidgetEntry {
8
+ return {
9
+ moduleId: 'example',
10
+ key: 'example.dashboard.widget',
11
+ source: 'app',
12
+ loader,
13
+ }
14
+ }
15
+
16
+ describe('dashboard widget registry', () => {
17
+ beforeEach(() => {
18
+ jest.resetModules()
19
+ })
20
+
21
+ it('waits for client bootstrap registration before resolving widget modules', async () => {
22
+ const { loadDashboardWidgetModule, registerDashboardWidgets } = await import('../widgetRegistry')
23
+ const loader = jest.fn().mockResolvedValue({ Widget: () => null })
24
+ const pending = loadDashboardWidgetModule('example.dashboard.widget')
25
+ let resolved = false
26
+ pending.then(() => {
27
+ resolved = true
28
+ })
29
+
30
+ await Promise.resolve()
31
+ expect(resolved).toBe(false)
32
+ expect(loader).not.toHaveBeenCalled()
33
+
34
+ registerDashboardWidgets([createEntry(loader)])
35
+
36
+ await expect(pending).resolves.toEqual({ Widget: expect.any(Function) })
37
+ expect(loader).toHaveBeenCalledTimes(1)
38
+ })
39
+
40
+ it('uses the latest entries after dashboard widgets are re-registered', async () => {
41
+ const { loadDashboardWidgetModule, registerDashboardWidgets } = await import('../widgetRegistry')
42
+ const firstLoader = jest.fn().mockResolvedValue({ Widget: () => null })
43
+ const secondLoader = jest.fn().mockResolvedValue({ Widget: () => null })
44
+
45
+ registerDashboardWidgets([createEntry(firstLoader)])
46
+ await loadDashboardWidgetModule('example.dashboard.widget')
47
+
48
+ registerDashboardWidgets([
49
+ {
50
+ ...createEntry(secondLoader),
51
+ key: 'example.dashboard.other',
52
+ },
53
+ ])
54
+
55
+ await loadDashboardWidgetModule('example.dashboard.other')
56
+
57
+ expect(firstLoader).toHaveBeenCalledTimes(1)
58
+ expect(secondLoader).toHaveBeenCalledTimes(1)
59
+ })
60
+ })
@@ -3,15 +3,30 @@ import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboa
3
3
  import { applyDashboardWidgetOverridesToEntries } from '@open-mercato/shared/modules/overrides'
4
4
 
5
5
  type Entry = ModuleDashboardWidgetEntry
6
+ type LoadedWidgetModule = DashboardWidgetModule<any>
7
+
8
+ const cache = new Map<string, Promise<LoadedWidgetModule>>()
6
9
 
7
10
  // Registration pattern for publishable packages
8
11
  let _dashboardWidgetEntries: Entry[] | null = null
12
+ let registrationWaiters: Array<(entries: Entry[]) => void> = []
13
+
14
+ function resolveRegistrationWaiters(entries: Entry[]) {
15
+ const waiters = registrationWaiters
16
+ registrationWaiters = []
17
+ for (const resolve of waiters) {
18
+ resolve(entries)
19
+ }
20
+ }
9
21
 
10
22
  export function registerDashboardWidgets(entries: Entry[]) {
11
23
  if (_dashboardWidgetEntries !== null && process.env.NODE_ENV === 'development') {
12
24
  console.debug('[Bootstrap] Dashboard widgets re-registered (this may occur during HMR)')
13
25
  }
14
26
  _dashboardWidgetEntries = applyDashboardWidgetOverridesToEntries(entries)
27
+ entriesPromise = Promise.resolve(_dashboardWidgetEntries)
28
+ cache.clear()
29
+ resolveRegistrationWaiters(_dashboardWidgetEntries)
15
30
  }
16
31
 
17
32
  export function getDashboardWidgets(): Entry[] {
@@ -28,8 +43,15 @@ export function getDashboardWidgets(): Entry[] {
28
43
  let entriesPromise: Promise<Entry[]> | null = null
29
44
 
30
45
  async function getEntries(): Promise<Entry[]> {
46
+ if (_dashboardWidgetEntries) {
47
+ return _dashboardWidgetEntries
48
+ }
31
49
  if (!entriesPromise) {
32
- const promise = Promise.resolve().then(() => getDashboardWidgets())
50
+ const promise = typeof window !== 'undefined'
51
+ ? new Promise<Entry[]>((resolve) => {
52
+ registrationWaiters.push(resolve)
53
+ })
54
+ : Promise.resolve().then(() => getDashboardWidgets())
33
55
  entriesPromise = promise.catch((err) => {
34
56
  // Clear cache on error so next call can retry after registration
35
57
  if (entriesPromise === promise) {
@@ -41,10 +63,6 @@ async function getEntries(): Promise<Entry[]> {
41
63
  return entriesPromise
42
64
  }
43
65
 
44
- type LoadedWidgetModule = DashboardWidgetModule<any>
45
-
46
- const cache = new Map<string, Promise<LoadedWidgetModule>>()
47
-
48
66
  async function findEntry(loaderKey: string): Promise<Entry | undefined> {
49
67
  const entries = await getEntries()
50
68
  return entries.find((entry) => entry.key === loaderKey)
@@ -104,6 +104,7 @@ import {
104
104
  Send,
105
105
  Settings,
106
106
  Shapes,
107
+ Share2,
107
108
  Shield,
108
109
  ShieldAlert,
109
110
  ShieldCheck,
@@ -242,6 +243,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
242
243
  'send': Send,
243
244
  'settings': Settings,
244
245
  'shapes': Shapes,
246
+ 'share-2': Share2,
245
247
  'shield': Shield,
246
248
  'shield-alert': ShieldAlert,
247
249
  'shield-check': ShieldCheck,
@@ -2,7 +2,12 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'
5
- import { loadInjectionDataWidgetsForSpot, type LoadedInjectionDataWidget } from '@open-mercato/shared/modules/widgets/injection-loader'
5
+ import {
6
+ loadInjectionDataWidgetsForSpot,
7
+ getInjectionRegistryVersion,
8
+ subscribeToInjectionRegistryChanges,
9
+ type LoadedInjectionDataWidget,
10
+ } from '@open-mercato/shared/modules/widgets/injection-loader'
6
11
 
7
12
  export function useInjectionDataWidgets(spotId: InjectionSpotId): {
8
13
  widgets: LoadedInjectionDataWidget[]
@@ -12,6 +17,17 @@ export function useInjectionDataWidgets(spotId: InjectionSpotId): {
12
17
  const [widgets, setWidgets] = React.useState<LoadedInjectionDataWidget[]>([])
13
18
  const [isLoading, setIsLoading] = React.useState(true)
14
19
  const [error, setError] = React.useState<string | null>(null)
20
+ // Re-load when the injection registry is (re-)populated. With the async
21
+ // ClientBootstrap, registration can land after this hook first runs; the
22
+ // version bump triggers a reload so injected menus/columns/fields appear
23
+ // without requiring an unrelated re-render.
24
+ const [registryVersion, setRegistryVersion] = React.useState(() => getInjectionRegistryVersion())
25
+
26
+ React.useEffect(() => {
27
+ return subscribeToInjectionRegistryChanges(() => {
28
+ setRegistryVersion(getInjectionRegistryVersion())
29
+ })
30
+ }, [])
15
31
 
16
32
  React.useEffect(() => {
17
33
  let mounted = true
@@ -35,7 +51,7 @@ export function useInjectionDataWidgets(spotId: InjectionSpotId): {
35
51
  return () => {
36
52
  mounted = false
37
53
  }
38
- }, [spotId])
54
+ }, [spotId, registryVersion])
39
55
 
40
56
  return { widgets, isLoading, error }
41
57
  }