@vendure/dashboard 3.6.0-minor-202511061555 → 3.6.0-minor-202512161252

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 (152) hide show
  1. package/dist/plugin/constants.js +2 -2
  2. package/dist/vite/constants.js +1 -0
  3. package/dist/vite/utils/compiler.d.ts +1 -0
  4. package/dist/vite/utils/compiler.js +5 -4
  5. package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
  6. package/dist/vite/utils/get-dashboard-paths.js +20 -0
  7. package/dist/vite/vite-plugin-dashboard-metadata.js +2 -1
  8. package/dist/vite/vite-plugin-tailwind-source.js +2 -15
  9. package/dist/vite/vite-plugin-translations.d.ts +10 -1
  10. package/dist/vite/vite-plugin-translations.js +156 -45
  11. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
  12. package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
  13. package/lingui.config.js +1 -0
  14. package/package.json +7 -7
  15. package/src/app/routeTree.gen.ts +1221 -0
  16. package/src/app/routes/_authenticated/_administrators/administrators.tsx +9 -12
  17. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +9 -12
  18. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +6 -9
  19. package/src/app/routes/_authenticated/_channels/channels.tsx +9 -12
  20. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +9 -12
  21. package/src/app/routes/_authenticated/_collections/collections.tsx +9 -12
  22. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +9 -12
  23. package/src/app/routes/_authenticated/_countries/countries.tsx +9 -12
  24. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -12
  25. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +9 -12
  26. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +9 -12
  27. package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
  28. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -12
  29. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +9 -12
  30. package/src/app/routes/_authenticated/_facets/facets.tsx +9 -12
  31. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +9 -12
  32. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +9 -12
  33. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +10 -13
  34. package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
  35. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
  36. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +3 -1
  37. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
  38. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +41 -41
  39. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
  40. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
  41. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +4 -1
  42. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +2 -3
  43. package/src/app/routes/_authenticated/_orders/orders.tsx +3 -3
  44. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +12 -3
  45. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +27 -30
  46. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
  47. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +9 -12
  48. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +9 -12
  49. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
  50. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
  51. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
  52. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +10 -12
  53. package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
  54. package/src/app/routes/_authenticated/_products/products.tsx +15 -18
  55. package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -12
  56. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +9 -12
  57. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +9 -12
  58. package/src/app/routes/_authenticated/_profile/profile.tsx +3 -3
  59. package/src/app/routes/_authenticated/_promotions/promotions.tsx +9 -12
  60. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +9 -12
  61. package/src/app/routes/_authenticated/_roles/roles.tsx +9 -12
  62. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +9 -12
  63. package/src/app/routes/_authenticated/_sellers/sellers.tsx +9 -12
  64. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +9 -12
  65. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +11 -12
  66. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +19 -20
  67. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +9 -12
  68. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +9 -12
  69. package/src/app/routes/_authenticated/_system/healthchecks.tsx +2 -3
  70. package/src/app/routes/_authenticated/_system/job-queue.tsx +3 -3
  71. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +9 -12
  72. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -12
  73. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -12
  74. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +9 -12
  75. package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +49 -1
  76. package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +34 -16
  77. package/src/app/routes/_authenticated/_zones/zones.tsx +9 -12
  78. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +9 -12
  79. package/src/app/routes/_authenticated/index.tsx +5 -3
  80. package/src/i18n/locales/bg.po +3436 -0
  81. package/src/lib/components/data-input/datetime-input.tsx +1 -1
  82. package/src/lib/components/data-input/default-relation-input.tsx +1 -1
  83. package/src/lib/components/data-input/relation-selector.tsx +1 -1
  84. package/src/lib/components/data-input/string-list-input.tsx +188 -26
  85. package/src/lib/components/data-input/struct-form-input.tsx +175 -174
  86. package/src/lib/components/data-table/column-header-wrapper.tsx +1 -1
  87. package/src/lib/components/data-table/data-table-filter-badge.tsx +2 -2
  88. package/src/lib/components/data-table/data-table.tsx +1 -1
  89. package/src/lib/components/data-table/use-generated-columns.tsx +1 -1
  90. package/src/lib/components/layout/channel-switcher.tsx +6 -2
  91. package/src/lib/components/layout/content-language-selector.tsx +6 -7
  92. package/src/lib/components/layout/dev-mode-indicator.tsx +7 -3
  93. package/src/lib/components/layout/language-dialog.tsx +26 -13
  94. package/src/lib/components/layout/manage-languages-dialog.tsx +10 -29
  95. package/src/lib/components/layout/nav-item-wrapper.tsx +1 -1
  96. package/src/lib/components/shared/asset/asset-gallery.tsx +8 -3
  97. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +14 -16
  98. package/src/lib/components/shared/custom-fields-form.tsx +14 -9
  99. package/src/lib/components/shared/language-selector.tsx +14 -6
  100. package/src/lib/components/shared/multi-select.tsx +1 -1
  101. package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
  102. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +4 -4
  103. package/src/lib/components/ui/carousel.tsx +2 -2
  104. package/src/lib/components/ui/chart.tsx +1 -1
  105. package/src/lib/components/ui/context-menu.tsx +1 -1
  106. package/src/lib/components/ui/drawer.tsx +1 -1
  107. package/src/lib/components/ui/grid-layout.tsx +1 -1
  108. package/src/lib/components/ui/input-group.tsx +1 -0
  109. package/src/lib/components/ui/input-otp.tsx +1 -1
  110. package/src/lib/components/ui/menubar.tsx +1 -1
  111. package/src/lib/components/ui/navigation-menu.tsx +1 -1
  112. package/src/lib/components/ui/progress.tsx +1 -1
  113. package/src/lib/components/ui/radio-group.tsx +1 -1
  114. package/src/lib/components/ui/resizable.tsx +1 -1
  115. package/src/lib/components/ui/select.tsx +1 -1
  116. package/src/lib/components/ui/slider.tsx +1 -1
  117. package/src/lib/components/ui/toggle-group.tsx +2 -2
  118. package/src/lib/components/ui/toggle.tsx +1 -1
  119. package/src/lib/framework/component-registry/component-registry.tsx +2 -6
  120. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +907 -1
  121. package/src/lib/framework/document-introspection/add-custom-fields.ts +248 -119
  122. package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
  123. package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
  124. package/src/lib/framework/extension-api/logic/navigation.ts +1 -1
  125. package/src/lib/framework/extension-api/types/data-table.ts +4 -2
  126. package/src/lib/framework/extension-api/types/layout.ts +34 -1
  127. package/src/lib/framework/extension-api/types/navigation.ts +7 -2
  128. package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
  129. package/src/lib/framework/history-entry/history-entry.tsx +1 -1
  130. package/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx +185 -0
  131. package/src/lib/framework/layout-engine/dev-mode-button.tsx +15 -13
  132. package/src/lib/framework/layout-engine/location-wrapper.tsx +3 -1
  133. package/src/lib/framework/layout-engine/page-layout.spec.tsx +138 -0
  134. package/src/lib/framework/layout-engine/page-layout.tsx +294 -69
  135. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
  136. package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
  137. package/src/lib/framework/page/page-api.ts +1 -1
  138. package/src/lib/framework/page/use-detail-page.ts +4 -2
  139. package/src/lib/framework/page/use-extended-router.tsx +20 -16
  140. package/src/lib/framework/registry/registry-types.ts +2 -1
  141. package/src/lib/graphql/api.ts +3 -8
  142. package/src/lib/graphql/graphql-env.d.ts +29 -10
  143. package/src/lib/hooks/use-permissions.ts +3 -3
  144. package/src/lib/hooks/use-sorted-languages.ts +41 -0
  145. package/src/lib/index.ts +1 -0
  146. package/src/lib/lib/load-i18n-messages.ts +4 -1
  147. package/src/lib/providers/channel-provider.tsx +11 -7
  148. package/src/lib/utils/config-utils.ts +19 -0
  149. package/src/lib/virtual.d.ts +3 -0
  150. package/LICENSE.md +0 -42
  151. package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
  152. /package/src/{app/routes/_authenticated/_global-settings → lib}/utils/global-languages.ts +0 -0
@@ -20,7 +20,7 @@ export function DataTableFilterBadge({
20
20
  const [operator, value] = Object.entries(filter.value as Record<string, unknown>)[0];
21
21
  return (
22
22
  <Badge key={filter.id} className="flex gap-2 flex-wrap items-center" variant="outline">
23
- <div
23
+ <button
24
24
  className="flex gap-1 flex-wrap items-center cursor-pointer flex-1"
25
25
  onClick={() => onClick?.(filter)}
26
26
  >
@@ -37,7 +37,7 @@ export function DataTableFilterBadge({
37
37
  <div className="@xs:overflow-hidden @xs:text-ellipsis @xs:whitespace-nowrap flex flex-col @xl:flex-row @2xl:gap-1">
38
38
  <FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
39
39
  </div>
40
- </div>
40
+ </button>
41
41
  <button className="border-l -mr-2" onClick={() => onRemove(filter)}>
42
42
  <XIcon className="h-4 flex-shrink-0 cursor-pointer" />
43
43
  </button>
@@ -209,7 +209,6 @@ export function DataTable<TData>({
209
209
  ...pagination,
210
210
  pageIndex: 0,
211
211
  });
212
- pagination.pageIndex;
213
212
  }
214
213
  prevColumnFiltersRef.current = columnFilters;
215
214
  }, [columnFilters]);
@@ -255,6 +254,7 @@ export function DataTable<TData>({
255
254
  title={filter?.title}
256
255
  options={filter?.options}
257
256
  optionsFn={filter?.optionsFn}
257
+ icon={filter?.icon}
258
258
  />
259
259
  ))}
260
260
  </Suspense>
@@ -163,7 +163,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
163
163
  if (!id) {
164
164
  throw new Error('Column id is required');
165
165
  }
166
- finalColumns.push(columnHelper.accessor(id as any, { ...column, id, enableColumnFilter: false }));
166
+ finalColumns.push(columnHelper.accessor(id as any, { enableColumnFilter: false, ...column, id }));
167
167
  }
168
168
 
169
169
  if (defaultColumnOrder) {
@@ -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>
@@ -2,14 +2,18 @@ import { Badge } from '@/vdb/components/ui/badge.js';
2
2
  import { Button } from '@/vdb/components/ui/button.js';
3
3
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
4
4
  import { Trans } from '@lingui/react/macro';
5
- import { CodeXmlIcon, XIcon } from 'lucide-react';
5
+ import { SquareDashedMousePointer, XIcon } from 'lucide-react';
6
6
 
7
7
  export function DevModeIndicator() {
8
8
  const { setDevMode } = useUserSettings();
9
9
  return (
10
10
  <Badge className="bg-dev-mode text-background">
11
- <CodeXmlIcon className="w-6 h-6" />
12
- <Trans>Dev Mode</Trans>
11
+ <div>
12
+ <SquareDashedMousePointer className="w-4 h-4" />
13
+ </div>
14
+ <div>
15
+ <Trans>Dev Mode</Trans>
16
+ </div>
13
17
  <Button variant="ghost" size="icon-xs" onClick={() => setDevMode(false)}>
14
18
  <XIcon className="w-4 h-4" />
15
19
  </Button>
@@ -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,8 @@ 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';
21
+ import { globalLanguageCodes } from '@/vdb/utils/global-languages.js';
20
22
  import { Trans } from '@lingui/react/macro';
21
23
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
22
24
  import { AlertCircle, Lock } from 'lucide-react';
@@ -67,35 +69,12 @@ const updateChannelDocument = graphql(`
67
69
  }
68
70
  `);
69
71
 
70
- // All possible language codes for global settings - includes more than what might be globally enabled
71
- const ALL_LANGUAGE_CODES = [
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',
90
- ];
91
-
92
72
  interface ManageLanguagesDialogProps {
93
73
  open: boolean;
94
74
  onClose: () => void;
95
75
  }
96
76
 
97
77
  export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogProps) {
98
- const { formatLanguageName } = useLocalFormat();
99
78
  const { activeChannel } = useChannel();
100
79
  const { hasPermissions } = usePermissions();
101
80
  const queryClient = useQueryClient();
@@ -114,6 +93,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
114
93
  const [channelLanguages, setChannelLanguages] = useState<string[]>([]);
115
94
  const [channelDefaultLanguage, setChannelDefaultLanguage] = useState<string>('');
116
95
 
96
+ // Map and sort channel languages by their formatted names
97
+ const sortedChannelLanguages = useSortedLanguages(channelLanguages || []);
98
+
117
99
  // Queries
118
100
  const {
119
101
  data: globalSettingsData,
@@ -304,7 +286,7 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
304
286
  value={globalLanguages}
305
287
  onChange={handleGlobalLanguagesChange}
306
288
  multiple={true}
307
- availableLanguageCodes={ALL_LANGUAGE_CODES}
289
+ availableLanguageCodes={globalLanguageCodes}
308
290
  />
309
291
  </div>
310
292
  <p className="text-xs text-muted-foreground">
@@ -362,7 +344,7 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
362
344
  )}
363
345
  </div>
364
346
 
365
- {channelLanguages.length > 0 && (
347
+ {sortedChannelLanguages.length > 0 && (
366
348
  <div>
367
349
  <Label className="text-sm font-medium mb-2 block">
368
350
  <Trans>Default Language</Trans>
@@ -376,10 +358,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
376
358
  <SelectValue placeholder="Select default language" />
377
359
  </SelectTrigger>
378
360
  <SelectContent>
379
- {channelLanguages.map(languageCode => (
380
- <SelectItem key={languageCode} value={languageCode}>
381
- {formatLanguageName(languageCode)} (
382
- {languageCode.toUpperCase()})
361
+ {sortedChannelLanguages.map(({ code, label }) => (
362
+ <SelectItem key={code} value={code}>
363
+ {label} ({code.toUpperCase()})
383
364
  </SelectItem>
384
365
  ))}
385
366
  </SelectContent>
@@ -79,7 +79,7 @@ export function NavItemWrapper({
79
79
  >
80
80
  <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
81
81
  <PopoverTrigger asChild>
82
- <DevModeButton className={`h-6 w-6`} />
82
+ <DevModeButton className={`h-5 w-5 top-0 end-0`} />
83
83
  </PopoverTrigger>
84
84
  <PopoverContent className="w-48 p-3">
85
85
  <div className="space-y-2">
@@ -13,6 +13,7 @@ import {
13
13
  PaginationPrevious,
14
14
  } from '@/vdb/components/ui/pagination.js';
15
15
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
16
+ import { ActionBarItem, PageActionBar } from '@/vdb/framework/layout-engine/page-layout.js';
16
17
  import { api } from '@/vdb/graphql/api.js';
17
18
  import { assetFragment, AssetFragment } from '@/vdb/graphql/fragments.js';
18
19
  import { graphql } from '@/vdb/graphql/graphql.js';
@@ -357,9 +358,13 @@ export function AssetGallery({
357
358
  <SelectItem value={AssetType.BINARY}>Binary</SelectItem>
358
359
  </SelectContent>
359
360
  </Select>
360
- <Button onClick={openFileDialog} className="whitespace-nowrap">
361
- <Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
362
- </Button>
361
+ <PageActionBar>
362
+ <ActionBarItem itemId="upload-assets-button">
363
+ <Button onClick={openFileDialog} className="whitespace-nowrap">
364
+ <Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
365
+ </Button>
366
+ </ActionBarItem>
367
+ </PageActionBar>
363
368
  </div>
364
369
 
365
370
  {hasTags && (
@@ -94,17 +94,17 @@ type QueryData = {
94
94
  * ```
95
95
  */
96
96
  export function ConfigurableOperationMultiSelector({
97
- value,
98
- onChange,
99
- queryDocument,
100
- queryOptions,
101
- queryKey,
102
- dataPath,
103
- buttonText,
104
- dropdownTitle,
105
- emptyText = 'No options found',
106
- showEnhancedDropdown = true,
107
- }: Readonly<ConfigurableOperationMultiSelectorProps>) {
97
+ value,
98
+ onChange,
99
+ queryDocument,
100
+ queryOptions,
101
+ queryKey,
102
+ dataPath,
103
+ buttonText,
104
+ dropdownTitle,
105
+ emptyText = 'No options found',
106
+ showEnhancedDropdown = true,
107
+ }: Readonly<ConfigurableOperationMultiSelectorProps>) {
108
108
  const { data } = useQuery<QueryData>(
109
109
  queryOptions || {
110
110
  queryKey: [queryKey],
@@ -134,7 +134,7 @@ export function ConfigurableOperationMultiSelector({
134
134
  code: operation.code,
135
135
  arguments: operationDef.args.map(arg => ({
136
136
  name: arg.name,
137
- value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
137
+ value: arg.defaultValue != null ? arg.defaultValue.toString() : arg.list ? '[]' : '',
138
138
  })),
139
139
  },
140
140
  ]);
@@ -195,10 +195,8 @@ export function ConfigurableOperationMultiSelector({
195
195
  onCombinationModeChange(index, newValue)
196
196
  }
197
197
  name={''}
198
- ref={() => {
199
- }}
200
- onBlur={() => {
201
- }}
198
+ ref={() => {}}
199
+ onBlur={() => {}}
202
200
  position={index}
203
201
  />
204
202
  </div>
@@ -10,6 +10,7 @@ import {
10
10
  } from '@/vdb/components/ui/form.js';
11
11
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/vdb/components/ui/tabs.js';
12
12
  import { CustomFormComponent } from '@/vdb/framework/form-engine/custom-form-component.js';
13
+ import { ConfigurableFieldDef } from '@/vdb/framework/form-engine/form-engine-types.js';
13
14
  import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
14
15
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
15
16
  import { customFieldConfigFragment } from '@/vdb/providers/server-config.js';
@@ -20,7 +21,7 @@ import { Control } from 'react-hook-form';
20
21
  import { FormControlAdapter } from '../../framework/form-engine/form-control-adapter.js';
21
22
  import { TranslatableFormField } from './translatable-form-field.js';
22
23
 
23
- type CustomFieldConfig = ResultOf<typeof customFieldConfigFragment>;
24
+ type CustomFieldConfig = Omit<ResultOf<typeof customFieldConfigFragment>, '__typename'>;
24
25
 
25
26
  interface CustomFieldsFormProps {
26
27
  entityType: string;
@@ -72,7 +73,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Readon
72
73
  const shouldShowTabs = useMemo(() => {
73
74
  if (!customFields) return false;
74
75
  const hasTabbedFields = customFields.some(field => field.ui?.tab);
75
- return hasTabbedFields || groupedFields.length > 1;
76
+ return hasTabbedFields && groupedFields.length > 1;
76
77
  }, [customFields, groupedFields.length]);
77
78
 
78
79
  if (!shouldShowTabs) {
@@ -120,8 +121,8 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Readon
120
121
  }
121
122
 
122
123
  interface CustomFieldItemProps {
123
- fieldDef: CustomFieldConfig;
124
- control: Control<any, any>;
124
+ fieldDef: ConfigurableFieldDef;
125
+ control: Control<any>;
125
126
  fieldName: string;
126
127
  }
127
128
 
@@ -130,14 +131,19 @@ function CustomFieldItem({ fieldDef, control, fieldName }: Readonly<CustomFieldI
130
131
  settings: { displayLanguage },
131
132
  } = useUserSettings();
132
133
 
133
- const getTranslation = (input: Array<{ languageCode: string; value: string }> | null | undefined) => {
134
+ const getTranslation = (
135
+ input: string | Array<{ languageCode: string; value: string }> | null | undefined,
136
+ ) => {
137
+ if (typeof input === 'string') {
138
+ return input;
139
+ }
134
140
  return input?.find(t => t.languageCode === displayLanguage)?.value;
135
141
  };
136
142
  const hasCustomFormComponent = fieldDef.ui?.component;
137
143
  const isLocaleField = fieldDef.type === 'localeString' || fieldDef.type === 'localeText';
138
144
  const shouldBeFullWidth = fieldDef.ui?.fullWidth === true;
139
145
  const containerClassName = shouldBeFullWidth ? 'col-span-2' : '';
140
- const isReadonly = fieldDef.readonly ?? false;
146
+ const isReadonly = (fieldDef as CustomFieldConfig).readonly ?? false;
141
147
 
142
148
  // For locale fields, always use TranslatableFormField regardless of custom components
143
149
  if (isLocaleField) {
@@ -212,7 +218,6 @@ function CustomFieldItem({ fieldDef, control, fieldName }: Readonly<CustomFieldI
212
218
  <StructFormInput {...inputField} fieldDef={fieldDef} />
213
219
  )}
214
220
  defaultValue={{}} // Empty struct object as default
215
- isFullWidth={true} // Structs should always be full-width
216
221
  />
217
222
  </FormControl>
218
223
  <FormDescription>{getTranslation(fieldDef.description)}</FormDescription>
@@ -266,9 +271,9 @@ function CustomFieldItem({ fieldDef, control, fieldName }: Readonly<CustomFieldI
266
271
  }
267
272
 
268
273
  interface CustomFieldFormItemProps {
269
- fieldDef: CustomFieldConfig;
274
+ fieldDef: ConfigurableFieldDef;
270
275
  getTranslation: (
271
- input: Array<{ languageCode: string; value: string }> | null | undefined,
276
+ input: string | Array<{ languageCode: string; value: string }> | null | undefined,
272
277
  ) => string | undefined;
273
278
  fieldName: string;
274
279
  children: React.ReactNode;
@@ -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
@@ -127,7 +127,7 @@ export function MultiSelect<T extends boolean>(props: MultiSelectProps<T>) {
127
127
  return (
128
128
  <Popover>
129
129
  <PopoverTrigger asChild>{renderTrigger()}</PopoverTrigger>
130
- <PopoverContent className="w-[200px] p-0" side="bottom" align="start">
130
+ <PopoverContent className="w-[200px] p-0" side="bottom" align="start" onWheel={(e) => e.stopPropagation()}>
131
131
  {(showSearch === true || items.length > 10) && (
132
132
  <div className="p-2">
133
133
  <Input
@@ -34,7 +34,7 @@ export function NavigationConfirmation(props: Readonly<NavigationConfirmationPro
34
34
  return props.form.formState.isDirty;
35
35
  },
36
36
  withResolver: true,
37
- enableBeforeUnload: true,
37
+ enableBeforeUnload: () => props.form.formState.isDirty,
38
38
  });
39
39
  return (
40
40
  <Dialog open={status === 'blocked'} onOpenChange={reset}>
@@ -47,10 +47,10 @@ export const RichTextDescriptionCell: DataTableCellComponent<{ description: stri
47
47
 
48
48
  // Strip HTML tags and decode HTML entities
49
49
  const textContent = useMemo(() => {
50
- const stripped = value?.replace(/<[^>]*>/g, '') || '';
51
- const textArea = document.createElement('textarea');
52
- textArea.innerHTML = stripped;
53
- return textArea.value;
50
+ if (!value) return '';
51
+ const div = document.createElement('div');
52
+ div.innerHTML = value;
53
+ return div.textContent ?? '';
54
54
  }, [value]);
55
55
 
56
56
  const shortLength = 100;
@@ -6,8 +6,8 @@ import useEmblaCarousel, {
6
6
  } from "embla-carousel-react"
7
7
  import { ArrowLeft, ArrowRight } from "lucide-react"
8
8
 
9
- import { cn } from "@/vdb/lib/utils"
10
- import { Button } from "@/vdb/components/ui/button"
9
+ import { cn } from "@/vdb/lib/utils.js"
10
+ import { Button } from "@/vdb/components/ui/button.js"
11
11
 
12
12
  type CarouselApi = UseEmblaCarouselType[1]
13
13
  type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
  import * as RechartsPrimitive from "recharts"
3
3
 
4
- import { cn } from "@/vdb/lib/utils"
4
+ import { cn } from "@/vdb/lib/utils.js"
5
5
 
6
6
  // Format: { THEME_NAME: CSS_SELECTOR }
7
7
  const THEMES = { light: "", dark: ".dark" } as const
@@ -4,7 +4,7 @@ import * as React from "react"
4
4
  import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
5
5
  import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
6
 
7
- import { cn } from "@/vdb/lib/utils"
7
+ import { cn } from "@/vdb/lib/utils.js"
8
8
 
9
9
  function ContextMenu({
10
10
  ...props
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { Drawer as DrawerPrimitive } from "vaul"
3
3
 
4
- import { cn } from "@/vdb/lib/utils"
4
+ import { cn } from "@/vdb/lib/utils.js"
5
5
 
6
6
  function Drawer({
7
7
  ...props
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { useState, useRef, useCallback, useEffect } from "react";
3
- import { cn } from "@/vdb/lib/utils";
3
+ import { cn } from "@/vdb/lib/utils.js";
4
4
 
5
5
  export interface GridLayout {
6
6
  x: number;
@@ -127,6 +127,7 @@ function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>)
127
127
  'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
128
128
  className,
129
129
  )}
130
+ value={props.value}
130
131
  {...props}
131
132
  />
132
133
  );
@@ -4,7 +4,7 @@ import * as React from "react"
4
4
  import { OTPInput, OTPInputContext } from "input-otp"
5
5
  import { MinusIcon } from "lucide-react"
6
6
 
7
- import { cn } from "@/vdb/lib/utils"
7
+ import { cn } from "@/vdb/lib/utils.js"
8
8
 
9
9
  function InputOTP({
10
10
  className,
@@ -2,7 +2,7 @@ import * as React from "react"
2
2
  import * as MenubarPrimitive from "@radix-ui/react-menubar"
3
3
  import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
4
4
 
5
- import { cn } from "@/vdb/lib/utils"
5
+ import { cn } from "@/vdb/lib/utils.js"
6
6
 
7
7
  function Menubar({
8
8
  className,
@@ -3,7 +3,7 @@ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3
3
  import { cva } from "class-variance-authority"
4
4
  import { ChevronDownIcon } from "lucide-react"
5
5
 
6
- import { cn } from "@/vdb/lib/utils"
6
+ import { cn } from "@/vdb/lib/utils.js"
7
7
 
8
8
  function NavigationMenu({
9
9
  className,