@orsetra/shared-ui 1.2.4 → 1.3.1
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/components/layout/layout-container.tsx +159 -328
- package/components/layout/sidebar/sidebar.tsx +3 -97
- package/components/layout/user-panel.tsx +145 -0
- package/components/ui/project-selector-modal.tsx +13 -28
- package/context/index.tsx +4 -1
- package/context/menu-context.tsx +29 -0
- package/context/project-selector-context.tsx +24 -0
- package/context/user-context.tsx +61 -0
- package/lib/interceptors.ts +2 -10
- package/package.json +1 -1
|
@@ -1,328 +1,159 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect
|
|
4
|
-
import { usePathname, useSearchParams, useParams } from "next/navigation"
|
|
5
|
-
import { MainSidebar, type SidebarMode } from "./sidebar/main-sidebar"
|
|
6
|
-
import { Sidebar, SidebarProvider, useSidebar } from "./sidebar/sidebar"
|
|
7
|
-
import { type SidebarMenus, type MainMenuItem,
|
|
8
|
-
import {
|
|
9
|
-
import { getMenuFromPath } from "../../lib/menu-utils"
|
|
10
|
-
import { useIsMobile } from "../../hooks/use-mobile"
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
onProjectChange,
|
|
161
|
-
}: LayoutContainerProps) {
|
|
162
|
-
const pathname = usePathname()
|
|
163
|
-
const searchParams = useSearchParams()
|
|
164
|
-
const params = useParams()
|
|
165
|
-
const { setOpen } = useSidebar()
|
|
166
|
-
const isMobile = useIsMobile()
|
|
167
|
-
const [isMainSidebarOpen, setIsMainSidebarOpen] = useState(false)
|
|
168
|
-
const [currentMenu, setCurrentMenu] = useState<string>("overview")
|
|
169
|
-
|
|
170
|
-
const effectiveMode = getSidebarMode ? getSidebarMode(pathname, params as Record<string, string>, searchParams) : mode
|
|
171
|
-
const isMinimized = effectiveMode === 'minimized'
|
|
172
|
-
const isHidden = effectiveMode === 'hidden' || isMobile
|
|
173
|
-
|
|
174
|
-
const [mainMenuItems, setMainMenuItems] = useState<MainMenuItem[]>([])
|
|
175
|
-
const [fetchedSidebarMenus, setFetchedSidebarMenus] = useState<SidebarMenus>({})
|
|
176
|
-
const [sectionLabels, setSectionLabels] = useState<Record<string, string>>({})
|
|
177
|
-
const [fetchedUserMenu, setFetchedUserMenu] = useState<Record<string, { id: string; label: string; href: string; icon: string }[]>>({})
|
|
178
|
-
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
if (!fetchMenus) return
|
|
181
|
-
fetchMenus().then((response) => {
|
|
182
|
-
if (!response.success) return
|
|
183
|
-
const { main, subMenus, userMenu: apiUserMenu } = response.data
|
|
184
|
-
|
|
185
|
-
setMainMenuItems(
|
|
186
|
-
main.map((item) => ({
|
|
187
|
-
id: item.id,
|
|
188
|
-
label: item.label,
|
|
189
|
-
icon: resolveIcon(item.icon),
|
|
190
|
-
}))
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
const menus: SidebarMenus = {}
|
|
194
|
-
const labels: Record<string, string> = {}
|
|
195
|
-
Object.entries(subMenus).forEach(([key, subMenu]) => {
|
|
196
|
-
menus[key] = subMenu.items.map((item) => ({
|
|
197
|
-
id: item.id,
|
|
198
|
-
name: item.label,
|
|
199
|
-
href: item.href,
|
|
200
|
-
icon: resolveIcon(item.icon),
|
|
201
|
-
}))
|
|
202
|
-
labels[key] = subMenu.header
|
|
203
|
-
})
|
|
204
|
-
setFetchedSidebarMenus(menus)
|
|
205
|
-
setSectionLabels(labels)
|
|
206
|
-
|
|
207
|
-
if (apiUserMenu) {
|
|
208
|
-
const userMenuMap: Record<string, { id: string; label: string; href: string; icon: string }[]> = {}
|
|
209
|
-
Object.entries(apiUserMenu).forEach(([key, subMenu]) => {
|
|
210
|
-
userMenuMap[key] = subMenu.items.map((item) => ({
|
|
211
|
-
id: item.id,
|
|
212
|
-
label: item.label,
|
|
213
|
-
href: item.href,
|
|
214
|
-
icon: item.icon,
|
|
215
|
-
}))
|
|
216
|
-
})
|
|
217
|
-
setFetchedUserMenu(userMenuMap)
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
221
|
-
}, [])
|
|
222
|
-
|
|
223
|
-
const effectiveSidebarMenus: SidebarMenus = { ...fetchedSidebarMenus, ...sidebarMenus }
|
|
224
|
-
|
|
225
|
-
// UserMenuConfig : priorité API (context-sensitive par currentMenu), sinon prop
|
|
226
|
-
const effectiveUserMenuConfig: UserMenuConfig | undefined =
|
|
227
|
-
fetchedUserMenu[currentMenu]?.length
|
|
228
|
-
? {
|
|
229
|
-
items: fetchedUserMenu[currentMenu].map((item) => ({
|
|
230
|
-
id: item.id,
|
|
231
|
-
label: item.label,
|
|
232
|
-
href: item.href,
|
|
233
|
-
icon: resolveIcon(item.icon),
|
|
234
|
-
})),
|
|
235
|
-
}
|
|
236
|
-
: userMenuConfig
|
|
237
|
-
|
|
238
|
-
useEffect(() => {
|
|
239
|
-
if (isMobile) setIsMainSidebarOpen(false)
|
|
240
|
-
}, [pathname, isMobile])
|
|
241
|
-
|
|
242
|
-
useEffect(() => {
|
|
243
|
-
const contextualMenu = getCurrentMenu ? getCurrentMenu(pathname, searchParams) : getMenuFromPath(pathname)
|
|
244
|
-
setCurrentMenu(contextualMenu)
|
|
245
|
-
}, [pathname, searchParams, getCurrentMenu])
|
|
246
|
-
|
|
247
|
-
const handleMainSidebarToggle = () => setIsMainSidebarOpen((v) => !v)
|
|
248
|
-
const handleMenuSelect = (menu: string) => setCurrentMenu(menu)
|
|
249
|
-
const handleSecondarySidebarOpen = () => setOpen(true)
|
|
250
|
-
|
|
251
|
-
return (
|
|
252
|
-
<div className="flex h-screen w-full bg-white overflow-visible">
|
|
253
|
-
{!isMinimized && !isHidden && (
|
|
254
|
-
<Sidebar
|
|
255
|
-
currentMenu={currentMenu}
|
|
256
|
-
onMainMenuToggle={handleMainSidebarToggle}
|
|
257
|
-
sidebarMenus={effectiveSidebarMenus}
|
|
258
|
-
main_base_url={main_base_url}
|
|
259
|
-
sectionLabels={sectionLabels}
|
|
260
|
-
getCurrentMenuItem={getCurrentMenuItem}
|
|
261
|
-
loadOrg={loadOrg}
|
|
262
|
-
/>
|
|
263
|
-
)}
|
|
264
|
-
|
|
265
|
-
<MainSidebar
|
|
266
|
-
main_base_url={main_base_url}
|
|
267
|
-
isOpen={isMainSidebarOpen}
|
|
268
|
-
onToggle={handleMainSidebarToggle}
|
|
269
|
-
onMenuSelect={handleMenuSelect}
|
|
270
|
-
currentMenu={currentMenu}
|
|
271
|
-
onSecondarySidebarOpen={handleSecondarySidebarOpen}
|
|
272
|
-
mode={isHidden ? "expanded" : mode}
|
|
273
|
-
sidebarMenus={effectiveSidebarMenus}
|
|
274
|
-
mainMenuItems={mainMenuItems}
|
|
275
|
-
/>
|
|
276
|
-
|
|
277
|
-
<div className="flex-1 flex flex-col min-w-0">
|
|
278
|
-
<header className="h-14 bg-gray-50 border-b border-ui-border flex-shrink-0">
|
|
279
|
-
<div className="h-full px-4 md:px-6 flex items-center justify-between">
|
|
280
|
-
{/* Gauche : bouton menu (mobile/hidden) */}
|
|
281
|
-
{isHidden ? (
|
|
282
|
-
<Button
|
|
283
|
-
variant="ghost"
|
|
284
|
-
size="sm"
|
|
285
|
-
onClick={handleMainSidebarToggle}
|
|
286
|
-
className="h-8 w-8 p-0 hover:bg-ui-background text-text-secondary"
|
|
287
|
-
title="Ouvrir le menu"
|
|
288
|
-
>
|
|
289
|
-
<Menu className="h-5 w-5" />
|
|
290
|
-
</Button>
|
|
291
|
-
) : (
|
|
292
|
-
<div />
|
|
293
|
-
)}
|
|
294
|
-
|
|
295
|
-
{/* Droite : dropdown organisation + user menu */}
|
|
296
|
-
<div className="flex items-center gap-1">
|
|
297
|
-
{loadOrg && loadProjects && onProjectChange && (
|
|
298
|
-
<OrgProjectDropdown
|
|
299
|
-
loadOrg={loadOrg}
|
|
300
|
-
loadProjects={loadProjects}
|
|
301
|
-
currentProject={currentProject}
|
|
302
|
-
onProjectChange={onProjectChange}
|
|
303
|
-
/>
|
|
304
|
-
)}
|
|
305
|
-
<UserMenu
|
|
306
|
-
username={user?.profile?.email || user?.profile?.preferred_username}
|
|
307
|
-
onSignOut={onSignOut || (() => {})}
|
|
308
|
-
menuConfig={effectiveUserMenuConfig}
|
|
309
|
-
/>
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
</header>
|
|
313
|
-
|
|
314
|
-
<main className="flex-1 overflow-auto">
|
|
315
|
-
{children}
|
|
316
|
-
</main>
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export function LayoutContainer(props: LayoutContainerProps) {
|
|
323
|
-
return (
|
|
324
|
-
<SidebarProvider defaultOpen={true}>
|
|
325
|
-
<LayoutContent {...props} />
|
|
326
|
-
</SidebarProvider>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { usePathname, useSearchParams, useParams } from "next/navigation"
|
|
5
|
+
import { MainSidebar, type SidebarMode } from "./sidebar/main-sidebar"
|
|
6
|
+
import { Sidebar, SidebarProvider, useSidebar } from "./sidebar/sidebar"
|
|
7
|
+
import { type SidebarMenus, type MainMenuItem, resolveIcon } from "./sidebar/data"
|
|
8
|
+
import { Button } from "../ui"
|
|
9
|
+
import { getMenuFromPath } from "../../lib/menu-utils"
|
|
10
|
+
import { useIsMobile } from "../../hooks/use-mobile"
|
|
11
|
+
import { Menu } from "lucide-react"
|
|
12
|
+
import { UserPanel } from "./user-panel"
|
|
13
|
+
import { useMenuContext } from "../../context/menu-context"
|
|
14
|
+
|
|
15
|
+
// ─── Layout ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface LayoutContainerProps {
|
|
18
|
+
main_base_url: string
|
|
19
|
+
children: React.ReactNode
|
|
20
|
+
sidebarMenus?: SidebarMenus
|
|
21
|
+
mode?: SidebarMode
|
|
22
|
+
getSidebarMode?: (pathname: string, params: Record<string, string>, searchParams: URLSearchParams) => SidebarMode
|
|
23
|
+
getCurrentMenu?: (pathname: string, searchParams: URLSearchParams) => string
|
|
24
|
+
getCurrentMenuItem?: (pathname: string, searchParams: URLSearchParams) => string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function LayoutContent({
|
|
28
|
+
children,
|
|
29
|
+
sidebarMenus = {},
|
|
30
|
+
mode = 'expanded',
|
|
31
|
+
main_base_url,
|
|
32
|
+
getSidebarMode,
|
|
33
|
+
getCurrentMenu,
|
|
34
|
+
getCurrentMenuItem,
|
|
35
|
+
}: LayoutContainerProps) {
|
|
36
|
+
const { fetchMenus } = useMenuContext()
|
|
37
|
+
const pathname = usePathname()
|
|
38
|
+
const searchParams = useSearchParams()
|
|
39
|
+
const params = useParams()
|
|
40
|
+
const { setOpen } = useSidebar()
|
|
41
|
+
const isMobile = useIsMobile()
|
|
42
|
+
const [isMainSidebarOpen, setIsMainSidebarOpen] = useState(false)
|
|
43
|
+
const [currentMenu, setCurrentMenu] = useState<string>("overview")
|
|
44
|
+
|
|
45
|
+
const effectiveMode = getSidebarMode ? getSidebarMode(pathname, params as Record<string, string>, searchParams) : mode
|
|
46
|
+
const isMinimized = effectiveMode === 'minimized'
|
|
47
|
+
const isHidden = effectiveMode === 'hidden' || isMobile
|
|
48
|
+
|
|
49
|
+
const [mainMenuItems, setMainMenuItems] = useState<MainMenuItem[]>([])
|
|
50
|
+
const [fetchedSidebarMenus, setFetchedSidebarMenus] = useState<SidebarMenus>({})
|
|
51
|
+
const [sectionLabels, setSectionLabels] = useState<Record<string, string>>({})
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!fetchMenus) return
|
|
55
|
+
fetchMenus().then((response) => {
|
|
56
|
+
if (!response.success) return
|
|
57
|
+
const { main, subMenus } = response.data
|
|
58
|
+
|
|
59
|
+
setMainMenuItems(
|
|
60
|
+
main.map((item) => ({
|
|
61
|
+
id: item.id,
|
|
62
|
+
label: item.label,
|
|
63
|
+
icon: resolveIcon(item.icon),
|
|
64
|
+
}))
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const menus: SidebarMenus = {}
|
|
68
|
+
const labels: Record<string, string> = {}
|
|
69
|
+
Object.entries(subMenus).forEach(([key, subMenu]) => {
|
|
70
|
+
menus[key] = subMenu.items.map((item) => ({
|
|
71
|
+
id: item.id,
|
|
72
|
+
name: item.label,
|
|
73
|
+
href: item.href,
|
|
74
|
+
icon: resolveIcon(item.icon),
|
|
75
|
+
}))
|
|
76
|
+
labels[key] = subMenu.header
|
|
77
|
+
})
|
|
78
|
+
setFetchedSidebarMenus(menus)
|
|
79
|
+
setSectionLabels(labels)
|
|
80
|
+
})
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
const effectiveSidebarMenus: SidebarMenus = { ...fetchedSidebarMenus, ...sidebarMenus }
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (isMobile) setIsMainSidebarOpen(false)
|
|
88
|
+
}, [pathname, isMobile])
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const contextualMenu = getCurrentMenu ? getCurrentMenu(pathname, searchParams) : getMenuFromPath(pathname)
|
|
92
|
+
setCurrentMenu(contextualMenu)
|
|
93
|
+
}, [pathname, searchParams, getCurrentMenu])
|
|
94
|
+
|
|
95
|
+
const handleMainSidebarToggle = () => setIsMainSidebarOpen((v) => !v)
|
|
96
|
+
const handleMenuSelect = (menu: string) => setCurrentMenu(menu)
|
|
97
|
+
const handleSecondarySidebarOpen = () => setOpen(true)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="flex h-screen w-full bg-white overflow-visible">
|
|
101
|
+
{!isMinimized && !isHidden && (
|
|
102
|
+
<Sidebar
|
|
103
|
+
currentMenu={currentMenu}
|
|
104
|
+
onMainMenuToggle={handleMainSidebarToggle}
|
|
105
|
+
sidebarMenus={effectiveSidebarMenus}
|
|
106
|
+
main_base_url={main_base_url}
|
|
107
|
+
sectionLabels={sectionLabels}
|
|
108
|
+
getCurrentMenuItem={getCurrentMenuItem}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<MainSidebar
|
|
113
|
+
main_base_url={main_base_url}
|
|
114
|
+
isOpen={isMainSidebarOpen}
|
|
115
|
+
onToggle={handleMainSidebarToggle}
|
|
116
|
+
onMenuSelect={handleMenuSelect}
|
|
117
|
+
currentMenu={currentMenu}
|
|
118
|
+
onSecondarySidebarOpen={handleSecondarySidebarOpen}
|
|
119
|
+
mode={isHidden ? "expanded" : mode}
|
|
120
|
+
sidebarMenus={effectiveSidebarMenus}
|
|
121
|
+
mainMenuItems={mainMenuItems}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
125
|
+
<header className="h-14 bg-gray-50 border-b border-ui-border flex-shrink-0">
|
|
126
|
+
<div className="h-full px-4 md:px-6 flex items-center justify-between">
|
|
127
|
+
{isHidden ? (
|
|
128
|
+
<Button
|
|
129
|
+
variant="ghost"
|
|
130
|
+
size="sm"
|
|
131
|
+
onClick={handleMainSidebarToggle}
|
|
132
|
+
className="h-8 w-8 p-0 hover:bg-ui-background text-text-secondary"
|
|
133
|
+
title="Ouvrir le menu"
|
|
134
|
+
>
|
|
135
|
+
<Menu className="h-5 w-5" />
|
|
136
|
+
</Button>
|
|
137
|
+
) : (
|
|
138
|
+
<div />
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
<UserPanel main_base_url={main_base_url} />
|
|
142
|
+
</div>
|
|
143
|
+
</header>
|
|
144
|
+
|
|
145
|
+
<main className="flex-1 overflow-auto">
|
|
146
|
+
{children}
|
|
147
|
+
</main>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function LayoutContainer(props: LayoutContainerProps) {
|
|
154
|
+
return (
|
|
155
|
+
<SidebarProvider defaultOpen={true}>
|
|
156
|
+
<LayoutContent {...props} />
|
|
157
|
+
</SidebarProvider>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -18,15 +18,9 @@ import {
|
|
|
18
18
|
TooltipProvider,
|
|
19
19
|
TooltipTrigger,
|
|
20
20
|
} from "../../ui/tooltip"
|
|
21
|
-
import {
|
|
22
|
-
Settings,
|
|
23
|
-
Menu,
|
|
24
|
-
User,
|
|
25
|
-
ChevronUp,
|
|
26
|
-
ChevronRight,
|
|
27
|
-
} from "lucide-react"
|
|
21
|
+
import { Menu } from "lucide-react"
|
|
28
22
|
import { Logo } from "../../ui/logo"
|
|
29
|
-
import { type SidebarMenus, type SubMenuItem
|
|
23
|
+
import { type SidebarMenus, type SubMenuItem } from "./data"
|
|
30
24
|
import { Skeleton } from "../skeleton"
|
|
31
25
|
|
|
32
26
|
const SIDEBAR_COOKIE_NAME = "sidebar:state"
|
|
@@ -173,42 +167,14 @@ interface SidebarProps {
|
|
|
173
167
|
main_base_url?: string
|
|
174
168
|
sectionLabels?: Record<string, string>
|
|
175
169
|
getCurrentMenuItem?: (pathname: string, searchParams: URLSearchParams) => string
|
|
176
|
-
loadOrg?: () => Promise<OrgInfo>
|
|
177
170
|
}
|
|
178
171
|
|
|
179
172
|
|
|
180
173
|
|
|
181
|
-
function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem
|
|
174
|
+
function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem }: SidebarProps = {}) {
|
|
182
175
|
const pathname = usePathname()
|
|
183
176
|
const searchParams = useSearchParams()
|
|
184
177
|
const { state } = useSidebar()
|
|
185
|
-
const [settingsOpen, setSettingsOpen] = React.useState(false)
|
|
186
|
-
const settingsRef = React.useRef<HTMLDivElement>(null)
|
|
187
|
-
const [orgData, setOrgData] = React.useState<OrgInfo | null>(null)
|
|
188
|
-
const [orgLoading, setOrgLoading] = React.useState(false)
|
|
189
|
-
|
|
190
|
-
// Load org lazily when settings panel opens
|
|
191
|
-
React.useEffect(() => {
|
|
192
|
-
if (settingsOpen && loadOrg && !orgData && !orgLoading) {
|
|
193
|
-
setOrgLoading(true)
|
|
194
|
-
loadOrg()
|
|
195
|
-
.then((data) => { setOrgData(data); setOrgLoading(false) })
|
|
196
|
-
.catch(() => setOrgLoading(false))
|
|
197
|
-
}
|
|
198
|
-
}, [settingsOpen, loadOrg, orgData, orgLoading])
|
|
199
|
-
|
|
200
|
-
// Close dropdown on click outside
|
|
201
|
-
React.useEffect(() => {
|
|
202
|
-
function handleClickOutside(event: MouseEvent) {
|
|
203
|
-
if (settingsRef.current && !settingsRef.current.contains(event.target as Node)) {
|
|
204
|
-
setSettingsOpen(false)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (settingsOpen) {
|
|
208
|
-
document.addEventListener("mousedown", handleClickOutside)
|
|
209
|
-
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
210
|
-
}
|
|
211
|
-
}, [settingsOpen])
|
|
212
178
|
|
|
213
179
|
// Micro apps: utilise 'items' comme clé standard
|
|
214
180
|
// Main app: utilise currentMenu pour sélectionner le bon groupe
|
|
@@ -278,66 +244,6 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
278
244
|
})}
|
|
279
245
|
</nav>
|
|
280
246
|
|
|
281
|
-
{/* Footer - Menu Paramètres */}
|
|
282
|
-
<div className="relative border-t border-ui-border px-3 py-3" ref={settingsRef}>
|
|
283
|
-
{/* Dropdown vers le haut */}
|
|
284
|
-
{settingsOpen && (
|
|
285
|
-
<div className="absolute bottom-full left-0 mb-3 bg-white border border-ui-border shadow-2xl z-50 min-w-[380px]">
|
|
286
|
-
{/* Flèche pointant vers le bas en direction du bouton Paramètres */}
|
|
287
|
-
<div className="absolute -bottom-[5px] left-6 w-[10px] h-[10px] bg-white border-r border-b border-ui-border rotate-45" />
|
|
288
|
-
|
|
289
|
-
{/* Organisation */}
|
|
290
|
-
<Link
|
|
291
|
-
href={`${main_base_url}/organization`}
|
|
292
|
-
onClick={() => setSettingsOpen(false)}
|
|
293
|
-
className="flex items-center gap-4 px-5 py-4 hover:bg-ui-background transition-colors border-b border-ui-border no-underline"
|
|
294
|
-
>
|
|
295
|
-
{orgLoading ? (
|
|
296
|
-
<div className="h-14 w-14 bg-ui-background animate-pulse flex-shrink-0" />
|
|
297
|
-
) : orgData?.logo ? (
|
|
298
|
-
<img src={orgData.logo} alt={orgData.name } className="h-14 w-14 object-cover flex-shrink-0" />
|
|
299
|
-
) : (
|
|
300
|
-
<div className="h-14 w-14 bg-interactive flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
|
|
301
|
-
{orgData?.name?.charAt(0)?.toUpperCase() ?? 'O'}
|
|
302
|
-
</div>
|
|
303
|
-
)}
|
|
304
|
-
<div className="flex flex-col min-w-0 flex-1">
|
|
305
|
-
<span className="text-xs text-text-secondary uppercase tracking-wide mb-1">Organisation</span>
|
|
306
|
-
<span className="text-base font-semibold text-text-primary truncate">
|
|
307
|
-
{orgLoading ? '…' : (orgData?.name ?? 'Organisation')}
|
|
308
|
-
</span>
|
|
309
|
-
</div>
|
|
310
|
-
<ChevronRight className="h-4 w-4 text-text-secondary flex-shrink-0 opacity-40" />
|
|
311
|
-
</Link>
|
|
312
|
-
|
|
313
|
-
{/* Paramètres du compte */}
|
|
314
|
-
<Link
|
|
315
|
-
href={`${main_base_url}/profile`}
|
|
316
|
-
onClick={() => setSettingsOpen(false)}
|
|
317
|
-
className="flex items-center gap-3 px-5 py-3 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline"
|
|
318
|
-
>
|
|
319
|
-
<User className="h-5 w-5 flex-shrink-0" />
|
|
320
|
-
<span className="flex-1">Paramètres du compte</span>
|
|
321
|
-
<ChevronRight className="h-4 w-4 flex-shrink-0 opacity-40" />
|
|
322
|
-
</Link>
|
|
323
|
-
</div>
|
|
324
|
-
)}
|
|
325
|
-
<button
|
|
326
|
-
onClick={() => setSettingsOpen(!settingsOpen)}
|
|
327
|
-
className={cn(
|
|
328
|
-
"flex items-center justify-between w-full px-3 py-2 text-sm rounded-none gap-x-3 transition-colors",
|
|
329
|
-
settingsOpen
|
|
330
|
-
? "bg-interactive/10 text-interactive font-medium"
|
|
331
|
-
: "text-text-secondary hover:bg-ui-background hover:text-text-primary"
|
|
332
|
-
)}
|
|
333
|
-
>
|
|
334
|
-
<span className="flex items-center gap-x-3">
|
|
335
|
-
<Settings className="h-4 w-4" />
|
|
336
|
-
{state === "expanded" && "Paramètres"}
|
|
337
|
-
</span>
|
|
338
|
-
{state === "expanded" && <ChevronUp className={cn("h-4 w-4 transition-transform", settingsOpen ? "rotate-0" : "rotate-180")} />}
|
|
339
|
-
</button>
|
|
340
|
-
</div>
|
|
341
247
|
</div>
|
|
342
248
|
)
|
|
343
249
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { ChevronRight, LogOut } from "lucide-react"
|
|
5
|
+
import ReactAvatar from "react-avatar"
|
|
6
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
7
|
+
import { useUserContext } from "../../context/user-context"
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "../ui/dropdown-menu"
|
|
13
|
+
import { Button } from "../ui"
|
|
14
|
+
|
|
15
|
+
const Divider = () => <div className="h-px bg-gray-200 w-full" />
|
|
16
|
+
|
|
17
|
+
interface UserPanelProps {
|
|
18
|
+
main_base_url: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function UserPanel({ main_base_url }: UserPanelProps) {
|
|
22
|
+
const [open, setOpen] = useState(false)
|
|
23
|
+
const { profile, currentProject, organization, logout, openBusinessUnitSwitcher } = useUserContext()
|
|
24
|
+
|
|
25
|
+
const displayName =
|
|
26
|
+
profile?.name || profile?.preferred_username || profile?.email || "User"
|
|
27
|
+
|
|
28
|
+
const close = () => setOpen(false)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
32
|
+
<DropdownMenuTrigger asChild>
|
|
33
|
+
<Button
|
|
34
|
+
variant="ghost"
|
|
35
|
+
size="sm"
|
|
36
|
+
className="flex items-center h-9 px-2"
|
|
37
|
+
style={{ borderRadius: 0 }}
|
|
38
|
+
>
|
|
39
|
+
<ReactAvatar
|
|
40
|
+
name={displayName}
|
|
41
|
+
src={profile?.avatar}
|
|
42
|
+
size="28"
|
|
43
|
+
round
|
|
44
|
+
color="#0f62fe"
|
|
45
|
+
textSizeRatio={2}
|
|
46
|
+
/>
|
|
47
|
+
</Button>
|
|
48
|
+
</DropdownMenuTrigger>
|
|
49
|
+
|
|
50
|
+
<DropdownMenuContent
|
|
51
|
+
align="end"
|
|
52
|
+
sideOffset={8}
|
|
53
|
+
className="w-80 bg-white shadow-xl border border-gray-200 p-0 rounded-none"
|
|
54
|
+
>
|
|
55
|
+
<DropdownMenuPrimitive.Arrow width={16} height={8} style={{ fill: 'white', stroke: '#e5e7eb', strokeWidth: 1 }} />
|
|
56
|
+
|
|
57
|
+
{/* Organisation | Business Unit */}
|
|
58
|
+
<div className="flex items-stretch">
|
|
59
|
+
|
|
60
|
+
{/* Gauche — Organisation */}
|
|
61
|
+
<a
|
|
62
|
+
href={`${main_base_url}/organization`}
|
|
63
|
+
onClick={close}
|
|
64
|
+
className="flex items-center gap-2 flex-1 min-w-0 px-4 py-4 hover:bg-gray-50 transition-colors no-underline group"
|
|
65
|
+
>
|
|
66
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
67
|
+
<span className="text-[11px] text-gray-400 uppercase tracking-widest mb-1">Organisation</span>
|
|
68
|
+
<span className="text-sm font-semibold text-gray-900 truncate">
|
|
69
|
+
{organization?.name ?? '—'}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
<ChevronRight className="h-4 w-4 text-gray-300 flex-shrink-0 group-hover:text-gray-500 transition-colors" />
|
|
73
|
+
</a>
|
|
74
|
+
|
|
75
|
+
{/* Séparateur vertical */}
|
|
76
|
+
<div className="flex items-center py-3">
|
|
77
|
+
<div className="w-px h-full bg-gray-200" />
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Droite — Business Unit */}
|
|
81
|
+
<div className="flex flex-col justify-center flex-1 min-w-0 px-4 py-4">
|
|
82
|
+
<span className="text-[11px] text-gray-400 uppercase tracking-widest mb-1">Business Unit</span>
|
|
83
|
+
<span className="text-sm font-semibold text-gray-900 truncate">
|
|
84
|
+
{currentProject?.name ?? '—'}
|
|
85
|
+
</span>
|
|
86
|
+
<div className="flex items-center gap-3 mt-1.5">
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => { close(); openBusinessUnitSwitcher() }}
|
|
89
|
+
className="text-xs text-[#0f62fe] hover:underline"
|
|
90
|
+
>
|
|
91
|
+
Switch
|
|
92
|
+
</button>
|
|
93
|
+
{currentProject && organization && (
|
|
94
|
+
<a
|
|
95
|
+
href={`${main_base_url}/${organization.id}/projects/${currentProject.id}`}
|
|
96
|
+
onClick={close}
|
|
97
|
+
className="text-xs text-[#0f62fe] hover:underline"
|
|
98
|
+
>
|
|
99
|
+
Settings
|
|
100
|
+
</a>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<Divider />
|
|
108
|
+
|
|
109
|
+
{/* User info + Logout */}
|
|
110
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
111
|
+
<ReactAvatar
|
|
112
|
+
name={displayName}
|
|
113
|
+
src={profile?.avatar}
|
|
114
|
+
size="40"
|
|
115
|
+
round
|
|
116
|
+
color="#0f62fe"
|
|
117
|
+
textSizeRatio={2}
|
|
118
|
+
/>
|
|
119
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
120
|
+
<span className="font-bold text-sm text-gray-900 truncate">{displayName}</span>
|
|
121
|
+
{profile?.email && (
|
|
122
|
+
<span className="text-xs text-gray-500 truncate">{profile.email}</span>
|
|
123
|
+
)}
|
|
124
|
+
<a
|
|
125
|
+
href={`${main_base_url}/profile`}
|
|
126
|
+
onClick={close}
|
|
127
|
+
className="text-xs text-[#0f62fe] hover:underline mt-0.5 w-fit"
|
|
128
|
+
>
|
|
129
|
+
Settings
|
|
130
|
+
</a>
|
|
131
|
+
</div>
|
|
132
|
+
<button
|
|
133
|
+
onClick={() => { close(); logout() }}
|
|
134
|
+
className="flex items-center gap-1.5 text-xs text-red-600 hover:text-red-700 flex-shrink-0 ml-2"
|
|
135
|
+
title="Logout"
|
|
136
|
+
>
|
|
137
|
+
<LogOut className="h-3.5 w-3.5" />
|
|
138
|
+
<span>Logout</span>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
</DropdownMenuContent>
|
|
143
|
+
</DropdownMenu>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -21,23 +21,21 @@ import {
|
|
|
21
21
|
import { Loader2, FolderKanban } from "lucide-react"
|
|
22
22
|
|
|
23
23
|
export interface Project {
|
|
24
|
-
id: string
|
|
24
|
+
id: string
|
|
25
25
|
name: string
|
|
26
26
|
alias?: string
|
|
27
27
|
description?: string
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export
|
|
31
|
-
open: boolean
|
|
32
|
-
onOpenChange?: (open: boolean) => void
|
|
30
|
+
export type ProjectSelectorProps = {
|
|
33
31
|
getProjects: () => Promise<Project[]>
|
|
34
32
|
createProject: (name: string) => Promise<Project>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
doInit: (id: string) => void | Promise<void>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ProjectSelectorModalProps extends ProjectSelectorProps {
|
|
37
|
+
open: boolean
|
|
38
|
+
onOpenChange?: (open: boolean) => void
|
|
41
39
|
title?: string
|
|
42
40
|
description?: string
|
|
43
41
|
}
|
|
@@ -45,13 +43,11 @@ export interface ProjectSelectorModalProps {
|
|
|
45
43
|
export function ProjectSelectorModal({
|
|
46
44
|
open,
|
|
47
45
|
onOpenChange,
|
|
46
|
+
title = "Select Business Unit",
|
|
47
|
+
description = "Choose a business unit to continue. This will be used for all operations in this application.",
|
|
48
48
|
getProjects,
|
|
49
49
|
createProject,
|
|
50
|
-
storage,
|
|
51
|
-
storageKey,
|
|
52
50
|
doInit,
|
|
53
|
-
title = "Select Business Unit",
|
|
54
|
-
description = "Choose a business unit to continue. This will be used for all operations in this application.",
|
|
55
51
|
}: ProjectSelectorModalProps) {
|
|
56
52
|
const [projects, setProjects] = React.useState<Project[]>([])
|
|
57
53
|
const [loading, setLoading] = React.useState(true)
|
|
@@ -63,13 +59,8 @@ export function ProjectSelectorModal({
|
|
|
63
59
|
React.useEffect(() => {
|
|
64
60
|
if (open) {
|
|
65
61
|
loadProjects()
|
|
66
|
-
// Try to load current project from storage
|
|
67
|
-
const currentProject = storage.getItem(storageKey)
|
|
68
|
-
if (currentProject) {
|
|
69
|
-
setSelectedProject(currentProject)
|
|
70
|
-
}
|
|
71
62
|
}
|
|
72
|
-
}, [open
|
|
63
|
+
}, [open])
|
|
73
64
|
|
|
74
65
|
const loadProjects = async () => {
|
|
75
66
|
setLoading(true)
|
|
@@ -107,11 +98,8 @@ export function ProjectSelectorModal({
|
|
|
107
98
|
// Create the new project
|
|
108
99
|
const newProject = await createProject(newProjectName.trim())
|
|
109
100
|
|
|
110
|
-
// Save to storage
|
|
111
|
-
storage.setItem(storageKey, newProject.name)
|
|
112
|
-
|
|
113
101
|
// Call doInit
|
|
114
|
-
await Promise.resolve(doInit())
|
|
102
|
+
await Promise.resolve(doInit(newProject.id))
|
|
115
103
|
|
|
116
104
|
// Close modal
|
|
117
105
|
onOpenChange?.(false)
|
|
@@ -132,11 +120,8 @@ export function ProjectSelectorModal({
|
|
|
132
120
|
setError(null)
|
|
133
121
|
|
|
134
122
|
try {
|
|
135
|
-
// Save to storage
|
|
136
|
-
storage.setItem(storageKey, selectedProject)
|
|
137
|
-
|
|
138
123
|
// Call doInit
|
|
139
|
-
await Promise.resolve(doInit())
|
|
124
|
+
await Promise.resolve(doInit(selectedProject))
|
|
140
125
|
|
|
141
126
|
// Close modal
|
|
142
127
|
onOpenChange?.(false)
|
package/context/index.tsx
CHANGED
|
@@ -36,4 +36,7 @@ type workflowEditContext = {
|
|
|
36
36
|
stepName?: string;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export const WorkflowEditContext = React.createContext<workflowEditContext>({})
|
|
39
|
+
export const WorkflowEditContext = React.createContext<workflowEditContext>({})
|
|
40
|
+
|
|
41
|
+
export { useUserContext, UserContextProvider, type UserProfile } from './user-context'
|
|
42
|
+
export { useMenuContext, MenuContextProvider } from './menu-context';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext } from "react"
|
|
4
|
+
import type { MenuApiResponse } from "../components/layout/sidebar/data"
|
|
5
|
+
|
|
6
|
+
interface MenuContextValue {
|
|
7
|
+
fetchMenus: () => Promise<MenuApiResponse>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MenuContext = createContext<MenuContextValue | null>(null)
|
|
11
|
+
|
|
12
|
+
export function useMenuContext(): MenuContextValue {
|
|
13
|
+
const ctx = useContext(MenuContext)
|
|
14
|
+
if (!ctx) throw new Error("useMenuContext must be used within MenuContextProvider")
|
|
15
|
+
return ctx
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface MenuContextProviderProps {
|
|
19
|
+
children: React.ReactNode
|
|
20
|
+
fetchMenus: () => Promise<MenuApiResponse>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MenuContextProvider({ children, fetchMenus }: MenuContextProviderProps) {
|
|
24
|
+
return (
|
|
25
|
+
<MenuContext.Provider value={{ fetchMenus }}>
|
|
26
|
+
{children}
|
|
27
|
+
</MenuContext.Provider>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
|
|
3
|
+
export interface Project {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
alias?: string
|
|
7
|
+
description?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProjectSelectorContextType {
|
|
11
|
+
getProjects: () => Promise<Project[]>
|
|
12
|
+
createProject: (name: string) => Promise<Project>
|
|
13
|
+
doInit: () => void | Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ProjectSelectorContext = createContext<ProjectSelectorContextType | undefined>(undefined)
|
|
17
|
+
|
|
18
|
+
export const useProjectSelector = () => {
|
|
19
|
+
const context = useContext(ProjectSelectorContext)
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error('useProjectSelector must be used within a ProjectSelectorProvider')
|
|
22
|
+
}
|
|
23
|
+
return context
|
|
24
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext } from "react"
|
|
4
|
+
import type { OrgInfo, Project } from "../components/layout/sidebar/data"
|
|
5
|
+
|
|
6
|
+
export interface UserProfile {
|
|
7
|
+
email?: string
|
|
8
|
+
preferred_username?: string
|
|
9
|
+
name?: string
|
|
10
|
+
avatar?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface UserContextValue {
|
|
14
|
+
profile: UserProfile | null
|
|
15
|
+
currentProject: Project | null
|
|
16
|
+
organization: OrgInfo | null
|
|
17
|
+
logout: () => void
|
|
18
|
+
switchBusinessUnit: (id: string) => void
|
|
19
|
+
openBusinessUnitSwitcher: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const UserContext = createContext<UserContextValue | null>(null)
|
|
23
|
+
|
|
24
|
+
export function useUserContext(): UserContextValue {
|
|
25
|
+
const ctx = useContext(UserContext)
|
|
26
|
+
if (!ctx) throw new Error("useUserContext must be used within UserContextProvider")
|
|
27
|
+
return ctx
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface UserContextProviderProps {
|
|
31
|
+
children: React.ReactNode
|
|
32
|
+
profile?: UserProfile | null
|
|
33
|
+
currentProject?: Project | null
|
|
34
|
+
organization?: OrgInfo | null
|
|
35
|
+
onSignOut?: () => void
|
|
36
|
+
onProjectChange?: (id: string) => void
|
|
37
|
+
onOpenBusinessUnitSwitcher?: () => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function UserContextProvider({
|
|
41
|
+
children,
|
|
42
|
+
profile,
|
|
43
|
+
currentProject = null,
|
|
44
|
+
organization = null,
|
|
45
|
+
onSignOut,
|
|
46
|
+
onProjectChange,
|
|
47
|
+
onOpenBusinessUnitSwitcher,
|
|
48
|
+
}: UserContextProviderProps) {
|
|
49
|
+
return (
|
|
50
|
+
<UserContext.Provider value={{
|
|
51
|
+
profile: profile ?? null,
|
|
52
|
+
currentProject,
|
|
53
|
+
organization,
|
|
54
|
+
logout: onSignOut ?? (() => {}),
|
|
55
|
+
switchBusinessUnit: onProjectChange ?? (() => {}),
|
|
56
|
+
openBusinessUnitSwitcher: onOpenBusinessUnitSwitcher ?? (() => {}),
|
|
57
|
+
}}>
|
|
58
|
+
{children}
|
|
59
|
+
</UserContext.Provider>
|
|
60
|
+
)
|
|
61
|
+
}
|
package/lib/interceptors.ts
CHANGED
|
@@ -6,16 +6,8 @@ export const interceptors: Interceptors = {
|
|
|
6
6
|
return response;
|
|
7
7
|
},
|
|
8
8
|
onResponseError: (error: any) => {
|
|
9
|
-
if (error.response?.status) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (typeof window !== 'undefined') {
|
|
13
|
-
switch (status) {
|
|
14
|
-
case 401:
|
|
15
|
-
window.location.href = '/login';
|
|
16
|
-
break;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
9
|
+
if (error.response?.status === 401 && typeof window !== 'undefined') {
|
|
10
|
+
window.dispatchEvent(new CustomEvent('http-unauthorized'));
|
|
19
11
|
}
|
|
20
12
|
return Promise.reject(error);
|
|
21
13
|
}
|