@vendure/dashboard 3.5.0-minor-202510031341 → 3.5.0-minor-202510071456

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 (26) hide show
  1. package/dist/plugin/default-page.html +1 -1
  2. package/dist/vite/utils/tsconfig-utils.js +2 -1
  3. package/package.json +5 -4
  4. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +4 -8
  5. package/src/app/routes/_authenticated/_global-settings/utils/global-languages.ts +268 -0
  6. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +15 -15
  7. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +4 -4
  8. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +49 -0
  9. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +56 -0
  10. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +12 -0
  11. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +178 -50
  12. package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +0 -11
  13. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +3 -14
  14. package/src/lib/components/data-input/customer-group-input.tsx +0 -1
  15. package/src/lib/components/data-input/money-input.tsx +7 -11
  16. package/src/lib/components/data-input/number-input.tsx +6 -1
  17. package/src/lib/components/data-table/data-table-filter-badge.tsx +15 -8
  18. package/src/lib/components/data-table/data-table.tsx +2 -2
  19. package/src/lib/components/layout/generated-breadcrumbs.tsx +4 -12
  20. package/src/lib/components/shared/configurable-operation-input.tsx +1 -1
  21. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +0 -2
  22. package/src/lib/framework/extension-api/types/layout.ts +41 -1
  23. package/src/lib/framework/form-engine/value-transformers.ts +8 -1
  24. package/src/lib/framework/layout-engine/page-layout.tsx +58 -48
  25. package/src/lib/framework/page/detail-page.tsx +12 -15
  26. package/src/lib/providers/channel-provider.tsx +1 -0
@@ -108,7 +108,7 @@
108
108
  </head>
109
109
  <body>
110
110
  <div class="container">
111
- <svg viewBox="0 0 539 100" fill="none" xmlns="http://www.w3.org/2000/svg">
111
+ <svg class="logo" viewBox="0 0 539 100" fill="none" xmlns="http://www.w3.org/2000/svg">
112
112
  <path d="M198.715 29.7044C198.311 29.1369 197.638 28.7809 196.916 28.7809H189.317C188.393 28.7809 187.547 29.3774 187.239 30.2431L174.3 67.6252L161.16 30.2239C160.862 29.3581 160.034 28.7617 159.082 28.7617H151.261C150.54 28.7617 149.886 29.108 149.462 29.6852C149.058 30.2527 148.962 31.0031 149.183 31.6764L167.817 82.9974C168.115 83.8632 168.942 84.4404 169.866 84.4404H178.139C179.062 84.4404 179.889 83.8728 180.188 82.9974L198.927 31.6764C199.177 31.0031 199.081 30.2623 198.648 29.6852L198.696 29.714L198.715 29.7044Z" fill="#17C1FF"/>
113
113
  <path d="M252.778 58.3518C252.855 57.5342 252.903 56.4086 252.903 54.9465C252.903 49.7807 251.729 45.0286 249.43 40.8055C247.131 36.5825 243.88 33.1771 239.839 30.7145C235.77 28.2518 231.143 26.982 226.15 26.982C220.85 26.982 216.031 28.2711 211.836 30.8107C207.642 33.3503 204.343 36.9288 201.995 41.4212C199.667 45.8655 198.494 50.9639 198.494 56.553C198.494 62.142 199.696 67.2404 202.044 71.7135C204.42 76.2156 207.767 79.7941 211.99 82.3241C216.213 84.8636 221.081 86.1527 226.458 86.1527C232.23 86.1527 237.329 84.9887 241.619 82.6511C245.89 80.3135 249.392 77.0621 252.008 72.9834C252.633 71.9925 252.354 70.6746 251.383 70.0012L245.438 66.0476C244.938 65.7205 244.341 65.5955 243.735 65.7205C243.158 65.8456 242.639 66.2207 242.34 66.7113C240.792 69.299 238.695 71.3384 236.145 72.8006C233.596 74.2435 230.277 74.9842 226.256 74.9842C221.456 74.9842 217.791 73.4162 215.04 70.1648C212.644 67.327 211.163 64.0755 210.595 60.2469H250.613C251.739 60.2469 252.691 59.4004 252.787 58.2845V58.3326L252.778 58.3518ZM211.355 49.8288C212.25 46.7986 213.78 44.2397 215.925 42.0753C218.57 39.4395 221.899 38.1505 226.122 38.1505C230.345 38.1505 233.846 39.2664 236.318 41.5558C238.339 43.4413 239.791 46.231 240.59 49.8095H211.336L211.365 49.8384L211.355 49.8288Z" fill="#17C1FF"/>
114
114
  <path d="M305.417 36.6883C303.396 33.5042 300.645 31.0512 297.25 29.4062C293.931 27.819 290.227 26.9917 286.331 26.9917C282.108 26.9917 278.337 28.0114 275.086 30.0026C273.758 30.8203 272.537 31.7919 271.44 32.8885V30.9261C271.44 29.7333 270.468 28.7425 269.237 28.7425H261.743C260.541 28.7425 259.54 29.714 259.54 30.9261V82.2471C259.54 83.4399 260.512 84.4308 261.743 84.4308H269.237C270.439 84.4308 271.44 83.4592 271.44 82.2471V56.5722C271.44 50.7811 272.642 46.1348 275.009 42.8064C277.231 39.6704 280.559 38.1312 285.129 38.1312C292.978 38.1312 296.624 42.2773 296.624 51.2044V82.2471C296.624 83.4399 297.596 84.4308 298.827 84.4308H306.321C307.523 84.4308 308.524 83.4592 308.524 82.2471V47.9241C308.524 43.653 307.504 39.8724 305.503 36.6883H305.426H305.417Z" fill="#17C1FF"/>
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
+ import stripJsonComments from 'strip-json-comments';
3
4
  /**
4
5
  * Finds and parses tsconfig files in the given directory and its parent directories.
5
6
  */
@@ -33,7 +34,7 @@ export async function findTsConfigPaths(configPath, logger, phase, transformTsCo
33
34
  }
34
35
  async function getCompilerOptionsFromFile(tsConfigFilePath) {
35
36
  const tsConfigContent = await fs.readFile(tsConfigFilePath, 'utf-8');
36
- const tsConfig = JSON.parse(tsConfigContent);
37
+ const tsConfig = JSON.parse(stripJsonComments(tsConfigContent));
37
38
  return tsConfig.compilerOptions || {};
38
39
  }
39
40
  function getTransformedPathMappings(paths, phase, transformTsConfigPathMappings) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.5.0-minor-202510031341",
4
+ "version": "3.5.0-minor-202510071456",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -134,6 +134,7 @@
134
134
  "react-resizable-panels": "^3.0.3",
135
135
  "recharts": "^2.15.4",
136
136
  "sonner": "^2.0.6",
137
+ "strip-json-comments": "^5.0.3",
137
138
  "tailwind-merge": "^3.2.0",
138
139
  "tailwindcss": "^4.1.5",
139
140
  "tailwindcss-animate": "^1.0.7",
@@ -146,8 +147,8 @@
146
147
  "devDependencies": {
147
148
  "@eslint/js": "^9.19.0",
148
149
  "@types/node": "^22.13.4",
149
- "@vendure/common": "^3.5.0-minor-202510031341",
150
- "@vendure/core": "^3.5.0-minor-202510031341",
150
+ "@vendure/common": "^3.5.0-minor-202510071456",
151
+ "@vendure/core": "^3.5.0-minor-202510071456",
151
152
  "eslint": "^9.19.0",
152
153
  "eslint-plugin-react": "^7.37.4",
153
154
  "eslint-plugin-react-hooks": "^5.0.0",
@@ -159,5 +160,5 @@
159
160
  "lightningcss-linux-arm64-musl": "^1.29.3",
160
161
  "lightningcss-linux-x64-musl": "^1.29.1"
161
162
  },
162
- "gitHead": "ffe957ddc6b419f17fced0b4b94fe66fc42528f8"
163
+ "gitHead": "a0d7f3f10de46e0ab1825b412baf42093dbd9ebf"
163
164
  }
@@ -1,9 +1,9 @@
1
+ import { NumberInput } from '@/vdb/components/data-input/number-input.js';
1
2
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
2
3
  import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
3
4
  import { LanguageSelector } from '@/vdb/components/shared/language-selector.js';
4
5
  import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
5
6
  import { Button } from '@/vdb/components/ui/button.js';
6
- import { Input } from '@/vdb/components/ui/input.js';
7
7
  import { Switch } from '@/vdb/components/ui/switch.js';
8
8
  import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
9
9
  import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
@@ -23,6 +23,7 @@ import { Trans, useLingui } from '@lingui/react/macro';
23
23
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
24
24
  import { toast } from 'sonner';
25
25
  import { globalSettingsDocument, updateGlobalSettingsDocument } from './global-settings.graphql.js';
26
+ import { globalLanguageCodes } from './utils/global-languages.js';
26
27
 
27
28
  const pageId = 'global-settings';
28
29
 
@@ -119,6 +120,7 @@ function GlobalSettingsPage() {
119
120
  <LanguageSelector
120
121
  value={field.value ?? []}
121
122
  onChange={field.onChange}
123
+ availableLanguageCodes={globalLanguageCodes}
122
124
  multiple={true}
123
125
  />
124
126
  )}
@@ -134,13 +136,7 @@ function GlobalSettingsPage() {
134
136
  by product variants.
135
137
  </Trans>
136
138
  }
137
- render={({ field }) => (
138
- <Input
139
- value={field.value ?? []}
140
- onChange={e => field.onChange(Number(e.target.valueAsNumber))}
141
- type="number"
142
- />
143
- )}
139
+ render={({ field }) => <NumberInput {...field} />}
144
140
  />
145
141
  <FormFieldWrapper
146
142
  control={form.control}
@@ -0,0 +1,268 @@
1
+ export const globalLanguageCodes = [
2
+ /** Afrikaans */
3
+ 'af',
4
+ /** Akan */
5
+ 'ak',
6
+ /** Amharic */
7
+ 'am',
8
+ /** Arabic */
9
+ 'ar',
10
+ /** Assamese */
11
+ 'as',
12
+ /** Azerbaijani */
13
+ 'az',
14
+ /** Belarusian */
15
+ 'be',
16
+ /** Bulgarian */
17
+ 'bg',
18
+ /** Bambara */
19
+ 'bm',
20
+ /** Bangla */
21
+ 'bn',
22
+ /** Breton */
23
+ 'br',
24
+ /** Bosnian */
25
+ 'bs',
26
+ /** Catalan */
27
+ 'ca',
28
+ /** Chechen */
29
+ 'co',
30
+ /** Czech */
31
+ 'cs',
32
+ /** Welsh */
33
+ 'cy',
34
+ /** Danish */
35
+ 'da',
36
+ /** German */
37
+ 'de',
38
+ /** Ewe */
39
+ 'ee',
40
+ /** Greek */
41
+ 'el',
42
+ /** English */
43
+ 'en',
44
+ /** Esperanto */
45
+ 'eo',
46
+ /** Spanish */
47
+ 'es',
48
+ /** European Spanish */
49
+ 'es_ES',
50
+ /** Mexican Spanish */
51
+ 'es_MX',
52
+ /** Estonian */
53
+ 'et',
54
+ /** Basque */
55
+ 'eu',
56
+ /** Persian */
57
+ 'fa',
58
+ /** Dari */
59
+ 'fa_AF',
60
+ /** Finnish */
61
+ 'fi',
62
+ /** Faroese */
63
+ 'fo',
64
+ /** French */
65
+ 'fr',
66
+ /** Canadian French */
67
+ 'fr_CA',
68
+ /** Swiss French */
69
+ 'fr_CH',
70
+ /** Western Frisian */
71
+ 'fy',
72
+ /** Irish */
73
+ 'ga',
74
+ /** Scottish Gaelic */
75
+ 'gd',
76
+ /** Galician */
77
+ 'gl',
78
+ /** Gujarati */
79
+ 'gu',
80
+ /** Hausa */
81
+ 'ha',
82
+ /** Hebrew */
83
+ 'he',
84
+ /** Hindi */
85
+ 'hi',
86
+ /** Croatian */
87
+ 'hr',
88
+ /** Haitian Creole */
89
+ 'ht',
90
+ /** Hungarian */
91
+ 'hu',
92
+ /** Armenian */
93
+ 'hy',
94
+ /** Interlingua */
95
+ 'ia',
96
+ /** Indonesian */
97
+ 'id',
98
+ /** Igbo */
99
+ 'ig',
100
+ /** Icelandic */
101
+ 'is',
102
+ /** Italian */
103
+ 'it',
104
+ /** Japanese */
105
+ 'ja',
106
+ /** Javanese */
107
+ 'jv',
108
+ /** Georgian */
109
+ 'ka',
110
+ /** Kazakh */
111
+ 'kk',
112
+ /** Khmer */
113
+ 'km',
114
+ /** Kannada */
115
+ 'kn',
116
+ /** Korean */
117
+ 'ko',
118
+ /** Kurdish */
119
+ 'ku',
120
+ /** Kyrgyz */
121
+ 'ky',
122
+ /** Latin */
123
+ 'la',
124
+ /** Luxembourgish */
125
+ 'lb',
126
+ /** Ganda */
127
+ 'lg',
128
+ /** Lingala */
129
+ 'ln',
130
+ /** Lao */
131
+ 'lo',
132
+ /** Lithuanian */
133
+ 'lt',
134
+ /** Latvian */
135
+ 'lv',
136
+ /** Malagasy */
137
+ 'mg',
138
+ /** Maori */
139
+ 'mi',
140
+ /** Macedonian */
141
+ 'mk',
142
+ /** Malayalam */
143
+ 'ml',
144
+ /** Mongolian */
145
+ 'mn',
146
+ /** Marathi */
147
+ 'mr',
148
+ /** Malay */
149
+ 'ms',
150
+ /** Maltese */
151
+ 'mt',
152
+ /** Burmese */
153
+ 'my',
154
+ /** Norwegian Bokmål */
155
+ 'nb',
156
+ /** Nepali */
157
+ 'ne',
158
+ /** Dutch */
159
+ 'nl',
160
+ /** Flemish */
161
+ 'nl_BE',
162
+ /** Norwegian Nynorsk */
163
+ 'nn',
164
+ /** Nyanja */
165
+ 'ny',
166
+ /** Oromo */
167
+ 'om',
168
+ /** Odia */
169
+ 'or',
170
+ /** Punjabi */
171
+ 'pa',
172
+ /** Polish */
173
+ 'pl',
174
+ /** Pashto */
175
+ 'ps',
176
+ /** Portuguese */
177
+ 'pt',
178
+ /** Brazilian Portuguese */
179
+ 'pt_BR',
180
+ /** European Portuguese */
181
+ 'pt_PT',
182
+ /** Quechua */
183
+ 'qu',
184
+ /** Romansh */
185
+ 'rm',
186
+ /** Romanian */
187
+ 'ro',
188
+ /** Moldavian */
189
+ 'ro_MD',
190
+ /** Russian */
191
+ 'ru',
192
+ /** Kinyarwanda */
193
+ 'rw',
194
+ /** Sanskrit */
195
+ 'sa',
196
+ /** Sindhi */
197
+ 'sd',
198
+ /** Sinhala */
199
+ 'si',
200
+ /** Slovak */
201
+ 'sk',
202
+ /** Slovenian */
203
+ 'sl',
204
+ /** Samoan */
205
+ 'sm',
206
+ /** Shona */
207
+ 'sn',
208
+ /** Somali */
209
+ 'so',
210
+ /** Albanian */
211
+ 'sq',
212
+ /** Serbian */
213
+ 'sr',
214
+ /** Southern Sotho */
215
+ 'st',
216
+ /** Sundanese */
217
+ 'su',
218
+ /** Swedish */
219
+ 'sv',
220
+ /** Swahili */
221
+ 'sw',
222
+ /** Congo Swahili */
223
+ 'sw_CD',
224
+ /** Tamil */
225
+ 'ta',
226
+ /** Telugu */
227
+ 'te',
228
+ /** Tajik */
229
+ 'tg',
230
+ /** Thai */
231
+ 'th',
232
+ /** Tigrinya */
233
+ 'ti',
234
+ /** Turkmen */
235
+ 'tk',
236
+ /** Tongan */
237
+ 'to',
238
+ /** Turkish */
239
+ 'tr',
240
+ /** Tatar */
241
+ 'tt',
242
+ /** Uyghur */
243
+ 'ug',
244
+ /** Ukrainian */
245
+ 'uk',
246
+ /** Urdu */
247
+ 'ur',
248
+ /** Uzbek */
249
+ 'uz',
250
+ /** Vietnamese */
251
+ 'vi',
252
+ /** Wolof */
253
+ 'wo',
254
+ /** Xhosa */
255
+ 'xh',
256
+ /** Yiddish */
257
+ 'yi',
258
+ /** Yoruba */
259
+ 'yo',
260
+ /** Chinese */
261
+ 'zh',
262
+ /** Simplified Chinese */
263
+ 'zh_Hans',
264
+ /** Traditional Chinese */
265
+ 'zh_Hant',
266
+ /** Zulu */
267
+ 'zu',
268
+ ];
@@ -1,15 +1,13 @@
1
- import { Separator } from '@/vdb/components/ui/separator.js';
1
+ import { Trans } from '@lingui/react/macro';
2
2
  import { ResultOf } from 'gql.tada';
3
3
  import { Globe, Phone } from 'lucide-react';
4
4
  import { orderAddressFragment } from '../orders.graphql.js';
5
- import { Trans } from '@lingui/react/macro';
6
5
 
7
6
  type OrderAddress = Omit<ResultOf<typeof orderAddressFragment>, 'country'> & {
8
7
  country: string | { code: string; name: string } | null;
9
8
  };
10
9
 
11
10
  export function OrderAddress({ address }: Readonly<{ address?: OrderAddress }>) {
12
-
13
11
  const {
14
12
  fullName,
15
13
  company,
@@ -27,7 +25,11 @@ export function OrderAddress({ address }: Readonly<{ address?: OrderAddress }>)
27
25
  const countryCodeString = country && typeof country !== 'string' ? country?.code : countryCode;
28
26
 
29
27
  if (!address || Object.values(address).every(value => !value)) {
30
- return <div className="text-sm text-muted-foreground"><Trans>No address</Trans></div>;
28
+ return (
29
+ <div className="text-sm text-muted-foreground">
30
+ <Trans>No address</Trans>
31
+ </div>
32
+ );
31
33
  }
32
34
 
33
35
  return (
@@ -41,7 +43,7 @@ export function OrderAddress({ address }: Readonly<{ address?: OrderAddress }>)
41
43
  <p>{[city, province].filter(Boolean).join(', ')}</p>
42
44
  {postalCode && <p>{postalCode}</p>}
43
45
  {country && (
44
- <div className="flex items-center gap-1.5 mt-1">
46
+ <div className="flex items-center gap-1.5">
45
47
  <Globe className="h-3 w-3 text-muted-foreground" />
46
48
  <span>{countryName}</span>
47
49
  {countryCodeString && (
@@ -49,17 +51,15 @@ export function OrderAddress({ address }: Readonly<{ address?: OrderAddress }>)
49
51
  )}
50
52
  </div>
51
53
  )}
54
+ {phoneNumber && (
55
+ <>
56
+ <div className="flex items-center gap-1.5">
57
+ <Phone className="h-3 w-3 text-muted-foreground" />
58
+ <span className="text-sm">{phoneNumber}</span>
59
+ </div>
60
+ </>
61
+ )}
52
62
  </div>
53
-
54
- {phoneNumber && (
55
- <>
56
- <Separator className="my-2" />
57
- <div className="flex items-center gap-1.5">
58
- <Phone className="h-3 w-3 text-muted-foreground" />
59
- <span className="text-sm">{phoneNumber}</span>
60
- </div>
61
- </>
62
- )}
63
63
  </div>
64
64
  );
65
65
  }
@@ -241,7 +241,7 @@ export function OrderDetailShared({
241
241
  </PageBlock>
242
242
  <PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
243
243
  {entity?.customer ? (
244
- <Button variant="ghost" asChild>
244
+ <Button variant="outline" asChild>
245
245
  <Link to={`/customers/${entity.customer.id}`}>
246
246
  <User className="w-4 h-4" />
247
247
  {entity.customer.firstName} {entity.customer.lastName}
@@ -255,7 +255,7 @@ export function OrderDetailShared({
255
255
  <div className="mt-4 divide-y">
256
256
  {entity?.shippingAddress && (
257
257
  <div className="pb-6">
258
- <div className="font-medium">
258
+ <div className="font-medium mb-6">
259
259
  <Trans>Shipping address</Trans>
260
260
  </div>
261
261
  <OrderAddress address={entity.shippingAddress} />
@@ -263,7 +263,7 @@ export function OrderDetailShared({
263
263
  )}
264
264
  {entity?.billingAddress && (
265
265
  <div className="pt-4">
266
- <div className="font-medium">
266
+ <div className="font-medium mb-6">
267
267
  <Trans>Billing address</Trans>
268
268
  </div>
269
269
  <OrderAddress address={entity.billingAddress} />
@@ -293,7 +293,7 @@ export function OrderDetailShared({
293
293
  ))}
294
294
  </div>
295
295
  ) : (
296
- <div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
296
+ <div className="text-muted-foreground text-sm">
297
297
  <Trans>No fulfillments</Trans>
298
298
  </div>
299
299
  )}
@@ -0,0 +1,49 @@
1
+ import { useLingui } from '@lingui/react/macro';
2
+ import { PlusIcon } from 'lucide-react';
3
+
4
+ import { Button } from '@/vdb/components/ui/button';
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ } from '@/vdb/components/ui/dropdown-menu';
11
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format';
12
+
13
+ interface AddCurrencyDropdownProps {
14
+ unusedCurrencies: string[];
15
+ onCurrencySelect: (currencyCode: string) => void;
16
+ placeholder?: string;
17
+ }
18
+
19
+ export function AddCurrencyDropdown({
20
+ unusedCurrencies,
21
+ onCurrencySelect,
22
+ placeholder,
23
+ }: AddCurrencyDropdownProps) {
24
+ const { formatCurrencyName } = useLocalFormat();
25
+ const { t } = useLingui();
26
+
27
+ if (unusedCurrencies.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <DropdownMenu>
33
+ <DropdownMenuTrigger asChild>
34
+ <Button variant="outline" className="gap-2">
35
+ <PlusIcon className="size-4" />
36
+ {placeholder || t`Add a price in another currency`}
37
+ </Button>
38
+ </DropdownMenuTrigger>
39
+ <DropdownMenuContent>
40
+ {unusedCurrencies.map(currencyCode => (
41
+ <DropdownMenuItem key={currencyCode} onSelect={() => onCurrencySelect(currencyCode)}>
42
+ <span className="uppercase text-muted-foreground">{currencyCode}</span>
43
+ {formatCurrencyName(currencyCode)}
44
+ </DropdownMenuItem>
45
+ ))}
46
+ </DropdownMenuContent>
47
+ </DropdownMenu>
48
+ );
49
+ }
@@ -0,0 +1,56 @@
1
+ import { useLingui } from '@lingui/react/macro';
2
+ import { PlusIcon } from 'lucide-react';
3
+
4
+ import { Button } from '@/vdb/components/ui/button';
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ } from '@/vdb/components/ui/dropdown-menu';
11
+ import { ResultOf } from 'gql.tada';
12
+
13
+ import { stockLocationsQueryDocument } from '../product-variants.graphql.js';
14
+
15
+ interface AddStockLocationDropdownProps {
16
+ availableStockLocations: ResultOf<typeof stockLocationsQueryDocument>['stockLocations']['items'];
17
+ usedStockLocationIds: string[];
18
+ onStockLocationSelect: (stockLocationId: string, stockLocationName: string) => void;
19
+ placeholder?: string;
20
+ }
21
+
22
+ export function AddStockLocationDropdown({
23
+ availableStockLocations,
24
+ usedStockLocationIds,
25
+ onStockLocationSelect,
26
+ placeholder,
27
+ }: AddStockLocationDropdownProps) {
28
+ const { t } = useLingui();
29
+
30
+ const unusedStockLocations = availableStockLocations.filter(sl => !usedStockLocationIds.includes(sl.id));
31
+
32
+ if (unusedStockLocations.length === 0) {
33
+ return null;
34
+ }
35
+
36
+ return (
37
+ <DropdownMenu>
38
+ <DropdownMenuTrigger asChild>
39
+ <Button variant="outline" className="gap-2">
40
+ <PlusIcon className="size-4" />
41
+ {placeholder || t`Add stock level for another location`}
42
+ </Button>
43
+ </DropdownMenuTrigger>
44
+ <DropdownMenuContent>
45
+ {unusedStockLocations.map(location => (
46
+ <DropdownMenuItem
47
+ key={location.id}
48
+ onSelect={() => onStockLocationSelect(location.id, location.name)}
49
+ >
50
+ {location.name}
51
+ </DropdownMenuItem>
52
+ ))}
53
+ </DropdownMenuContent>
54
+ </DropdownMenu>
55
+ );
56
+ }
@@ -193,3 +193,15 @@ export const updateProductVariantsDocument = graphql(`
193
193
  }
194
194
  }
195
195
  `);
196
+
197
+ export const stockLocationsQueryDocument = graphql(`
198
+ query StockLocations {
199
+ stockLocations(options: { take: 100 }) {
200
+ items {
201
+ id
202
+ name
203
+ description
204
+ }
205
+ }
206
+ }
207
+ `);