@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.
Files changed (131) hide show
  1. package/README.md +62 -0
  2. package/dist/plugin/api/api-extensions.d.ts +1 -0
  3. package/dist/plugin/api/api-extensions.js +38 -0
  4. package/dist/plugin/api/metrics.resolver.d.ts +8 -0
  5. package/dist/plugin/api/metrics.resolver.js +40 -0
  6. package/dist/plugin/config/metrics-strategies.d.ts +39 -0
  7. package/dist/plugin/config/metrics-strategies.js +74 -0
  8. package/dist/plugin/constants.d.ts +4 -3
  9. package/dist/plugin/constants.js +10 -277
  10. package/dist/plugin/dashboard.plugin.d.ts +95 -0
  11. package/dist/plugin/dashboard.plugin.js +168 -0
  12. package/dist/plugin/index.d.ts +2 -1
  13. package/dist/plugin/index.js +18 -1
  14. package/dist/plugin/package.json +3 -0
  15. package/dist/plugin/service/metrics.service.d.ts +15 -0
  16. package/dist/plugin/service/metrics.service.js +145 -0
  17. package/dist/plugin/types.d.ts +20 -37
  18. package/dist/plugin/types.js +13 -1
  19. package/dist/vite/constants.d.ts +5 -0
  20. package/dist/vite/constants.js +277 -0
  21. package/dist/vite/index.d.ts +1 -0
  22. package/dist/vite/index.js +1 -0
  23. package/dist/vite/types.d.ts +40 -0
  24. package/dist/vite/utils/config-loader.js +1 -0
  25. package/dist/{plugin → vite}/utils/plugin-discovery.js +1 -1
  26. package/dist/vite/utils/ui-config.d.ts +3 -0
  27. package/dist/vite/utils/ui-config.js +30 -0
  28. package/dist/vite/vite-plugin-ui-config.d.ts +123 -0
  29. package/dist/{plugin → vite}/vite-plugin-ui-config.js +3 -11
  30. package/dist/{plugin → vite}/vite-plugin-vendure-dashboard.js +1 -1
  31. package/index.html +1 -1
  32. package/package.json +16 -7
  33. package/src/app/app-providers.tsx +1 -1
  34. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
  35. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +20 -35
  36. package/src/app/routes/_authenticated/_facets/facets.graphql.ts +40 -0
  37. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +147 -0
  38. package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +380 -33
  39. package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +1 -1
  40. package/src/app/routes/_authenticated/_system/healthchecks.tsx +1 -1
  41. package/src/app/routes/_authenticated/_system/job-queue.tsx +1 -0
  42. package/src/app/routes/_authenticated/index.tsx +2 -2
  43. package/src/app/routes/_authenticated.tsx +1 -1
  44. package/src/lib/components/data-input/rich-text-input.tsx +14 -8
  45. package/src/lib/components/data-table/data-table-bulk-actions.tsx +17 -4
  46. package/src/lib/components/layout/app-layout.tsx +2 -7
  47. package/src/lib/components/layout/channel-switcher.tsx +166 -57
  48. package/src/lib/components/layout/dev-mode-indicator.tsx +18 -0
  49. package/src/lib/components/layout/language-dialog.tsx +2 -1
  50. package/src/lib/components/layout/manage-languages-dialog.tsx +77 -40
  51. package/src/lib/components/layout/nav-item-wrapper.tsx +107 -0
  52. package/src/lib/components/layout/nav-main.tsx +196 -107
  53. package/src/lib/components/login/login-form.tsx +80 -45
  54. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +19 -4
  55. package/src/lib/components/shared/asset/asset-gallery.tsx +2 -2
  56. package/src/lib/components/shared/detail-page-button.tsx +42 -0
  57. package/src/lib/components/shared/history-timeline/history-entry-date.tsx +37 -0
  58. package/src/lib/components/shared/history-timeline/history-entry.tsx +135 -65
  59. package/src/lib/components/shared/history-timeline/history-note-input.tsx +4 -4
  60. package/src/lib/components/shared/history-timeline/history-timeline.tsx +7 -54
  61. package/src/lib/components/shared/translatable-form-field.tsx +16 -2
  62. package/src/lib/framework/defaults.ts +4 -10
  63. package/src/lib/framework/extension-api/define-dashboard-extension.ts +4 -0
  64. package/src/lib/framework/extension-api/extension-api-types.ts +11 -2
  65. package/src/lib/framework/extension-api/logic/index.ts +1 -0
  66. package/src/lib/framework/extension-api/logic/login.ts +17 -0
  67. package/src/lib/framework/extension-api/logic/navigation.ts +1 -0
  68. package/src/lib/framework/extension-api/types/data-table.ts +12 -3
  69. package/src/lib/framework/extension-api/types/detail-forms.ts +13 -0
  70. package/src/lib/framework/extension-api/types/form-components.ts +11 -0
  71. package/src/lib/framework/extension-api/types/index.ts +1 -0
  72. package/src/lib/framework/extension-api/types/layout.ts +3 -6
  73. package/src/lib/framework/extension-api/types/login.ts +96 -0
  74. package/src/lib/framework/extension-api/types/navigation.ts +57 -0
  75. package/src/lib/framework/extension-api/types/widgets.ts +0 -4
  76. package/src/lib/framework/extension-api/use-login-extensions.ts +26 -0
  77. package/src/lib/framework/layout-engine/dev-mode-button.tsx +24 -0
  78. package/src/lib/framework/layout-engine/location-wrapper.tsx +5 -12
  79. package/src/lib/framework/registry/global-registry.ts +4 -0
  80. package/src/lib/framework/registry/registry-types.ts +2 -0
  81. package/src/lib/graphql/api.ts +25 -3
  82. package/src/lib/graphql/graphql-env.d.ts +28 -28
  83. package/src/lib/graphql/settings-store-operations.ts +17 -0
  84. package/src/lib/hooks/use-floating-bulk-actions.ts +82 -0
  85. package/src/lib/hooks/use-local-format.ts +20 -5
  86. package/src/lib/index.ts +2 -1
  87. package/src/lib/providers/channel-provider.tsx +13 -11
  88. package/src/lib/providers/user-settings.tsx +78 -3
  89. package/src/lib/virtual.d.ts +26 -2
  90. package/src/vite-env.d.ts +2 -0
  91. package/vite/utils/plugin-discovery.ts +1 -1
  92. package/vite/utils/ui-config.ts +30 -42
  93. package/vite/vite-plugin-ui-config.ts +119 -17
  94. package/vite/vite-plugin-vendure-dashboard.ts +1 -1
  95. package/dist/plugin/utils/ui-config.d.ts +0 -3
  96. package/dist/plugin/utils/ui-config.js +0 -34
  97. package/dist/plugin/vite-plugin-ui-config.d.ts +0 -15
  98. package/src/app/routes/_authenticated/_facets/components/add-facet-value-dialog.tsx +0 -146
  99. package/src/lib/components/shared/rich-text-editor.tsx +0 -0
  100. /package/dist/{plugin/utils/ast-utils.spec.d.ts → vite/types.js} +0 -0
  101. /package/dist/{plugin → vite}/utils/ast-utils.d.ts +0 -0
  102. /package/dist/{plugin → vite}/utils/ast-utils.js +0 -0
  103. /package/dist/{plugin/utils/config-loader.d.ts → vite/utils/ast-utils.spec.d.ts} +0 -0
  104. /package/dist/{plugin → vite}/utils/ast-utils.spec.js +0 -0
  105. /package/dist/{plugin → vite}/utils/compiler.d.ts +0 -0
  106. /package/dist/{plugin → vite}/utils/compiler.js +0 -0
  107. /package/dist/{plugin/utils/config-loader.js → vite/utils/config-loader.d.ts} +0 -0
  108. /package/dist/{plugin → vite}/utils/logger.d.ts +0 -0
  109. /package/dist/{plugin → vite}/utils/logger.js +0 -0
  110. /package/dist/{plugin → vite}/utils/plugin-discovery.d.ts +0 -0
  111. /package/dist/{plugin → vite}/utils/schema-generator.d.ts +0 -0
  112. /package/dist/{plugin → vite}/utils/schema-generator.js +0 -0
  113. /package/dist/{plugin → vite}/utils/tsconfig-utils.d.ts +0 -0
  114. /package/dist/{plugin → vite}/utils/tsconfig-utils.js +0 -0
  115. /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.d.ts +0 -0
  116. /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.js +0 -0
  117. /package/dist/{plugin → vite}/vite-plugin-config-loader.d.ts +0 -0
  118. /package/dist/{plugin → vite}/vite-plugin-config-loader.js +0 -0
  119. /package/dist/{plugin → vite}/vite-plugin-config.d.ts +0 -0
  120. /package/dist/{plugin → vite}/vite-plugin-config.js +0 -0
  121. /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.d.ts +0 -0
  122. /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.js +0 -0
  123. /package/dist/{plugin → vite}/vite-plugin-gql-tada.d.ts +0 -0
  124. /package/dist/{plugin → vite}/vite-plugin-gql-tada.js +0 -0
  125. /package/dist/{plugin → vite}/vite-plugin-tailwind-source.d.ts +0 -0
  126. /package/dist/{plugin → vite}/vite-plugin-tailwind-source.js +0 -0
  127. /package/dist/{plugin → vite}/vite-plugin-theme.d.ts +0 -0
  128. /package/dist/{plugin → vite}/vite-plugin-theme.js +0 -0
  129. /package/dist/{plugin → vite}/vite-plugin-transform-index.d.ts +0 -0
  130. /package/dist/{plugin → vite}/vite-plugin-transform-index.js +0 -0
  131. /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
- <SidebarMenu>
26
- <SidebarMenuItem>
27
- <DropdownMenu>
28
- <DropdownMenuTrigger asChild>
29
- <SidebarMenuButton
30
- size="lg"
31
- className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
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 border">
65
- <span className="truncate font-semibold text-xs">
66
- {channel.defaultCurrencyCode}
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
- <ChannelCodeLabel code={channel.code} />
70
- </DropdownMenuItem>
71
- ))}
72
- <DropdownMenuSeparator />
73
- <DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
74
- <Link to={'/channels/new'}>
75
- <div className="bg-background flex size-6 items-center justify-center rounded-md border">
76
- <Plus className="size-4" />
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
- <div className="text-muted-foreground font-medium">Add channel</div>
79
- </Link>
80
- </DropdownMenuItem>
81
- </DropdownMenuContent>
82
- </DropdownMenu>
83
- </SidebarMenuItem>
84
- </SidebarMenu>
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 { availableLocales, availableLanguages } = uiConfig;
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', 'es', 'fr', 'de', 'it', 'pt', 'nl', 'pl', 'ru', 'ja', 'zh', 'ko', 'ar', 'hi', 'sv', 'da', 'no', 'fi'
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 = hasPermissions(['UpdateSettings']) || hasPermissions(['UpdateGlobalSettings']);
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: { id: string; availableLanguageCodes?: string[]; defaultLanguageCode?: string }) =>
125
- api.mutate(updateChannelDocument, { input }),
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(updateGlobalSettingsMutation.mutateAsync({ availableLanguages: globalLanguages }));
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 = JSON.stringify(currentChannelLanguages.sort()) !== JSON.stringify(channelLanguages.sort());
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(updateChannelMutation.mutateAsync({
189
- id: displayChannel.id,
190
- availableLanguageCodes: channelLanguages,
191
- defaultLanguageCode: channelDefaultLanguage,
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><Trans>Manage Languages</Trans></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="text-lg font-semibold"><Trans>Global Languages</Trans></h3>
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 className="text-sm font-medium">
295
+ <Label>
266
296
  <Trans>Select Available Languages</Trans>
267
297
  </Label>
268
- <div className={!canUpdateGlobalSettings ? 'pointer-events-none opacity-50' : ''}>
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="text-lg font-semibold">
289
- <Trans>Channel Languages</Trans> - <ChannelCodeLabel code={displayChannel?.code} />
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 className={!canUpdateChannel ? 'pointer-events-none opacity-50' : ''}>
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>Select from globally available languages for this channel</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)} ({languageCode.toUpperCase()})
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
+ }