@vendure/dashboard 3.5.3-master-202601070244 → 3.5.3-master-202601100240
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/dist/vite/vite-plugin-lingui-babel.d.ts +27 -0
- package/dist/vite/vite-plugin-lingui-babel.js +86 -0
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +1 -0
- package/dist/vite/vite-plugin-vendure-dashboard.js +12 -4
- package/package.json +12 -11
- package/src/lib/components/layout/channel-switcher.tsx +97 -86
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +7 -1
- package/src/lib/providers/channel-provider.tsx +32 -7
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* @description
|
|
4
|
+
* A custom Vite plugin that transforms Lingui macros in files using Babel instead of SWC.
|
|
5
|
+
*
|
|
6
|
+
* This plugin solves a critical compatibility issue with SWC plugins:
|
|
7
|
+
* - SWC plugins are compiled Wasm binaries that require exact version matching with `@swc/core`
|
|
8
|
+
* - When users have different SWC versions in their projects (e.g., from Next.js, Nx, etc.),
|
|
9
|
+
* the Lingui SWC plugin fails with "failed to invoke plugin" errors
|
|
10
|
+
* - Babel has no such binary compatibility issues, making it much more reliable for library code
|
|
11
|
+
*
|
|
12
|
+
* The plugin runs BEFORE `@vitejs/plugin-react` and transforms files containing Lingui macros
|
|
13
|
+
* (imports from `@lingui/core/macro` or `@lingui/react/macro`) using the Babel-based
|
|
14
|
+
* `@lingui/babel-plugin-lingui-macro`.
|
|
15
|
+
*
|
|
16
|
+
* Files processed:
|
|
17
|
+
* - `@vendure/dashboard/src` files (in node_modules for external projects)
|
|
18
|
+
* - `packages/dashboard/src` files (in monorepo development)
|
|
19
|
+
* - User's dashboard extension files (e.g., custom plugins using Lingui)
|
|
20
|
+
*
|
|
21
|
+
* Files NOT processed:
|
|
22
|
+
* - Other node_modules packages (they shouldn't contain Lingui macros)
|
|
23
|
+
*
|
|
24
|
+
* @see https://github.com/vendurehq/vendure/issues/3929
|
|
25
|
+
* @see https://github.com/lingui/swc-plugin/issues/179
|
|
26
|
+
*/
|
|
27
|
+
export declare function linguiBabelPlugin(): Plugin;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as babel from '@babel/core';
|
|
2
|
+
/**
|
|
3
|
+
* @description
|
|
4
|
+
* A custom Vite plugin that transforms Lingui macros in files using Babel instead of SWC.
|
|
5
|
+
*
|
|
6
|
+
* This plugin solves a critical compatibility issue with SWC plugins:
|
|
7
|
+
* - SWC plugins are compiled Wasm binaries that require exact version matching with `@swc/core`
|
|
8
|
+
* - When users have different SWC versions in their projects (e.g., from Next.js, Nx, etc.),
|
|
9
|
+
* the Lingui SWC plugin fails with "failed to invoke plugin" errors
|
|
10
|
+
* - Babel has no such binary compatibility issues, making it much more reliable for library code
|
|
11
|
+
*
|
|
12
|
+
* The plugin runs BEFORE `@vitejs/plugin-react` and transforms files containing Lingui macros
|
|
13
|
+
* (imports from `@lingui/core/macro` or `@lingui/react/macro`) using the Babel-based
|
|
14
|
+
* `@lingui/babel-plugin-lingui-macro`.
|
|
15
|
+
*
|
|
16
|
+
* Files processed:
|
|
17
|
+
* - `@vendure/dashboard/src` files (in node_modules for external projects)
|
|
18
|
+
* - `packages/dashboard/src` files (in monorepo development)
|
|
19
|
+
* - User's dashboard extension files (e.g., custom plugins using Lingui)
|
|
20
|
+
*
|
|
21
|
+
* Files NOT processed:
|
|
22
|
+
* - Other node_modules packages (they shouldn't contain Lingui macros)
|
|
23
|
+
*
|
|
24
|
+
* @see https://github.com/vendurehq/vendure/issues/3929
|
|
25
|
+
* @see https://github.com/lingui/swc-plugin/issues/179
|
|
26
|
+
*/
|
|
27
|
+
export function linguiBabelPlugin() {
|
|
28
|
+
return {
|
|
29
|
+
name: 'vendure:lingui-babel',
|
|
30
|
+
// Run BEFORE @vitejs/plugin-react so the macros are already transformed
|
|
31
|
+
// when the react plugin processes the file
|
|
32
|
+
enforce: 'pre',
|
|
33
|
+
async transform(code, id) {
|
|
34
|
+
// Strip query params for path matching (Vite adds ?v=xxx for cache busting)
|
|
35
|
+
const cleanId = id.split('?')[0];
|
|
36
|
+
// Only process TypeScript/JavaScript files
|
|
37
|
+
if (!/\.[tj]sx?$/.test(cleanId)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// Only process files that actually contain Lingui macro imports
|
|
41
|
+
// This is a fast check to avoid running Babel on files that don't need it
|
|
42
|
+
if (!code.includes('@lingui/') || !code.includes('/macro')) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Skip node_modules files EXCEPT for @vendure/dashboard source
|
|
46
|
+
// This ensures:
|
|
47
|
+
// 1. Dashboard source files get transformed (both in monorepo and external projects)
|
|
48
|
+
// 2. User's extension files get transformed (not in node_modules)
|
|
49
|
+
// 3. Other node_modules packages are left alone
|
|
50
|
+
if (cleanId.includes('node_modules')) {
|
|
51
|
+
const isVendureDashboard = cleanId.includes('@vendure/dashboard/src') || cleanId.includes('packages/dashboard/src');
|
|
52
|
+
if (!isVendureDashboard) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const result = await babel.transformAsync(code, {
|
|
58
|
+
filename: id,
|
|
59
|
+
presets: [
|
|
60
|
+
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
|
|
61
|
+
['@babel/preset-react', { runtime: 'automatic' }],
|
|
62
|
+
],
|
|
63
|
+
plugins: ['@lingui/babel-plugin-lingui-macro'],
|
|
64
|
+
sourceMaps: true,
|
|
65
|
+
// Don't look for babel config files - we want to control the config completely
|
|
66
|
+
configFile: false,
|
|
67
|
+
babelrc: false,
|
|
68
|
+
});
|
|
69
|
+
if (!(result === null || result === void 0 ? void 0 : result.code)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
code: result.code,
|
|
74
|
+
map: result.map,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Log the error but don't crash - let the build continue
|
|
79
|
+
// The lingui vite plugin will catch untransformed macros later
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
console.error(`[vendure:lingui-babel] Failed to transform ${id}:`, error);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { lingui } from '@lingui/vite-plugin';
|
|
2
2
|
import tailwindcss from '@tailwindcss/vite';
|
|
3
3
|
import { tanstackRouter } from '@tanstack/router-plugin/vite';
|
|
4
|
-
import react from '@vitejs/plugin-react
|
|
4
|
+
import react from '@vitejs/plugin-react';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { adminApiSchemaPlugin } from './vite-plugin-admin-api-schema.js';
|
|
7
7
|
import { configLoaderPlugin } from './vite-plugin-config-loader.js';
|
|
@@ -9,6 +9,7 @@ import { viteConfigPlugin } from './vite-plugin-config.js';
|
|
|
9
9
|
import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
|
|
10
10
|
import { gqlTadaPlugin } from './vite-plugin-gql-tada.js';
|
|
11
11
|
import { hmrPlugin } from './vite-plugin-hmr.js';
|
|
12
|
+
import { linguiBabelPlugin } from './vite-plugin-lingui-babel.js';
|
|
12
13
|
import { dashboardTailwindSourcePlugin } from './vite-plugin-tailwind-source.js';
|
|
13
14
|
import { themeVariablesPlugin } from './vite-plugin-theme.js';
|
|
14
15
|
import { transformIndexHtmlPlugin } from './vite-plugin-transform-index.js';
|
|
@@ -47,11 +48,18 @@ export function vendureDashboardPlugin(options) {
|
|
|
47
48
|
generatedRouteTree: path.join(packageRoot, 'src/app/routeTree.gen.ts'),
|
|
48
49
|
}),
|
|
49
50
|
},
|
|
51
|
+
{
|
|
52
|
+
// Custom plugin that transforms Lingui macros using Babel instead of SWC.
|
|
53
|
+
// This runs BEFORE the react plugin to ensure macros are transformed first.
|
|
54
|
+
// Using Babel eliminates the SWC binary compatibility issues that caused
|
|
55
|
+
// "failed to invoke plugin" errors in external projects.
|
|
56
|
+
// See: https://github.com/vendurehq/vendure/issues/3929
|
|
57
|
+
key: 'linguiBabel',
|
|
58
|
+
plugin: () => linguiBabelPlugin(),
|
|
59
|
+
},
|
|
50
60
|
{
|
|
51
61
|
key: 'react',
|
|
52
|
-
plugin: () => react(
|
|
53
|
-
plugins: [['@lingui/swc-plugin', {}]],
|
|
54
|
-
}),
|
|
62
|
+
plugin: () => react(),
|
|
55
63
|
},
|
|
56
64
|
{
|
|
57
65
|
key: 'lingui',
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.5.3-master-
|
|
4
|
+
"version": "3.5.3-master-202601100240",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/
|
|
8
|
+
"url": "https://github.com/vendurehq/vendure"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://www.vendure.io",
|
|
11
11
|
"funding": "https://github.com/sponsors/michaelbromley",
|
|
@@ -57,15 +57,18 @@
|
|
|
57
57
|
"index.html"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"@babel/core": "^7.26.0",
|
|
61
|
+
"@babel/preset-react": "^7.26.3",
|
|
62
|
+
"@babel/preset-typescript": "^7.26.0",
|
|
60
63
|
"@dnd-kit/core": "^6.3.1",
|
|
61
64
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
62
65
|
"@dnd-kit/sortable": "^10.0.0",
|
|
63
66
|
"@hookform/resolvers": "^4.1.3",
|
|
64
|
-
"@lingui/
|
|
65
|
-
"@lingui/
|
|
66
|
-
"@lingui/
|
|
67
|
-
"@lingui/
|
|
68
|
-
"@lingui/vite-plugin": "^5.
|
|
67
|
+
"@lingui/babel-plugin-lingui-macro": "^5.7.0",
|
|
68
|
+
"@lingui/cli": "^5.7.0",
|
|
69
|
+
"@lingui/core": "^5.7.0",
|
|
70
|
+
"@lingui/react": "^5.7.0",
|
|
71
|
+
"@lingui/vite-plugin": "^5.7.0",
|
|
69
72
|
"@radix-ui/react-accordion": "^1.2.11",
|
|
70
73
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
|
71
74
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
|
@@ -92,7 +95,6 @@
|
|
|
92
95
|
"@radix-ui/react-toggle": "^1.1.9",
|
|
93
96
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
|
94
97
|
"@radix-ui/react-tooltip": "^1.2.7",
|
|
95
|
-
"@swc/core": "1.13.5",
|
|
96
98
|
"@tailwindcss/vite": "^4.1.5",
|
|
97
99
|
"@tanstack/eslint-plugin-query": "^5.66.1",
|
|
98
100
|
"@tanstack/react-query": "^5.66.7",
|
|
@@ -112,7 +114,6 @@
|
|
|
112
114
|
"@types/react-dom": "^19.0.4",
|
|
113
115
|
"@uidotdev/usehooks": "^2.4.1",
|
|
114
116
|
"@vitejs/plugin-react": "^5.0.4",
|
|
115
|
-
"@vitejs/plugin-react-swc": "^4.1.0",
|
|
116
117
|
"acorn": "^8.11.3",
|
|
117
118
|
"acorn-walk": "^8.3.2",
|
|
118
119
|
"awesome-graphql-client": "^2.1.0",
|
|
@@ -156,8 +157,8 @@
|
|
|
156
157
|
"@storybook/addon-vitest": "^10.0.0-beta.9",
|
|
157
158
|
"@storybook/react-vite": "^10.0.0-beta.9",
|
|
158
159
|
"@types/node": "^22.13.4",
|
|
159
|
-
"@vendure/common": "^3.5.3-master-
|
|
160
|
-
"@vendure/core": "^3.5.3-master-
|
|
160
|
+
"@vendure/common": "^3.5.3-master-202601100240",
|
|
161
|
+
"@vendure/core": "^3.5.3-master-202601100240",
|
|
161
162
|
"@vitest/browser": "^3.2.4",
|
|
162
163
|
"@vitest/coverage-v8": "^3.2.4",
|
|
163
164
|
"eslint": "^9.19.0",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChevronsUpDown, Languages, Plus } from 'lucide-react';
|
|
2
2
|
|
|
3
3
|
import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
|
|
4
|
+
import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
|
|
4
5
|
import {
|
|
5
6
|
DropdownMenu,
|
|
6
7
|
DropdownMenuContent,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
DropdownMenuTrigger,
|
|
14
15
|
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
15
16
|
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/vdb/components/ui/sidebar.js';
|
|
17
|
+
import { ScrollArea } from '@/vdb/components/ui/scroll-area.js';
|
|
16
18
|
import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
|
|
17
19
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
18
20
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
@@ -61,9 +63,9 @@ export function ChannelSwitcher() {
|
|
|
61
63
|
const availableLanguages = serverConfig?.availableLanguages || [];
|
|
62
64
|
const hasMultipleLanguages = availableLanguages.length > 1;
|
|
63
65
|
|
|
64
|
-
//
|
|
66
|
+
// Currently selected channel is displayed separately so filter it out of the list
|
|
65
67
|
const orderedChannels = displayChannel
|
|
66
|
-
?
|
|
68
|
+
? channels.filter(ch => ch.id !== displayChannel.id)
|
|
67
69
|
: channels;
|
|
68
70
|
|
|
69
71
|
// Sort language codes by their formatted names and map to code and label
|
|
@@ -79,6 +81,32 @@ export function ChannelSwitcher() {
|
|
|
79
81
|
}
|
|
80
82
|
}, [activeChannel, contentLanguage]);
|
|
81
83
|
|
|
84
|
+
const renderChannel = (channel: typeof channels[number]) => (
|
|
85
|
+
<div key={channel.code}>
|
|
86
|
+
<DropdownMenuItem
|
|
87
|
+
onClick={() => setActiveChannel(channel.id)}
|
|
88
|
+
className="gap-2 p-2"
|
|
89
|
+
>
|
|
90
|
+
<div
|
|
91
|
+
className={cn(
|
|
92
|
+
'flex size-8 items-center justify-center rounded border',
|
|
93
|
+
channel.code === DEFAULT_CHANNEL_CODE ? 'bg-primary' : '',
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<span className="truncate font-semibold text-xs uppercase">
|
|
97
|
+
{getChannelInitialsFromCode(channel.code)}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
<ChannelCodeLabel code={channel.code} />
|
|
101
|
+
{channel.id === displayChannel?.id && (
|
|
102
|
+
<span className="ms-auto text-xs text-muted-foreground">
|
|
103
|
+
<Trans context="current channel">Current</Trans>
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</DropdownMenuItem>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
|
|
82
110
|
return (
|
|
83
111
|
<>
|
|
84
112
|
<SidebarMenu>
|
|
@@ -112,99 +140,82 @@ export function ChannelSwitcher() {
|
|
|
112
140
|
</SidebarMenuButton>
|
|
113
141
|
</DropdownMenuTrigger>
|
|
114
142
|
<DropdownMenuContent
|
|
115
|
-
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
143
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg pt-0 pr-0"
|
|
116
144
|
align="start"
|
|
117
145
|
side={isMobile ? 'bottom' : 'right'}
|
|
118
146
|
sideOffset={4}
|
|
119
147
|
>
|
|
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
|
-
|
|
148
|
+
<ScrollArea className="max-h-[calc(100vh_-_24px)] overflow-y-auto pr-1">
|
|
149
|
+
<div className="sticky top-0 pt-1 bg-popover z-10">
|
|
150
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
151
|
+
<Trans>Channels</Trans>
|
|
152
|
+
</DropdownMenuLabel>
|
|
153
|
+
{!!displayChannel && (
|
|
154
|
+
<>
|
|
155
|
+
{renderChannel(displayChannel)}
|
|
156
|
+
{/* Show language sub-menu for the current channel */}
|
|
157
|
+
<DropdownMenuSub>
|
|
158
|
+
<DropdownMenuSubTrigger className="gap-2 p-2 ps-4">
|
|
159
|
+
<Languages className="w-4 h-4" />
|
|
160
|
+
<div className="flex gap-1 ms-2">
|
|
161
|
+
<span className="text-muted-foreground">Content: </span>
|
|
162
|
+
{formatLanguageName(contentLanguage)}
|
|
163
|
+
</div>
|
|
164
|
+
</DropdownMenuSubTrigger>
|
|
165
|
+
<DropdownMenuSubContent>
|
|
166
|
+
{sortedLanguages?.map(({ code: languageCode, label }) => (
|
|
167
|
+
<DropdownMenuItem
|
|
168
|
+
key={`${displayChannel.code}-${languageCode}`}
|
|
169
|
+
onClick={() => setContentLanguage(languageCode)}
|
|
170
|
+
className={`gap-2 p-2 ${contentLanguage === languageCode ? 'bg-accent' : ''}`}
|
|
171
|
+
>
|
|
172
|
+
<div className="flex w-6 h-5 items-center justify-center rounded border">
|
|
173
|
+
<span className="truncate font-medium text-xs">
|
|
174
|
+
{languageCode.toUpperCase()}
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
177
|
+
<span>{label}</span>
|
|
178
|
+
{contentLanguage === languageCode && (
|
|
179
|
+
<span className="ms-auto text-xs text-muted-foreground">
|
|
180
|
+
<Trans context="active language">
|
|
181
|
+
Active
|
|
182
|
+
</Trans>
|
|
183
|
+
</span>
|
|
184
|
+
)}
|
|
185
|
+
</DropdownMenuItem>
|
|
186
|
+
))}
|
|
187
|
+
<DropdownMenuSeparator />
|
|
158
188
|
<DropdownMenuItem
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
className={`gap-2 p-2 ${contentLanguage === languageCode ? 'bg-accent' : ''}`}
|
|
189
|
+
onClick={() => setShowManageLanguagesDialog(true)}
|
|
190
|
+
className="gap-2 p-2"
|
|
162
191
|
>
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
</div>
|
|
168
|
-
<span>{label}</span>
|
|
169
|
-
{contentLanguage === languageCode && (
|
|
170
|
-
<span className="ms-auto text-xs text-muted-foreground">
|
|
171
|
-
<Trans context="active language">
|
|
172
|
-
Active
|
|
173
|
-
</Trans>
|
|
174
|
-
</span>
|
|
175
|
-
)}
|
|
192
|
+
<Languages className="w-4 h-4" />
|
|
193
|
+
<span>
|
|
194
|
+
<Trans>Manage Languages</Trans>
|
|
195
|
+
</span>
|
|
176
196
|
</DropdownMenuItem>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
>
|
|
183
|
-
<Languages className="w-4 h-4" />
|
|
184
|
-
<span>
|
|
185
|
-
<Trans>Manage Languages</Trans>
|
|
186
|
-
</span>
|
|
187
|
-
</DropdownMenuItem>
|
|
188
|
-
</DropdownMenuSubContent>
|
|
189
|
-
</DropdownMenuSub>
|
|
197
|
+
</DropdownMenuSubContent>
|
|
198
|
+
</DropdownMenuSub>
|
|
199
|
+
{/* Add separator after the current channel group */}
|
|
200
|
+
{orderedChannels.length > 0 && <DropdownMenuSeparator />}
|
|
201
|
+
</>
|
|
190
202
|
)}
|
|
191
|
-
{/* Add separator after the current channel group */}
|
|
192
|
-
{channel.id === displayChannel?.id &&
|
|
193
|
-
index === 0 &&
|
|
194
|
-
orderedChannels.length > 1 && <DropdownMenuSeparator />}
|
|
195
203
|
</div>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
{orderedChannels.map(renderChannel)}
|
|
205
|
+
<PermissionGuard requires={['CreateChannel']}>
|
|
206
|
+
<DropdownMenuSeparator />
|
|
207
|
+
<DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
|
|
208
|
+
<Link to={'/channels/new'}>
|
|
209
|
+
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
|
|
210
|
+
<Plus className="size-4" />
|
|
211
|
+
</div>
|
|
212
|
+
<div className="text-muted-foreground font-medium">
|
|
213
|
+
<Trans>Add channel</Trans>
|
|
214
|
+
</div>
|
|
215
|
+
</Link>
|
|
216
|
+
</DropdownMenuItem>
|
|
217
|
+
</PermissionGuard>
|
|
218
|
+
</ScrollArea>
|
|
208
219
|
</DropdownMenuContent>
|
|
209
220
|
</DropdownMenu>
|
|
210
221
|
</SidebarMenuItem>
|
|
@@ -114,10 +114,16 @@ export function LatestOrdersWidget() {
|
|
|
114
114
|
},
|
|
115
115
|
},
|
|
116
116
|
total: {
|
|
117
|
+
meta: {
|
|
118
|
+
dependencies: ['currencyCode'],
|
|
119
|
+
},
|
|
117
120
|
header: t`Total`,
|
|
118
121
|
cell: OrderMoneyCell,
|
|
119
122
|
},
|
|
120
|
-
totalWithTax: {
|
|
123
|
+
totalWithTax: {
|
|
124
|
+
meta: { dependencies: ['currencyCode'] },
|
|
125
|
+
cell: OrderMoneyCell,
|
|
126
|
+
},
|
|
121
127
|
state: { cell: OrderStateCell },
|
|
122
128
|
customer: { cell: CustomerCell },
|
|
123
129
|
}}
|
|
@@ -96,6 +96,19 @@ export interface ChannelContext {
|
|
|
96
96
|
refreshChannels: () => void;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Retrieves the channel token from localStorage.
|
|
101
|
+
* Returns null if localStorage is unavailable or an error occurs.
|
|
102
|
+
*/
|
|
103
|
+
function getChannelTokenFromLocalStorage(): string | null {
|
|
104
|
+
try {
|
|
105
|
+
return localStorage.getItem(LS_KEY_SELECTED_CHANNEL_TOKEN);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error('Failed to retrieve channel token from localStorage', e);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
/**
|
|
100
113
|
* Sets the channel token in localStorage, which is then used by the `api`
|
|
101
114
|
* object to ensure we add the correct token header to all API calls.
|
|
@@ -183,27 +196,39 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
|
|
|
183
196
|
// If selected channel is not valid for this user, reset it
|
|
184
197
|
if (selectedChannelId && validChannelIds.length && !validChannelIds.includes(selectedChannelId)) {
|
|
185
198
|
setSelectedChannelId(undefined);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
} else if (selectedChannelId && channels.length > 0) {
|
|
200
|
+
// Ensure channel token in localStorage stays in sync with selected channel.
|
|
201
|
+
// This handles the case where activeChannelId persists but the token was cleared (e.g., after logout).
|
|
202
|
+
const selectedChannel = channels.find(c => c.id === selectedChannelId);
|
|
203
|
+
if (selectedChannel) {
|
|
204
|
+
const currentToken = getChannelTokenFromLocalStorage();
|
|
205
|
+
if (currentToken !== selectedChannel.token) {
|
|
206
|
+
setChannelTokenInLocalStorage(selectedChannel.token);
|
|
207
|
+
// Invalidate queries to refetch with the corrected token
|
|
208
|
+
queryClient.invalidateQueries({
|
|
209
|
+
queryKey: ['activeChannel', isAuthenticated],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} else if (channels.length > 0) {
|
|
214
|
+
// If no selected channel is set, use the first available channel
|
|
190
215
|
const defaultChannel = channels[0];
|
|
191
216
|
setSelectedChannelId(defaultChannel.id);
|
|
192
217
|
setChannelTokenInLocalStorage(defaultChannel.token);
|
|
193
218
|
}
|
|
194
|
-
}, [selectedChannelId, channels]);
|
|
219
|
+
}, [selectedChannelId, channels, queryClient, isAuthenticated]);
|
|
195
220
|
|
|
196
221
|
const isLoading = isActiveChannelLoading;
|
|
197
222
|
|
|
198
223
|
// Find the selected channel from the list of channels
|
|
199
224
|
const selectedChannel = activeChannelData?.activeChannel;
|
|
200
225
|
|
|
201
|
-
const refreshChannels = () => {
|
|
226
|
+
const refreshChannels = React.useCallback(() => {
|
|
202
227
|
refreshCurrentUser();
|
|
203
228
|
queryClient.invalidateQueries({
|
|
204
229
|
queryKey: ['channels', isAuthenticated],
|
|
205
230
|
});
|
|
206
|
-
};
|
|
231
|
+
}, [refreshCurrentUser, queryClient, isAuthenticated]);
|
|
207
232
|
|
|
208
233
|
const contextValue: ChannelContext = React.useMemo(
|
|
209
234
|
() => ({
|