@vendure/dashboard 3.5.2-master-202511190231 → 3.5.2-master-202511220230

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.5.2-master-202511190231",
4
+ "version": "3.5.2-master-202511220230",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -155,8 +155,8 @@
155
155
  "@storybook/addon-vitest": "^10.0.0-beta.9",
156
156
  "@storybook/react-vite": "^10.0.0-beta.9",
157
157
  "@types/node": "^22.13.4",
158
- "@vendure/common": "^3.5.2-master-202511190231",
159
- "@vendure/core": "^3.5.2-master-202511190231",
158
+ "@vendure/common": "^3.5.2-master-202511220230",
159
+ "@vendure/core": "^3.5.2-master-202511220230",
160
160
  "@vitest/browser": "^3.2.4",
161
161
  "@vitest/coverage-v8": "^3.2.4",
162
162
  "eslint": "^9.19.0",
@@ -173,5 +173,5 @@
173
173
  "lightningcss-linux-arm64-musl": "^1.29.3",
174
174
  "lightningcss-linux-x64-musl": "^1.29.1"
175
175
  },
176
- "gitHead": "51a67b8992bed6a052aeaa01dba623a5d970574e"
176
+ "gitHead": "a7f2e68fea355ab17c8455df426aa754e6525180"
177
177
  }
@@ -17,6 +17,7 @@ import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
17
17
  import { useChannel } from '@/vdb/hooks/use-channel.js';
18
18
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
19
19
  import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
20
+ import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
20
21
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
21
22
  import { cn } from '@/vdb/lib/utils.js';
22
23
  import { Trans } from '@lingui/react/macro';
@@ -65,6 +66,9 @@ export function ChannelSwitcher() {
65
66
  ? [displayChannel, ...channels.filter(ch => ch.id !== displayChannel.id)]
66
67
  : channels;
67
68
 
69
+ // Sort language codes by their formatted names and map to code and label
70
+ const sortedLanguages = useSortedLanguages(displayChannel?.availableLanguageCodes);
71
+
68
72
  useEffect(() => {
69
73
  if (activeChannel?.availableLanguageCodes) {
70
74
  // Ensure the current content language is a valid one for the active
@@ -150,7 +154,7 @@ export function ChannelSwitcher() {
150
154
  </div>
151
155
  </DropdownMenuSubTrigger>
152
156
  <DropdownMenuSubContent>
153
- {channel.availableLanguageCodes?.map(languageCode => (
157
+ {sortedLanguages?.map(({ code: languageCode, label }) => (
154
158
  <DropdownMenuItem
155
159
  key={`${channel.code}-${languageCode}`}
156
160
  onClick={() => setContentLanguage(languageCode)}
@@ -161,7 +165,7 @@ export function ChannelSwitcher() {
161
165
  {languageCode.toUpperCase()}
162
166
  </span>
163
167
  </div>
164
- <span>{formatLanguageName(languageCode)}</span>
168
+ <span>{label}</span>
165
169
  {contentLanguage === languageCode && (
166
170
  <span className="ml-auto text-xs text-muted-foreground">
167
171
  <Trans context="active language">
@@ -1,6 +1,6 @@
1
1
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
2
- import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
2
  import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
3
+ import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
4
4
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
5
5
  import { cn } from '@/vdb/lib/utils.js';
6
6
 
@@ -12,14 +12,13 @@ interface ContentLanguageSelectorProps {
12
12
 
13
13
  export function ContentLanguageSelector({ value, onChange, className }: ContentLanguageSelectorProps) {
14
14
  const serverConfig = useServerConfig();
15
- const { formatLanguageName } = useLocalFormat();
16
15
  const {
17
16
  settings: { contentLanguage },
18
17
  setContentLanguage,
19
18
  } = useUserSettings();
20
19
 
21
- // Fallback to empty array if serverConfig is null
22
- const languages = serverConfig?.availableLanguages || [];
20
+ // Map languages to code and label, then sort by label
21
+ const sortedLanguages = useSortedLanguages(serverConfig?.availableLanguages);
23
22
 
24
23
  // If no value is provided but languages are available, use the first language
25
24
  const currentValue = contentLanguage;
@@ -36,9 +35,9 @@ export function ContentLanguageSelector({ value, onChange, className }: ContentL
36
35
  <SelectValue placeholder="Select language" />
37
36
  </SelectTrigger>
38
37
  <SelectContent>
39
- {languages.map(language => (
40
- <SelectItem key={language} value={language}>
41
- {formatLanguageName(language)}
38
+ {sortedLanguages.map(({ code, label }) => (
39
+ <SelectItem key={code} value={code}>
40
+ {label}
42
41
  </SelectItem>
43
42
  ))}
44
43
  </SelectContent>
@@ -1,10 +1,11 @@
1
1
  import { CurrencyCode } from '@/vdb/constants.js';
2
2
  import { useDisplayLocale } from '@/vdb/hooks/use-display-locale.js';
3
3
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
4
+ import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
4
5
  import { useUiLanguageLoader } from '@/vdb/hooks/use-ui-language-loader.js';
5
6
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
6
7
  import { Trans } from '@lingui/react/macro';
7
- import { useState } from 'react';
8
+ import { useMemo, useState } from 'react';
8
9
  import { uiConfig } from 'virtual:vendure-ui-config';
9
10
  import { Button } from '../ui/button.js';
10
11
  import { DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog.js';
@@ -18,12 +19,24 @@ export function LanguageDialog() {
18
19
  const { settings, setDisplayLanguage, setDisplayLocale } = useUserSettings();
19
20
  const { humanReadableLanguageAndLocale } = useDisplayLocale();
20
21
  const availableCurrencyCodes = Object.values(CurrencyCode);
21
- const { formatCurrency, formatLanguageName, formatRegionName, formatCurrencyName, formatDate } =
22
- useLocalFormat();
22
+ const { formatCurrency, formatRegionName, formatCurrencyName, formatDate } = useLocalFormat();
23
23
  const [selectedCurrency, setSelectedCurrency] = useState<string>('USD');
24
24
 
25
- const orderedAvailableLanguages = availableLanguages.slice().sort((a, b) => a.localeCompare(b));
26
- const orderedAvailableLocales = availableLocales.slice().sort((a, b) => a.localeCompare(b));
25
+ // Map and sort languages by their formatted names
26
+ const sortedLanguages = useSortedLanguages(availableLanguages);
27
+
28
+ // Map and sort locales by their formatted region names
29
+ const sortedLocales = useMemo(
30
+ () =>
31
+ availableLocales
32
+ .map(code => ({
33
+ code,
34
+ label: formatRegionName(code),
35
+ }))
36
+ .sort((a, b) => a.label.localeCompare(b.label)),
37
+ [availableLocales, formatRegionName],
38
+ );
39
+
27
40
  const handleLanguageChange = async (value: string) => {
28
41
  setDisplayLanguage(value);
29
42
  void loadAndActivateLocale(value);
@@ -46,10 +59,10 @@ export function LanguageDialog() {
46
59
  <SelectValue placeholder="Select a language" />
47
60
  </SelectTrigger>
48
61
  <SelectContent>
49
- {orderedAvailableLanguages.map(language => (
50
- <SelectItem key={language} value={language} className="flex gap-1">
51
- <span className="uppercase text-muted-foreground">{language}</span>
52
- <span>{formatLanguageName(language)}</span>
62
+ {sortedLanguages.map(({ code, label }) => (
63
+ <SelectItem key={code} value={code} className="flex gap-1">
64
+ <span className="uppercase text-muted-foreground">{code}</span>
65
+ <span>{label}</span>
53
66
  </SelectItem>
54
67
  ))}
55
68
  </SelectContent>
@@ -64,10 +77,10 @@ export function LanguageDialog() {
64
77
  <SelectValue placeholder="Select a locale" />
65
78
  </SelectTrigger>
66
79
  <SelectContent>
67
- {orderedAvailableLocales.map(locale => (
68
- <SelectItem key={locale} value={locale} className="flex gap-1">
69
- <span className="uppercase text-muted-foreground">{locale}</span>
70
- <span>{formatRegionName(locale)}</span>
80
+ {sortedLocales.map(({ code, label }) => (
81
+ <SelectItem key={code} value={code} className="flex gap-1">
82
+ <span className="uppercase text-muted-foreground">{code}</span>
83
+ <span>{label}</span>
71
84
  </SelectItem>
72
85
  ))}
73
86
  </SelectContent>
@@ -17,6 +17,7 @@ import { graphql } from '@/vdb/graphql/graphql.js';
17
17
  import { useChannel } from '@/vdb/hooks/use-channel.js';
18
18
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
19
19
  import { usePermissions } from '@/vdb/hooks/use-permissions.js';
20
+ import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
20
21
  import { Trans } from '@lingui/react/macro';
21
22
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
22
23
  import { AlertCircle, Lock } from 'lucide-react';
@@ -115,6 +116,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
115
116
  const [channelLanguages, setChannelLanguages] = useState<string[]>([]);
116
117
  const [channelDefaultLanguage, setChannelDefaultLanguage] = useState<string>('');
117
118
 
119
+ // Map and sort channel languages by their formatted names
120
+ const sortedChannelLanguages = useSortedLanguages(channelLanguages || []);
121
+
118
122
  // Queries
119
123
  const {
120
124
  data: globalSettingsData,
@@ -363,7 +367,7 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
363
367
  )}
364
368
  </div>
365
369
 
366
- {channelLanguages.length > 0 && (
370
+ {sortedChannelLanguages.length > 0 && (
367
371
  <div>
368
372
  <Label className="text-sm font-medium mb-2 block">
369
373
  <Trans>Default Language</Trans>
@@ -377,10 +381,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
377
381
  <SelectValue placeholder="Select default language" />
378
382
  </SelectTrigger>
379
383
  <SelectContent>
380
- {channelLanguages.map(languageCode => (
381
- <SelectItem key={languageCode} value={languageCode}>
382
- {formatLanguageName(languageCode)} (
383
- {languageCode.toUpperCase()})
384
+ {sortedChannelLanguages.map(({ code, label }) => (
385
+ <SelectItem key={code} value={code}>
386
+ {label} ({code.toUpperCase()})
384
387
  </SelectItem>
385
388
  ))}
386
389
  </SelectContent>
@@ -1,8 +1,9 @@
1
1
  import { api } from '@/vdb/graphql/api.js';
2
2
  import { graphql } from '@/vdb/graphql/graphql.js';
3
- import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
+ import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
4
4
  import { useLingui } from '@lingui/react/macro';
5
5
  import { useQuery } from '@tanstack/react-query';
6
+ import { useMemo } from 'react';
6
7
  import { MultiSelect } from './multi-select.js';
7
8
 
8
9
  const availableGlobalLanguages = graphql(`
@@ -26,14 +27,21 @@ export function LanguageSelector<T extends boolean>(props: LanguageSelectorProps
26
27
  queryFn: () => api.query(availableGlobalLanguages),
27
28
  staleTime: 1000 * 60 * 5, // 5 minutes
28
29
  });
29
- const { formatLanguageName } = useLocalFormat();
30
30
  const { value, onChange, multiple, availableLanguageCodes } = props;
31
31
  const { t } = useLingui();
32
32
 
33
- const items = (availableLanguageCodes ?? data?.globalSettings.availableLanguages ?? []).map(language => ({
34
- value: language,
35
- label: formatLanguageName(language),
36
- }));
33
+ const sortedLanguages = useSortedLanguages(
34
+ availableLanguageCodes ?? data?.globalSettings.availableLanguages ?? undefined,
35
+ );
36
+
37
+ const items = useMemo(
38
+ () =>
39
+ sortedLanguages.map(language => ({
40
+ value: language.code,
41
+ label: language.label,
42
+ })),
43
+ [sortedLanguages],
44
+ );
37
45
 
38
46
  return (
39
47
  <MultiSelect
@@ -0,0 +1,41 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { useLocalFormat } from './use-local-format.js';
4
+
5
+ export interface SortedLanguage {
6
+ code: string;
7
+ label: string;
8
+ }
9
+
10
+ /**
11
+ * @description
12
+ * This hook takes an array of language codes and returns a sorted array of language objects
13
+ * with code and localized label, sorted alphabetically by the label.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const sortedLanguages = useSortedLanguages(['en', 'fr', 'de']);
18
+ * // Returns: [{ code: 'de', label: 'German' }, { code: 'en', label: 'English' }, { code: 'fr', label: 'French' }]
19
+ * ```
20
+ *
21
+ * @param availableLanguages - Array of language codes to sort
22
+ * @returns Sorted array of language objects with code and label
23
+ *
24
+ * @docsCategory hooks
25
+ * @docsPage useSortedLanguages
26
+ * @docsWeight 0
27
+ */
28
+ export function useSortedLanguages(availableLanguages?: string[] | null): SortedLanguage[] {
29
+ const { formatLanguageName } = useLocalFormat();
30
+
31
+ return useMemo(
32
+ () =>
33
+ (availableLanguages ?? [])
34
+ .map(code => ({
35
+ code,
36
+ label: formatLanguageName(code),
37
+ }))
38
+ .sort((a, b) => a.label.localeCompare(b.label)),
39
+ [availableLanguages, formatLanguageName],
40
+ );
41
+ }