@vendure/dashboard 3.3.8 → 3.4.0
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/README.md +62 -0
- package/dist/plugin/api/api-extensions.d.ts +1 -0
- package/dist/plugin/api/api-extensions.js +38 -0
- package/dist/plugin/api/metrics.resolver.d.ts +8 -0
- package/dist/plugin/api/metrics.resolver.js +40 -0
- package/dist/plugin/config/metrics-strategies.d.ts +39 -0
- package/dist/plugin/config/metrics-strategies.js +74 -0
- package/dist/plugin/constants.d.ts +4 -3
- package/dist/plugin/constants.js +10 -277
- package/dist/plugin/dashboard.plugin.d.ts +95 -0
- package/dist/plugin/dashboard.plugin.js +168 -0
- package/dist/plugin/index.d.ts +2 -1
- package/dist/plugin/index.js +18 -1
- package/dist/plugin/package.json +3 -0
- package/dist/plugin/service/metrics.service.d.ts +15 -0
- package/dist/plugin/service/metrics.service.js +145 -0
- package/dist/plugin/types.d.ts +20 -37
- package/dist/plugin/types.js +13 -1
- package/dist/vite/constants.d.ts +5 -0
- package/dist/vite/constants.js +277 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +1 -0
- package/dist/vite/types.d.ts +40 -0
- package/dist/vite/utils/config-loader.js +1 -0
- package/dist/{plugin → vite}/utils/plugin-discovery.js +1 -1
- package/dist/vite/utils/ui-config.d.ts +3 -0
- package/dist/vite/utils/ui-config.js +30 -0
- package/dist/vite/vite-plugin-ui-config.d.ts +123 -0
- package/dist/{plugin → vite}/vite-plugin-ui-config.js +3 -11
- package/dist/{plugin → vite}/vite-plugin-vendure-dashboard.js +1 -1
- package/index.html +1 -1
- package/package.json +16 -7
- package/src/app/app-providers.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +20 -35
- package/src/app/routes/_authenticated/_facets/facets.graphql.ts +40 -0
- package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +147 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +380 -33
- package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +1 -1
- package/src/app/routes/_authenticated/_system/healthchecks.tsx +1 -1
- package/src/app/routes/_authenticated/_system/job-queue.tsx +1 -0
- package/src/app/routes/_authenticated/index.tsx +2 -2
- package/src/app/routes/_authenticated.tsx +1 -1
- package/src/lib/components/data-input/rich-text-input.tsx +14 -8
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +17 -4
- package/src/lib/components/layout/app-layout.tsx +2 -7
- package/src/lib/components/layout/channel-switcher.tsx +166 -57
- package/src/lib/components/layout/dev-mode-indicator.tsx +18 -0
- package/src/lib/components/layout/language-dialog.tsx +2 -1
- package/src/lib/components/layout/manage-languages-dialog.tsx +77 -40
- package/src/lib/components/layout/nav-item-wrapper.tsx +107 -0
- package/src/lib/components/layout/nav-main.tsx +196 -107
- package/src/lib/components/login/login-form.tsx +80 -45
- package/src/lib/components/shared/asset/asset-bulk-actions.tsx +19 -4
- package/src/lib/components/shared/asset/asset-gallery.tsx +2 -2
- package/src/lib/components/shared/detail-page-button.tsx +42 -0
- package/src/lib/components/shared/history-timeline/history-entry-date.tsx +37 -0
- package/src/lib/components/shared/history-timeline/history-entry.tsx +135 -65
- package/src/lib/components/shared/history-timeline/history-note-input.tsx +4 -4
- package/src/lib/components/shared/history-timeline/history-timeline.tsx +7 -54
- package/src/lib/components/shared/translatable-form-field.tsx +16 -2
- package/src/lib/framework/defaults.ts +4 -10
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +4 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +11 -2
- package/src/lib/framework/extension-api/logic/index.ts +1 -0
- package/src/lib/framework/extension-api/logic/login.ts +17 -0
- package/src/lib/framework/extension-api/logic/navigation.ts +1 -0
- package/src/lib/framework/extension-api/types/data-table.ts +12 -3
- package/src/lib/framework/extension-api/types/detail-forms.ts +13 -0
- package/src/lib/framework/extension-api/types/form-components.ts +11 -0
- package/src/lib/framework/extension-api/types/index.ts +1 -0
- package/src/lib/framework/extension-api/types/layout.ts +3 -6
- package/src/lib/framework/extension-api/types/login.ts +96 -0
- package/src/lib/framework/extension-api/types/navigation.ts +57 -0
- package/src/lib/framework/extension-api/types/widgets.ts +0 -4
- package/src/lib/framework/extension-api/use-login-extensions.ts +26 -0
- package/src/lib/framework/layout-engine/dev-mode-button.tsx +24 -0
- package/src/lib/framework/layout-engine/location-wrapper.tsx +5 -12
- package/src/lib/framework/registry/global-registry.ts +4 -0
- package/src/lib/framework/registry/registry-types.ts +2 -0
- package/src/lib/graphql/api.ts +25 -3
- package/src/lib/graphql/graphql-env.d.ts +28 -28
- package/src/lib/graphql/settings-store-operations.ts +17 -0
- package/src/lib/hooks/use-floating-bulk-actions.ts +82 -0
- package/src/lib/hooks/use-local-format.ts +20 -5
- package/src/lib/index.ts +2 -1
- package/src/lib/providers/channel-provider.tsx +13 -11
- package/src/lib/providers/user-settings.tsx +78 -3
- package/src/lib/virtual.d.ts +26 -2
- package/src/vite-env.d.ts +2 -0
- package/vite/utils/plugin-discovery.ts +1 -1
- package/vite/utils/ui-config.ts +30 -42
- package/vite/vite-plugin-ui-config.ts +119 -17
- package/vite/vite-plugin-vendure-dashboard.ts +1 -1
- package/dist/plugin/utils/ui-config.d.ts +0 -3
- package/dist/plugin/utils/ui-config.js +0 -34
- package/dist/plugin/vite-plugin-ui-config.d.ts +0 -15
- package/src/app/routes/_authenticated/_facets/components/add-facet-value-dialog.tsx +0 -146
- package/src/lib/components/shared/rich-text-editor.tsx +0 -0
- /package/dist/{plugin/utils/ast-utils.spec.d.ts → vite/types.js} +0 -0
- /package/dist/{plugin → vite}/utils/ast-utils.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/ast-utils.js +0 -0
- /package/dist/{plugin/utils/config-loader.d.ts → vite/utils/ast-utils.spec.d.ts} +0 -0
- /package/dist/{plugin → vite}/utils/ast-utils.spec.js +0 -0
- /package/dist/{plugin → vite}/utils/compiler.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/compiler.js +0 -0
- /package/dist/{plugin/utils/config-loader.js → vite/utils/config-loader.d.ts} +0 -0
- /package/dist/{plugin → vite}/utils/logger.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/logger.js +0 -0
- /package/dist/{plugin → vite}/utils/plugin-discovery.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/schema-generator.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/schema-generator.js +0 -0
- /package/dist/{plugin → vite}/utils/tsconfig-utils.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/tsconfig-utils.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config-loader.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config-loader.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-gql-tada.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-gql-tada.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-tailwind-source.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-tailwind-source.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-theme.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-theme.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-transform-index.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-transform-index.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-vendure-dashboard.d.ts +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChevronsUpDown, Plus } from 'lucide-react';
|
|
1
|
+
import { ChevronsUpDown, Languages, Plus } from 'lucide-react';
|
|
2
2
|
|
|
3
3
|
import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
|
|
4
4
|
import {
|
|
@@ -7,80 +7,189 @@ import {
|
|
|
7
7
|
DropdownMenuItem,
|
|
8
8
|
DropdownMenuLabel,
|
|
9
9
|
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuSub,
|
|
11
|
+
DropdownMenuSubContent,
|
|
12
|
+
DropdownMenuSubTrigger,
|
|
10
13
|
DropdownMenuTrigger,
|
|
11
14
|
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
12
15
|
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/vdb/components/ui/sidebar.js';
|
|
13
16
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
17
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
18
|
+
import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
|
|
19
|
+
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
14
20
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
15
21
|
import { Link } from '@tanstack/react-router';
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import { ManageLanguagesDialog } from './manage-languages-dialog.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert the channel code to initials.
|
|
27
|
+
* Splits by punctuation like '-' and '_' and takes the first letter of each part
|
|
28
|
+
* up to 3 parts.
|
|
29
|
+
*
|
|
30
|
+
* If no splits, takes the first 3 letters.
|
|
31
|
+
*/
|
|
32
|
+
function getChannelInitialsFromCode(code: string) {
|
|
33
|
+
const parts = code.split(/[-_]/);
|
|
34
|
+
if (parts.length > 1) {
|
|
35
|
+
return parts
|
|
36
|
+
.filter(part => part.length > 0)
|
|
37
|
+
.slice(0, 3)
|
|
38
|
+
.map(part => part[0])
|
|
39
|
+
.join('');
|
|
40
|
+
} else {
|
|
41
|
+
return code.slice(0, 3);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
16
44
|
|
|
17
45
|
export function ChannelSwitcher() {
|
|
18
46
|
const { isMobile } = useSidebar();
|
|
19
47
|
const { channels, activeChannel, selectedChannel, setSelectedChannel } = useChannel();
|
|
48
|
+
const serverConfig = useServerConfig();
|
|
49
|
+
const { formatLanguageName } = useLocalFormat();
|
|
50
|
+
const {
|
|
51
|
+
settings: { contentLanguage },
|
|
52
|
+
setContentLanguage,
|
|
53
|
+
} = useUserSettings();
|
|
54
|
+
const [showManageLanguagesDialog, setShowManageLanguagesDialog] = useState(false);
|
|
20
55
|
|
|
21
56
|
// Use the selected channel if available, otherwise fall back to the active channel
|
|
22
57
|
const displayChannel = selectedChannel || activeChannel;
|
|
23
58
|
|
|
59
|
+
// Get available languages from server config
|
|
60
|
+
const availableLanguages = serverConfig?.availableLanguages || [];
|
|
61
|
+
const hasMultipleLanguages = availableLanguages.length > 1;
|
|
62
|
+
|
|
63
|
+
// Reorder channels to put the currently selected one first
|
|
64
|
+
const orderedChannels = displayChannel
|
|
65
|
+
? [displayChannel, ...channels.filter(ch => ch.id !== displayChannel.id)]
|
|
66
|
+
: channels;
|
|
67
|
+
|
|
24
68
|
return (
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
|
34
|
-
<span className="truncate font-semibold text-xs">
|
|
35
|
-
{displayChannel?.defaultCurrencyCode}
|
|
36
|
-
</span>
|
|
37
|
-
</div>
|
|
38
|
-
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
39
|
-
<span className="truncate font-semibold">
|
|
40
|
-
<ChannelCodeLabel code={displayChannel?.code} />
|
|
41
|
-
</span>
|
|
42
|
-
<span className="truncate text-xs">
|
|
43
|
-
Default Language: {displayChannel?.defaultLanguageCode?.toUpperCase()}
|
|
44
|
-
</span>
|
|
45
|
-
</div>
|
|
46
|
-
<ChevronsUpDown className="ml-auto" />
|
|
47
|
-
</SidebarMenuButton>
|
|
48
|
-
</DropdownMenuTrigger>
|
|
49
|
-
<DropdownMenuContent
|
|
50
|
-
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
51
|
-
align="start"
|
|
52
|
-
side={isMobile ? 'bottom' : 'right'}
|
|
53
|
-
sideOffset={4}
|
|
54
|
-
>
|
|
55
|
-
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
56
|
-
<Trans>Channels</Trans>
|
|
57
|
-
</DropdownMenuLabel>
|
|
58
|
-
{channels.map((channel, index) => (
|
|
59
|
-
<DropdownMenuItem
|
|
60
|
-
key={channel.code}
|
|
61
|
-
onClick={() => setSelectedChannel(channel.id)}
|
|
62
|
-
className="gap-2 p-2"
|
|
69
|
+
<>
|
|
70
|
+
<SidebarMenu>
|
|
71
|
+
<SidebarMenuItem>
|
|
72
|
+
<DropdownMenu>
|
|
73
|
+
<DropdownMenuTrigger asChild>
|
|
74
|
+
<SidebarMenuButton
|
|
75
|
+
size="lg"
|
|
76
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
63
77
|
>
|
|
64
|
-
<div className="flex size-8 items-center justify-center rounded
|
|
65
|
-
<span className="truncate font-semibold text-xs">
|
|
66
|
-
{
|
|
78
|
+
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
|
79
|
+
<span className="truncate font-semibold text-xs uppercase">
|
|
80
|
+
{getChannelInitialsFromCode(displayChannel?.code || '')}
|
|
67
81
|
</span>
|
|
68
82
|
</div>
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
84
|
+
<span className="truncate font-semibold">
|
|
85
|
+
<ChannelCodeLabel code={displayChannel?.code} />
|
|
86
|
+
</span>
|
|
87
|
+
<span className="truncate text-xs">
|
|
88
|
+
{hasMultipleLanguages ? (
|
|
89
|
+
<span className="cursor-pointer hover:text-foreground">
|
|
90
|
+
Language: {formatLanguageName(contentLanguage)}
|
|
91
|
+
</span>
|
|
92
|
+
) : (
|
|
93
|
+
<span>Language: {formatLanguageName(contentLanguage)}</span>
|
|
94
|
+
)}
|
|
95
|
+
</span>
|
|
77
96
|
</div>
|
|
78
|
-
<
|
|
79
|
-
</
|
|
80
|
-
</
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
<ChevronsUpDown className="ml-auto" />
|
|
98
|
+
</SidebarMenuButton>
|
|
99
|
+
</DropdownMenuTrigger>
|
|
100
|
+
<DropdownMenuContent
|
|
101
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
102
|
+
align="start"
|
|
103
|
+
side={isMobile ? 'bottom' : 'right'}
|
|
104
|
+
sideOffset={4}
|
|
105
|
+
>
|
|
106
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
107
|
+
<Trans>Channels</Trans>
|
|
108
|
+
</DropdownMenuLabel>
|
|
109
|
+
{orderedChannels.map((channel, index) => (
|
|
110
|
+
<div key={channel.code}>
|
|
111
|
+
<DropdownMenuItem
|
|
112
|
+
onClick={() => setSelectedChannel(channel.id)}
|
|
113
|
+
className="gap-2 p-2"
|
|
114
|
+
>
|
|
115
|
+
<div className="flex size-8 items-center justify-center rounded border">
|
|
116
|
+
<span className="truncate font-semibold text-xs uppercase">
|
|
117
|
+
{getChannelInitialsFromCode(channel.code)}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<ChannelCodeLabel code={channel.code} />
|
|
121
|
+
{channel.id === displayChannel?.id && (
|
|
122
|
+
<span className="ml-auto text-xs text-muted-foreground">
|
|
123
|
+
Current
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
</DropdownMenuItem>
|
|
127
|
+
{/* Show language sub-menu for the current channel */}
|
|
128
|
+
{channel.id === displayChannel?.id && (
|
|
129
|
+
<DropdownMenuSub>
|
|
130
|
+
<DropdownMenuSubTrigger className="gap-2 p-2 pl-4">
|
|
131
|
+
<Languages className="w-4 h-4" />
|
|
132
|
+
<div className="flex gap-1 ml-2">
|
|
133
|
+
<span className="text-muted-foreground">Content: </span>
|
|
134
|
+
{formatLanguageName(contentLanguage)}
|
|
135
|
+
</div>
|
|
136
|
+
</DropdownMenuSubTrigger>
|
|
137
|
+
<DropdownMenuSubContent>
|
|
138
|
+
{channel.availableLanguageCodes?.map(languageCode => (
|
|
139
|
+
<DropdownMenuItem
|
|
140
|
+
key={`${channel.code}-${languageCode}`}
|
|
141
|
+
onClick={() => setContentLanguage(languageCode)}
|
|
142
|
+
className={`gap-2 p-2 ${contentLanguage === languageCode ? 'bg-accent' : ''}`}
|
|
143
|
+
>
|
|
144
|
+
<div className="flex w-6 h-5 items-center justify-center rounded border">
|
|
145
|
+
<span className="truncate font-medium text-xs">
|
|
146
|
+
{languageCode.toUpperCase()}
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
<span>{formatLanguageName(languageCode)}</span>
|
|
150
|
+
{contentLanguage === languageCode && (
|
|
151
|
+
<span className="ml-auto text-xs text-muted-foreground">
|
|
152
|
+
Active
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
</DropdownMenuItem>
|
|
156
|
+
))}
|
|
157
|
+
<DropdownMenuSeparator />
|
|
158
|
+
<DropdownMenuItem
|
|
159
|
+
onClick={() => setShowManageLanguagesDialog(true)}
|
|
160
|
+
className="gap-2 p-2"
|
|
161
|
+
>
|
|
162
|
+
<Languages className="w-4 h-4" />
|
|
163
|
+
<span>
|
|
164
|
+
<Trans>Manage Languages</Trans>
|
|
165
|
+
</span>
|
|
166
|
+
</DropdownMenuItem>
|
|
167
|
+
</DropdownMenuSubContent>
|
|
168
|
+
</DropdownMenuSub>
|
|
169
|
+
)}
|
|
170
|
+
{/* Add separator after the current channel group */}
|
|
171
|
+
{channel.id === displayChannel?.id &&
|
|
172
|
+
index === 0 &&
|
|
173
|
+
orderedChannels.length > 1 && <DropdownMenuSeparator />}
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
<DropdownMenuSeparator />
|
|
177
|
+
<DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
|
|
178
|
+
<Link to={'/channels/new'}>
|
|
179
|
+
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
|
|
180
|
+
<Plus className="size-4" />
|
|
181
|
+
</div>
|
|
182
|
+
<div className="text-muted-foreground font-medium">Add channel</div>
|
|
183
|
+
</Link>
|
|
184
|
+
</DropdownMenuItem>
|
|
185
|
+
</DropdownMenuContent>
|
|
186
|
+
</DropdownMenu>
|
|
187
|
+
</SidebarMenuItem>
|
|
188
|
+
</SidebarMenu>
|
|
189
|
+
<ManageLanguagesDialog
|
|
190
|
+
open={showManageLanguagesDialog}
|
|
191
|
+
onClose={() => setShowManageLanguagesDialog(false)}
|
|
192
|
+
/>
|
|
193
|
+
</>
|
|
85
194
|
);
|
|
86
195
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
2
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
4
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
5
|
+
import { CodeXmlIcon, XIcon } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export function DevModeIndicator() {
|
|
8
|
+
const { setDevMode } = useUserSettings();
|
|
9
|
+
return (
|
|
10
|
+
<Badge className="bg-dev-mode text-background">
|
|
11
|
+
<CodeXmlIcon className="w-6 h-6" />
|
|
12
|
+
<Trans>Dev Mode</Trans>
|
|
13
|
+
<Button variant="ghost" size="icon-xs" onClick={() => setDevMode(false)}>
|
|
14
|
+
<XIcon className="w-4 h-4" />
|
|
15
|
+
</Button>
|
|
16
|
+
</Badge>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -10,7 +10,8 @@ import { Label } from '../ui/label.js';
|
|
|
10
10
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
|
|
11
11
|
|
|
12
12
|
export function LanguageDialog() {
|
|
13
|
-
const {
|
|
13
|
+
const { i18n } = uiConfig;
|
|
14
|
+
const { availableLocales, availableLanguages } = i18n;
|
|
14
15
|
const { settings, setDisplayLanguage, setDisplayLocale } = useUserSettings();
|
|
15
16
|
const availableCurrencyCodes = Object.values(CurrencyCode);
|
|
16
17
|
const { formatCurrency, formatLanguageName, formatCurrencyName, formatDate } = useLocalFormat();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
|
|
1
2
|
import { LanguageSelector } from '@/vdb/components/shared/language-selector.js';
|
|
2
3
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
4
|
import {
|
|
@@ -16,7 +17,6 @@ import { graphql } from '@/vdb/graphql/graphql.js';
|
|
|
16
17
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
17
18
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
18
19
|
import { usePermissions } from '@/vdb/hooks/use-permissions.js';
|
|
19
|
-
import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
|
|
20
20
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
21
21
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
22
22
|
import { AlertCircle, Lock } from 'lucide-react';
|
|
@@ -69,7 +69,24 @@ const updateChannelDocument = graphql(`
|
|
|
69
69
|
|
|
70
70
|
// All possible language codes for global settings - includes more than what might be globally enabled
|
|
71
71
|
const ALL_LANGUAGE_CODES = [
|
|
72
|
-
'en',
|
|
72
|
+
'en',
|
|
73
|
+
'es',
|
|
74
|
+
'fr',
|
|
75
|
+
'de',
|
|
76
|
+
'it',
|
|
77
|
+
'pt',
|
|
78
|
+
'nl',
|
|
79
|
+
'pl',
|
|
80
|
+
'ru',
|
|
81
|
+
'ja',
|
|
82
|
+
'zh',
|
|
83
|
+
'ko',
|
|
84
|
+
'ar',
|
|
85
|
+
'hi',
|
|
86
|
+
'sv',
|
|
87
|
+
'da',
|
|
88
|
+
'no',
|
|
89
|
+
'fi',
|
|
73
90
|
];
|
|
74
91
|
|
|
75
92
|
interface ManageLanguagesDialogProps {
|
|
@@ -82,12 +99,13 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
82
99
|
const { activeChannel, selectedChannel } = useChannel();
|
|
83
100
|
const { hasPermissions } = usePermissions();
|
|
84
101
|
const queryClient = useQueryClient();
|
|
85
|
-
|
|
102
|
+
|
|
86
103
|
const displayChannel = selectedChannel || activeChannel;
|
|
87
|
-
|
|
104
|
+
|
|
88
105
|
// Permission checks
|
|
89
106
|
const canReadGlobalSettings = hasPermissions(['ReadSettings']) || hasPermissions(['ReadGlobalSettings']);
|
|
90
|
-
const canUpdateGlobalSettings =
|
|
107
|
+
const canUpdateGlobalSettings =
|
|
108
|
+
hasPermissions(['UpdateSettings']) || hasPermissions(['UpdateGlobalSettings']);
|
|
91
109
|
const canReadChannel = hasPermissions(['ReadChannel']);
|
|
92
110
|
const canUpdateChannel = hasPermissions(['UpdateChannel']);
|
|
93
111
|
|
|
@@ -97,10 +115,10 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
97
115
|
const [channelDefaultLanguage, setChannelDefaultLanguage] = useState<string>('');
|
|
98
116
|
|
|
99
117
|
// Queries
|
|
100
|
-
const {
|
|
101
|
-
data: globalSettingsData,
|
|
118
|
+
const {
|
|
119
|
+
data: globalSettingsData,
|
|
102
120
|
isLoading: globalSettingsLoading,
|
|
103
|
-
error: globalSettingsError
|
|
121
|
+
error: globalSettingsError,
|
|
104
122
|
} = useQuery({
|
|
105
123
|
queryKey: ['globalSettings', 'languages'],
|
|
106
124
|
queryFn: () => api.query(globalSettingsLanguagesDocument),
|
|
@@ -121,8 +139,11 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
121
139
|
});
|
|
122
140
|
|
|
123
141
|
const updateChannelMutation = useMutation({
|
|
124
|
-
mutationFn: (input: {
|
|
125
|
-
|
|
142
|
+
mutationFn: (input: {
|
|
143
|
+
id: string;
|
|
144
|
+
availableLanguageCodes?: string[];
|
|
145
|
+
defaultLanguageCode?: string;
|
|
146
|
+
}) => api.mutate(updateChannelDocument, { input }),
|
|
126
147
|
onSuccess: () => {
|
|
127
148
|
queryClient.invalidateQueries({ queryKey: ['channels'] });
|
|
128
149
|
toast.success('Channel language settings updated successfully');
|
|
@@ -145,11 +166,11 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
145
166
|
|
|
146
167
|
const handleGlobalLanguagesChange = (newLanguages: string[]) => {
|
|
147
168
|
setGlobalLanguages(newLanguages);
|
|
148
|
-
|
|
169
|
+
|
|
149
170
|
// Remove channel languages that are no longer in global languages
|
|
150
171
|
const updatedChannelLanguages = channelLanguages.filter(lang => newLanguages.includes(lang));
|
|
151
172
|
setChannelLanguages(updatedChannelLanguages);
|
|
152
|
-
|
|
173
|
+
|
|
153
174
|
// If the default language is no longer available, reset it
|
|
154
175
|
if (!newLanguages.includes(channelDefaultLanguage)) {
|
|
155
176
|
setChannelDefaultLanguage(updatedChannelLanguages[0] || '');
|
|
@@ -158,7 +179,7 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
158
179
|
|
|
159
180
|
const handleChannelLanguagesChange = (newLanguages: string[]) => {
|
|
160
181
|
setChannelLanguages(newLanguages);
|
|
161
|
-
|
|
182
|
+
|
|
162
183
|
// If the default language is no longer available, reset it
|
|
163
184
|
if (!newLanguages.includes(channelDefaultLanguage)) {
|
|
164
185
|
setChannelDefaultLanguage(newLanguages[0] || '');
|
|
@@ -172,7 +193,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
172
193
|
if (canUpdateGlobalSettings && globalSettingsData) {
|
|
173
194
|
const currentGlobalLanguages = globalSettingsData.globalSettings.availableLanguages || [];
|
|
174
195
|
if (JSON.stringify(currentGlobalLanguages.sort()) !== JSON.stringify(globalLanguages.sort())) {
|
|
175
|
-
promises.push(
|
|
196
|
+
promises.push(
|
|
197
|
+
updateGlobalSettingsMutation.mutateAsync({ availableLanguages: globalLanguages }),
|
|
198
|
+
);
|
|
176
199
|
}
|
|
177
200
|
}
|
|
178
201
|
|
|
@@ -180,16 +203,19 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
180
203
|
if (canUpdateChannel && displayChannel) {
|
|
181
204
|
const currentChannelLanguages = displayChannel.availableLanguageCodes || [];
|
|
182
205
|
const currentChannelDefault = displayChannel.defaultLanguageCode || '';
|
|
183
|
-
|
|
184
|
-
const languagesChanged =
|
|
206
|
+
|
|
207
|
+
const languagesChanged =
|
|
208
|
+
JSON.stringify(currentChannelLanguages.sort()) !== JSON.stringify(channelLanguages.sort());
|
|
185
209
|
const defaultChanged = currentChannelDefault !== channelDefaultLanguage;
|
|
186
|
-
|
|
210
|
+
|
|
187
211
|
if (languagesChanged || defaultChanged) {
|
|
188
|
-
promises.push(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
212
|
+
promises.push(
|
|
213
|
+
updateChannelMutation.mutateAsync({
|
|
214
|
+
id: displayChannel.id,
|
|
215
|
+
availableLanguageCodes: channelLanguages,
|
|
216
|
+
defaultLanguageCode: channelDefaultLanguage,
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
193
219
|
}
|
|
194
220
|
}
|
|
195
221
|
|
|
@@ -208,17 +234,17 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
208
234
|
return true;
|
|
209
235
|
}
|
|
210
236
|
}
|
|
211
|
-
|
|
237
|
+
|
|
212
238
|
if (displayChannel && canUpdateChannel) {
|
|
213
239
|
const currentChannelLangs = displayChannel.availableLanguageCodes || [];
|
|
214
240
|
const currentChannelDefault = displayChannel.defaultLanguageCode || '';
|
|
215
|
-
|
|
241
|
+
|
|
216
242
|
return (
|
|
217
243
|
JSON.stringify(currentChannelLangs.sort()) !== JSON.stringify(channelLanguages.sort()) ||
|
|
218
244
|
currentChannelDefault !== channelDefaultLanguage
|
|
219
245
|
);
|
|
220
246
|
}
|
|
221
|
-
|
|
247
|
+
|
|
222
248
|
return false;
|
|
223
249
|
};
|
|
224
250
|
|
|
@@ -228,7 +254,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
228
254
|
<Dialog open={open} onOpenChange={onClose}>
|
|
229
255
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
230
256
|
<DialogHeader>
|
|
231
|
-
<DialogTitle
|
|
257
|
+
<DialogTitle>
|
|
258
|
+
<Trans>Manage Languages</Trans>
|
|
259
|
+
</DialogTitle>
|
|
232
260
|
<DialogDescription>
|
|
233
261
|
<Trans>Configure available languages for your store and channels</Trans>
|
|
234
262
|
</DialogDescription>
|
|
@@ -238,10 +266,12 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
238
266
|
{/* Global Settings Section */}
|
|
239
267
|
<div>
|
|
240
268
|
<div className="flex items-center gap-2 mb-3">
|
|
241
|
-
<h3 className="
|
|
269
|
+
<h3 className="font-semibold">
|
|
270
|
+
<Trans>Global Languages</Trans>
|
|
271
|
+
</h3>
|
|
242
272
|
{!canReadGlobalSettings && <Lock className="h-4 w-4 text-muted-foreground" />}
|
|
243
273
|
</div>
|
|
244
|
-
|
|
274
|
+
|
|
245
275
|
{!canReadGlobalSettings ? (
|
|
246
276
|
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
|
|
247
277
|
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
|
@@ -262,10 +292,14 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
262
292
|
</div>
|
|
263
293
|
) : (
|
|
264
294
|
<div className="space-y-2">
|
|
265
|
-
<Label
|
|
295
|
+
<Label>
|
|
266
296
|
<Trans>Select Available Languages</Trans>
|
|
267
297
|
</Label>
|
|
268
|
-
<div
|
|
298
|
+
<div
|
|
299
|
+
className={
|
|
300
|
+
!canUpdateGlobalSettings ? 'pointer-events-none opacity-50' : ''
|
|
301
|
+
}
|
|
302
|
+
>
|
|
269
303
|
<LanguageSelector
|
|
270
304
|
value={globalLanguages}
|
|
271
305
|
onChange={handleGlobalLanguagesChange}
|
|
@@ -285,8 +319,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
285
319
|
{/* Channel Settings Section */}
|
|
286
320
|
<div>
|
|
287
321
|
<div className="flex items-center gap-2 mb-3">
|
|
288
|
-
<h3 className="
|
|
289
|
-
<Trans>Channel Languages</Trans> -
|
|
322
|
+
<h3 className="font-semibold">
|
|
323
|
+
<Trans>Channel Languages</Trans> -{' '}
|
|
324
|
+
<ChannelCodeLabel code={displayChannel?.code} />
|
|
290
325
|
</h3>
|
|
291
326
|
{!canReadChannel && <Lock className="h-4 w-4 text-muted-foreground" />}
|
|
292
327
|
</div>
|
|
@@ -304,7 +339,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
304
339
|
<Label className="text-sm font-medium">
|
|
305
340
|
<Trans>Available Languages</Trans>
|
|
306
341
|
</Label>
|
|
307
|
-
<div
|
|
342
|
+
<div
|
|
343
|
+
className={!canUpdateChannel ? 'pointer-events-none opacity-50' : ''}
|
|
344
|
+
>
|
|
308
345
|
<LanguageSelector
|
|
309
346
|
value={channelLanguages}
|
|
310
347
|
onChange={handleChannelLanguagesChange}
|
|
@@ -318,7 +355,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
318
355
|
</p>
|
|
319
356
|
) : (
|
|
320
357
|
<p className="text-xs text-muted-foreground">
|
|
321
|
-
<Trans>
|
|
358
|
+
<Trans>
|
|
359
|
+
Select from globally available languages for this channel
|
|
360
|
+
</Trans>
|
|
322
361
|
</p>
|
|
323
362
|
)}
|
|
324
363
|
</div>
|
|
@@ -339,7 +378,8 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
339
378
|
<SelectContent>
|
|
340
379
|
{channelLanguages.map(languageCode => (
|
|
341
380
|
<SelectItem key={languageCode} value={languageCode}>
|
|
342
|
-
{formatLanguageName(languageCode)} (
|
|
381
|
+
{formatLanguageName(languageCode)} (
|
|
382
|
+
{languageCode.toUpperCase()})
|
|
343
383
|
</SelectItem>
|
|
344
384
|
))}
|
|
345
385
|
</SelectContent>
|
|
@@ -355,14 +395,11 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
|
|
|
355
395
|
<Button variant="outline" onClick={onClose} disabled={isLoading}>
|
|
356
396
|
<Trans>Cancel</Trans>
|
|
357
397
|
</Button>
|
|
358
|
-
<Button
|
|
359
|
-
onClick={handleSave}
|
|
360
|
-
disabled={!hasChanges() || isLoading}
|
|
361
|
-
>
|
|
398
|
+
<Button onClick={handleSave} disabled={!hasChanges() || isLoading}>
|
|
362
399
|
{isLoading ? <Trans>Saving...</Trans> : <Trans>Save Changes</Trans>}
|
|
363
400
|
</Button>
|
|
364
401
|
</DialogFooter>
|
|
365
402
|
</DialogContent>
|
|
366
403
|
</Dialog>
|
|
367
404
|
);
|
|
368
|
-
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CopyableText } from '@/vdb/components/shared/copyable-text.js';
|
|
2
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
|
|
3
|
+
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
4
|
+
import { cn } from '@/vdb/lib/utils.js';
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
|
+
import { DevModeButton } from '../../framework/layout-engine/dev-mode-button.js';
|
|
7
|
+
|
|
8
|
+
// Singleton state for hover tracking
|
|
9
|
+
let globalHoveredNavId: string | null = null;
|
|
10
|
+
const navHoverListeners: Set<(id: string | null) => void> = new Set();
|
|
11
|
+
|
|
12
|
+
const setGlobalHoveredNavId = (id: string | null) => {
|
|
13
|
+
globalHoveredNavId = id;
|
|
14
|
+
navHoverListeners.forEach(listener => listener(id));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface NavItemWrapperProps {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
locationId: string;
|
|
20
|
+
order?: number;
|
|
21
|
+
parentLocationId?: string;
|
|
22
|
+
offset?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function NavItemWrapper({
|
|
26
|
+
children,
|
|
27
|
+
locationId,
|
|
28
|
+
order,
|
|
29
|
+
parentLocationId,
|
|
30
|
+
offset,
|
|
31
|
+
}: Readonly<NavItemWrapperProps>) {
|
|
32
|
+
const { settings } = useUserSettings();
|
|
33
|
+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
34
|
+
const [hoveredId, setHoveredId] = useState<string | null>(globalHoveredNavId);
|
|
35
|
+
|
|
36
|
+
const isHovered = hoveredId === locationId;
|
|
37
|
+
|
|
38
|
+
// Subscribe to global hover changes
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const listener = (newHoveredId: string | null) => {
|
|
41
|
+
setHoveredId(newHoveredId);
|
|
42
|
+
};
|
|
43
|
+
navHoverListeners.add(listener);
|
|
44
|
+
return () => {
|
|
45
|
+
navHoverListeners.delete(listener);
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const setHoverId = (id: string | null) => {
|
|
50
|
+
setGlobalHoveredNavId(id);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleMouseEnter = () => {
|
|
54
|
+
setHoverId(locationId);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleMouseLeave = () => {
|
|
58
|
+
// If we have a parent, fall back to the parent on mouse leave
|
|
59
|
+
// Otherwise, clear the hover
|
|
60
|
+
setHoverId(parentLocationId || null);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (settings.devMode) {
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
'ring-2 ring-transparent rounded-md transition-all delay-50 relative',
|
|
68
|
+
isHovered || isPopoverOpen ? 'ring-dev-mode ring-offset-1 ring-offset-background' : '',
|
|
69
|
+
)}
|
|
70
|
+
onMouseEnter={handleMouseEnter}
|
|
71
|
+
onMouseLeave={handleMouseLeave}
|
|
72
|
+
>
|
|
73
|
+
<div
|
|
74
|
+
className={cn(
|
|
75
|
+
`absolute right-0 transition-all delay-50 z-10`,
|
|
76
|
+
isHovered || isPopoverOpen ? 'visible' : 'invisible',
|
|
77
|
+
offset ? 'right-[26px] top-[3px]' : 'right-[3px] top-0.5 ',
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
|
81
|
+
<PopoverTrigger asChild>
|
|
82
|
+
<DevModeButton className={`h-6 w-6`} />
|
|
83
|
+
</PopoverTrigger>
|
|
84
|
+
<PopoverContent className="w-48 p-3">
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
<div className="space-y-1">
|
|
87
|
+
<div className="text-xs">
|
|
88
|
+
<div className="text-muted-foreground mb-0.5">locationId</div>
|
|
89
|
+
<CopyableText text={locationId} />
|
|
90
|
+
</div>
|
|
91
|
+
{order !== undefined && (
|
|
92
|
+
<div className="text-xs">
|
|
93
|
+
<div className="text-muted-foreground mb-0.5">order</div>
|
|
94
|
+
<CopyableText text={order.toString()} />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</PopoverContent>
|
|
100
|
+
</Popover>
|
|
101
|
+
</div>
|
|
102
|
+
{children}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return children;
|
|
107
|
+
}
|