@vendure/dashboard 3.5.0-minor-202510012036 → 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.
- package/dist/plugin/dashboard.plugin.d.ts +25 -6
- package/dist/plugin/dashboard.plugin.js +184 -27
- package/dist/plugin/default-page.html +188 -0
- package/dist/vite/utils/tsconfig-utils.js +2 -1
- package/package.json +10 -9
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +4 -8
- package/src/app/routes/_authenticated/_global-settings/utils/global-languages.ts +268 -0
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +15 -15
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +4 -4
- package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +49 -0
- package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +56 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +12 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +178 -50
- package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +0 -11
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +3 -14
- package/src/app/routes/_authenticated/_shipping-methods/components/test-address-form.tsx +13 -12
- package/src/app/routes/_authenticated/_shipping-methods/components/test-order-builder.tsx +3 -2
- package/src/lib/components/data-input/customer-group-input.tsx +0 -1
- package/src/lib/components/data-input/money-input.tsx +7 -11
- package/src/lib/components/data-input/number-input.tsx +6 -1
- package/src/lib/components/data-table/data-table-filter-badge.tsx +15 -8
- package/src/lib/components/data-table/data-table.tsx +2 -2
- package/src/lib/components/data-table/my-views-button.tsx +12 -12
- package/src/lib/components/data-table/save-view-button.tsx +5 -1
- package/src/lib/components/layout/generated-breadcrumbs.tsx +4 -12
- package/src/lib/components/shared/configurable-operation-input.tsx +1 -1
- package/src/lib/constants.ts +10 -0
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +0 -2
- package/src/lib/framework/extension-api/types/layout.ts +41 -1
- package/src/lib/framework/form-engine/value-transformers.ts +8 -1
- package/src/lib/framework/layout-engine/page-layout.tsx +58 -48
- package/src/lib/framework/page/detail-page.tsx +12 -15
- package/src/lib/graphql/api.ts +17 -4
- package/src/lib/graphql/graphql-env.d.ts +29 -50
- package/src/lib/hooks/use-saved-views.ts +7 -0
- package/src/lib/providers/auth.tsx +2 -2
- package/src/lib/providers/channel-provider.tsx +4 -2
- package/src/lib/providers/user-settings.tsx +46 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
|
|
2
|
+
import { NumberInput } from '@/vdb/components/data-input/number-input.js';
|
|
2
3
|
import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
|
|
3
4
|
import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
|
|
4
5
|
import { ErrorPage } from '@/vdb/components/shared/error-page.js';
|
|
@@ -24,15 +25,21 @@ import {
|
|
|
24
25
|
} from '@/vdb/framework/layout-engine/page-layout.js';
|
|
25
26
|
import { detailPageRouteLoader } from '@/vdb/framework/page/detail-page-route-loader.js';
|
|
26
27
|
import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
28
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
27
29
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
28
30
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
31
|
+
import { useQuery } from '@tanstack/react-query';
|
|
29
32
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
30
|
-
import {
|
|
33
|
+
import { VariablesOf } from 'gql.tada';
|
|
34
|
+
import { Trash } from 'lucide-react';
|
|
31
35
|
import { toast } from 'sonner';
|
|
36
|
+
import { AddCurrencyDropdown } from './components/add-currency-dropdown.js';
|
|
37
|
+
import { AddStockLocationDropdown } from './components/add-stock-location-dropdown.js';
|
|
32
38
|
import { VariantPriceDetail } from './components/variant-price-detail.js';
|
|
33
39
|
import {
|
|
34
40
|
createProductVariantDocument,
|
|
35
41
|
productVariantDetailDocument,
|
|
42
|
+
stockLocationsQueryDocument,
|
|
36
43
|
updateProductVariantDocument,
|
|
37
44
|
} from './product-variants.graphql.js';
|
|
38
45
|
|
|
@@ -57,6 +64,8 @@ export const Route = createFileRoute('/_authenticated/_product-variants/product-
|
|
|
57
64
|
errorComponent: ({ error }) => <ErrorPage message={error.message} />,
|
|
58
65
|
});
|
|
59
66
|
|
|
67
|
+
type PriceInput = NonNullable<VariablesOf<typeof updateProductVariantDocument>['input']['prices']>[number];
|
|
68
|
+
|
|
60
69
|
function ProductVariantDetailPage() {
|
|
61
70
|
const params = Route.useParams();
|
|
62
71
|
const navigate = useNavigate();
|
|
@@ -64,6 +73,11 @@ function ProductVariantDetailPage() {
|
|
|
64
73
|
const { t } = useLingui();
|
|
65
74
|
const { activeChannel } = useChannel();
|
|
66
75
|
|
|
76
|
+
const { data: stockLocationsData } = useQuery({
|
|
77
|
+
queryKey: ['stockLocations'],
|
|
78
|
+
queryFn: () => api.query(stockLocationsQueryDocument, {}),
|
|
79
|
+
});
|
|
80
|
+
|
|
67
81
|
const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
|
|
68
82
|
pageId,
|
|
69
83
|
queryDocument: productVariantDetailDocument,
|
|
@@ -79,7 +93,7 @@ function ProductVariantDetailPage() {
|
|
|
79
93
|
facetValueIds: entity.facetValues.map(facetValue => facetValue.id),
|
|
80
94
|
taxCategoryId: entity.taxCategory.id,
|
|
81
95
|
price: entity.price,
|
|
82
|
-
prices:
|
|
96
|
+
prices: entity.prices,
|
|
83
97
|
trackInventory: entity.trackInventory,
|
|
84
98
|
outOfStockThreshold: entity.outOfStockThreshold,
|
|
85
99
|
stockLevels: entity.stockLevels.map(stockLevel => ({
|
|
@@ -117,7 +131,74 @@ function ProductVariantDetailPage() {
|
|
|
117
131
|
},
|
|
118
132
|
});
|
|
119
133
|
|
|
120
|
-
const
|
|
134
|
+
const availableCurrencies = activeChannel?.availableCurrencyCodes ?? [];
|
|
135
|
+
const [prices, taxCategoryId, stockLevels] = form.watch(['prices', 'taxCategoryId', 'stockLevels']);
|
|
136
|
+
|
|
137
|
+
// Filter out deleted prices for display
|
|
138
|
+
const activePrices = prices?.filter(p => !p.delete) ?? [];
|
|
139
|
+
|
|
140
|
+
// Get currencies that are currently active (not deleted)
|
|
141
|
+
const usedCurrencies = activePrices.map(p => p.currencyCode);
|
|
142
|
+
const unusedCurrencies = availableCurrencies.filter(c => !usedCurrencies.includes(c));
|
|
143
|
+
|
|
144
|
+
// Get used stock location IDs
|
|
145
|
+
const usedStockLocationIds = stockLevels?.map(sl => sl.stockLocationId) ?? [];
|
|
146
|
+
|
|
147
|
+
const handleAddCurrency = (currencyCode: string) => {
|
|
148
|
+
const currentPrices = form.getValues('prices') || [];
|
|
149
|
+
|
|
150
|
+
// Check if this currency already exists (including deleted ones)
|
|
151
|
+
const existingPriceIndex = currentPrices.findIndex(p => p.currencyCode === currencyCode);
|
|
152
|
+
|
|
153
|
+
if (existingPriceIndex !== -1) {
|
|
154
|
+
// Currency exists, mark it as not deleted
|
|
155
|
+
const updatedPrices = [...currentPrices];
|
|
156
|
+
updatedPrices[existingPriceIndex] = {
|
|
157
|
+
...updatedPrices[existingPriceIndex],
|
|
158
|
+
delete: false,
|
|
159
|
+
};
|
|
160
|
+
form.setValue('prices', updatedPrices, {
|
|
161
|
+
shouldDirty: true,
|
|
162
|
+
shouldValidate: true,
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
// Add new currency
|
|
166
|
+
const newPrice = {
|
|
167
|
+
currencyCode,
|
|
168
|
+
price: 0,
|
|
169
|
+
delete: false,
|
|
170
|
+
} as PriceInput;
|
|
171
|
+
form.setValue('prices', [...currentPrices, newPrice], {
|
|
172
|
+
shouldDirty: true,
|
|
173
|
+
shouldValidate: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleRemoveCurrency = (indexToRemove: number) => {
|
|
179
|
+
const currentPrices = form.getValues('prices') || [];
|
|
180
|
+
const updatedPrices = [...currentPrices];
|
|
181
|
+
updatedPrices[indexToRemove] = {
|
|
182
|
+
...updatedPrices[indexToRemove],
|
|
183
|
+
delete: true,
|
|
184
|
+
};
|
|
185
|
+
form.setValue('prices', updatedPrices, {
|
|
186
|
+
shouldDirty: true,
|
|
187
|
+
shouldValidate: true,
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleAddStockLocation = (stockLocationId: string, stockLocationName: string) => {
|
|
192
|
+
const currentStockLevels = form.getValues('stockLevels') || [];
|
|
193
|
+
const newStockLevel = {
|
|
194
|
+
stockLocationId,
|
|
195
|
+
stockOnHand: 0,
|
|
196
|
+
};
|
|
197
|
+
form.setValue('stockLevels', [...currentStockLevels, newStockLevel], {
|
|
198
|
+
shouldDirty: true,
|
|
199
|
+
shouldValidate: true,
|
|
200
|
+
});
|
|
201
|
+
};
|
|
121
202
|
|
|
122
203
|
return (
|
|
123
204
|
<Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
|
|
@@ -168,7 +249,7 @@ function ProductVariantDetailPage() {
|
|
|
168
249
|
<CustomFieldsPageBlock column="main" entityType="ProductVariant" control={form.control} />
|
|
169
250
|
|
|
170
251
|
<PageBlock column="main" blockId="price-and-tax" title={<Trans>Price and tax</Trans>}>
|
|
171
|
-
<
|
|
252
|
+
<DetailFormGrid>
|
|
172
253
|
<FormFieldWrapper
|
|
173
254
|
control={form.control}
|
|
174
255
|
name="taxCategoryId"
|
|
@@ -177,56 +258,62 @@ function ProductVariantDetailPage() {
|
|
|
177
258
|
<TaxCategorySelector value={field.value} onChange={field.onChange} />
|
|
178
259
|
)}
|
|
179
260
|
/>
|
|
261
|
+
</DetailFormGrid>
|
|
262
|
+
{activePrices.map((price, displayIndex) => {
|
|
263
|
+
// Find the actual index in the full prices array
|
|
264
|
+
const actualIndex = prices?.findIndex(p => p === price) ?? displayIndex;
|
|
180
265
|
|
|
181
|
-
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
266
|
+
const currencyCodeLabel = (
|
|
267
|
+
<div className="uppercase text-muted-foreground">{price.currencyCode}</div>
|
|
268
|
+
);
|
|
269
|
+
const priceLabel = (
|
|
270
|
+
<div className="flex gap-1 items-center justify-between">
|
|
271
|
+
<Trans>Price</Trans> {activePrices.length > 1 ? currencyCodeLabel : null}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
return (
|
|
275
|
+
<DetailFormGrid key={price.currencyCode}>
|
|
276
|
+
<div className="flex gap-1 items-end">
|
|
277
|
+
<FormFieldWrapper
|
|
278
|
+
control={form.control}
|
|
279
|
+
name={`prices.${actualIndex}.price`}
|
|
280
|
+
label={priceLabel}
|
|
281
|
+
render={({ field }) => (
|
|
282
|
+
<MoneyInput {...field} currency={price.currencyCode} />
|
|
283
|
+
)}
|
|
284
|
+
/>
|
|
285
|
+
{activePrices.length > 1 && (
|
|
286
|
+
<Button
|
|
287
|
+
type="button"
|
|
288
|
+
variant="ghost"
|
|
289
|
+
size="sm"
|
|
290
|
+
onClick={() => handleRemoveCurrency(actualIndex)}
|
|
291
|
+
className="h-6 w-6 p-0 mb-2 hover:text-destructive hover:bg-destructive-100"
|
|
292
|
+
>
|
|
293
|
+
<Trash className="size-4" />
|
|
294
|
+
</Button>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
<VariantPriceDetail
|
|
298
|
+
priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
|
|
299
|
+
price={price.price}
|
|
300
|
+
currencyCode={
|
|
301
|
+
price.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
|
|
302
|
+
}
|
|
303
|
+
taxCategoryId={taxCategoryId}
|
|
304
|
+
/>
|
|
305
|
+
</DetailFormGrid>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
308
|
+
{unusedCurrencies.length ? (
|
|
309
|
+
<AddCurrencyDropdown
|
|
310
|
+
onCurrencySelect={handleAddCurrency}
|
|
311
|
+
unusedCurrencies={unusedCurrencies}
|
|
312
|
+
/>
|
|
313
|
+
) : null}
|
|
200
314
|
</PageBlock>
|
|
201
315
|
<PageBlock column="main" blockId="stock" title={<Trans>Stock</Trans>}>
|
|
202
316
|
<DetailFormGrid>
|
|
203
|
-
{entity?.stockLevels.map((stockLevel, index) => (
|
|
204
|
-
<Fragment key={stockLevel.id}>
|
|
205
|
-
<FormFieldWrapper
|
|
206
|
-
control={form.control}
|
|
207
|
-
name={`stockLevels.${index}.stockOnHand`}
|
|
208
|
-
label={<Trans>Stock level</Trans>}
|
|
209
|
-
render={({ field }) => (
|
|
210
|
-
<Input
|
|
211
|
-
type="number"
|
|
212
|
-
value={field.value}
|
|
213
|
-
onChange={e => {
|
|
214
|
-
field.onChange(e.target.valueAsNumber);
|
|
215
|
-
}}
|
|
216
|
-
/>
|
|
217
|
-
)}
|
|
218
|
-
/>
|
|
219
|
-
<div>
|
|
220
|
-
<FormItem>
|
|
221
|
-
<FormLabel>
|
|
222
|
-
<Trans>Allocated</Trans>
|
|
223
|
-
</FormLabel>
|
|
224
|
-
<div className="text-sm pt-1.5">{stockLevel.stockAllocated}</div>
|
|
225
|
-
</FormItem>
|
|
226
|
-
</div>
|
|
227
|
-
</Fragment>
|
|
228
|
-
))}
|
|
229
|
-
|
|
230
317
|
<FormFieldWrapper
|
|
231
318
|
control={form.control}
|
|
232
319
|
name="trackInventory"
|
|
@@ -292,6 +379,47 @@ function ProductVariantDetailPage() {
|
|
|
292
379
|
)}
|
|
293
380
|
/>
|
|
294
381
|
</DetailFormGrid>
|
|
382
|
+
{stockLevels?.map((stockLevel, index) => {
|
|
383
|
+
const stockAllocated =
|
|
384
|
+
entity?.stockLevels.find(sl => sl.stockLocation.id === stockLevel.stockLocationId)
|
|
385
|
+
?.stockAllocated ?? 0;
|
|
386
|
+
const stockLocationName = stockLocationsData?.stockLocations.items?.find(
|
|
387
|
+
sl => sl.id === stockLevel.stockLocationId,
|
|
388
|
+
)?.name;
|
|
389
|
+
const stockLocationNameLabel =
|
|
390
|
+
stockLevels.length > 1 ? (
|
|
391
|
+
<div className="text-muted-foreground">{stockLocationName}</div>
|
|
392
|
+
) : null;
|
|
393
|
+
const stockLabel = (
|
|
394
|
+
<>
|
|
395
|
+
<Trans>Stock level</Trans>
|
|
396
|
+
{stockLocationNameLabel}
|
|
397
|
+
</>
|
|
398
|
+
);
|
|
399
|
+
return (
|
|
400
|
+
<DetailFormGrid key={stockLevel.stockLocationId}>
|
|
401
|
+
<FormFieldWrapper
|
|
402
|
+
control={form.control}
|
|
403
|
+
name={`stockLevels.${index}.stockOnHand`}
|
|
404
|
+
label={stockLabel}
|
|
405
|
+
render={({ field }) => <NumberInput {...field} value={field.value} />}
|
|
406
|
+
/>
|
|
407
|
+
<div>
|
|
408
|
+
<FormItem>
|
|
409
|
+
<FormLabel>
|
|
410
|
+
<Trans>Allocated</Trans>
|
|
411
|
+
</FormLabel>
|
|
412
|
+
<div className="text-sm pt-1.5">{stockAllocated}</div>
|
|
413
|
+
</FormItem>
|
|
414
|
+
</div>
|
|
415
|
+
</DetailFormGrid>
|
|
416
|
+
);
|
|
417
|
+
})}
|
|
418
|
+
<AddStockLocationDropdown
|
|
419
|
+
availableStockLocations={stockLocationsData?.stockLocations.items ?? []}
|
|
420
|
+
usedStockLocationIds={usedStockLocationIds}
|
|
421
|
+
onStockLocationSelect={handleAddStockLocation}
|
|
422
|
+
/>
|
|
295
423
|
</PageBlock>
|
|
296
424
|
|
|
297
425
|
<PageBlock column="side" blockId="facet-values">
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
PaginatedListRefresherRegisterFn,
|
|
6
6
|
} from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
7
7
|
import { StockLevelLabel } from '@/vdb/components/shared/stock-level-label.js';
|
|
8
|
-
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
9
8
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
10
9
|
import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
|
|
11
10
|
import { useState } from 'react';
|
|
@@ -17,15 +16,6 @@ import {
|
|
|
17
16
|
} from '../../_product-variants/components/product-variant-bulk-actions.js';
|
|
18
17
|
import { productVariantListDocument } from '../products.graphql.js';
|
|
19
18
|
|
|
20
|
-
export const deleteProductVariantDocument = graphql(`
|
|
21
|
-
mutation DeleteProductVariant($id: ID!) {
|
|
22
|
-
deleteProductVariant(id: $id) {
|
|
23
|
-
result
|
|
24
|
-
message
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
`);
|
|
28
|
-
|
|
29
19
|
interface ProductVariantsTableProps {
|
|
30
20
|
productId: string;
|
|
31
21
|
registerRefresher?: PaginatedListRefresherRegisterFn;
|
|
@@ -47,7 +37,6 @@ export function ProductVariantsTable({
|
|
|
47
37
|
<PaginatedListDataTable
|
|
48
38
|
registerRefresher={registerRefresher}
|
|
49
39
|
listQuery={productVariantListDocument}
|
|
50
|
-
deleteMutation={deleteProductVariantDocument}
|
|
51
40
|
transformVariables={variables => ({
|
|
52
41
|
...variables,
|
|
53
42
|
productId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
|
|
2
|
+
import { NumberInput } from '@/vdb/components/data-input/number-input.js';
|
|
2
3
|
import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
|
|
3
4
|
import { ErrorPage } from '@/vdb/components/shared/error-page.js';
|
|
4
5
|
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
@@ -202,25 +203,13 @@ function PromotionDetailPage() {
|
|
|
202
203
|
control={form.control}
|
|
203
204
|
name="perCustomerUsageLimit"
|
|
204
205
|
label={<Trans>Per customer usage limit</Trans>}
|
|
205
|
-
render={({ field }) =>
|
|
206
|
-
<Input
|
|
207
|
-
type="number"
|
|
208
|
-
value={field.value ?? ''}
|
|
209
|
-
onChange={e => field.onChange(e.target.valueAsNumber)}
|
|
210
|
-
/>
|
|
211
|
-
)}
|
|
206
|
+
render={({ field }) => <NumberInput {...field} value={field.value ?? ''} />}
|
|
212
207
|
/>
|
|
213
208
|
<FormFieldWrapper
|
|
214
209
|
control={form.control}
|
|
215
210
|
name="usageLimit"
|
|
216
211
|
label={<Trans>Usage limit</Trans>}
|
|
217
|
-
render={({ field }) =>
|
|
218
|
-
<Input
|
|
219
|
-
type="number"
|
|
220
|
-
value={field.value ?? ''}
|
|
221
|
-
onChange={e => field.onChange(e.target.valueAsNumber)}
|
|
222
|
-
/>
|
|
223
|
-
)}
|
|
212
|
+
render={({ field }) => <NumberInput {...field} value={field.value ?? ''} />}
|
|
224
213
|
/>
|
|
225
214
|
</DetailFormGrid>
|
|
226
215
|
</PageBlock>
|
|
@@ -3,6 +3,7 @@ import { AccordionContent, AccordionItem, AccordionTrigger } from '@/vdb/compone
|
|
|
3
3
|
import { Form } from '@/vdb/components/ui/form.js';
|
|
4
4
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
5
5
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
|
|
6
|
+
import { LS_KEY_SHIPPING_TEST_ADDRESS } from '@/vdb/constants.js';
|
|
6
7
|
import { api } from '@/vdb/graphql/api.js';
|
|
7
8
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
8
9
|
import { Trans } from '@lingui/react/macro';
|
|
@@ -43,20 +44,20 @@ export function TestAddressForm({ onAddressChange }: Readonly<TestAddressFormPro
|
|
|
43
44
|
const form = useForm<TestAddress>({
|
|
44
45
|
defaultValues: (() => {
|
|
45
46
|
try {
|
|
46
|
-
const stored = localStorage.getItem(
|
|
47
|
+
const stored = localStorage.getItem(LS_KEY_SHIPPING_TEST_ADDRESS);
|
|
47
48
|
return stored
|
|
48
49
|
? JSON.parse(stored)
|
|
49
50
|
: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
fullName: '',
|
|
52
|
+
company: '',
|
|
53
|
+
streetLine1: '',
|
|
54
|
+
streetLine2: '',
|
|
55
|
+
city: '',
|
|
56
|
+
province: '',
|
|
57
|
+
postalCode: '',
|
|
58
|
+
countryCode: '',
|
|
59
|
+
phoneNumber: '',
|
|
60
|
+
};
|
|
60
61
|
} catch {
|
|
61
62
|
return {
|
|
62
63
|
fullName: '',
|
|
@@ -92,7 +93,7 @@ export function TestAddressForm({ onAddressChange }: Readonly<TestAddressFormPro
|
|
|
92
93
|
previousValuesRef.current = currentValueString;
|
|
93
94
|
|
|
94
95
|
try {
|
|
95
|
-
localStorage.setItem(
|
|
96
|
+
localStorage.setItem(LS_KEY_SHIPPING_TEST_ADDRESS, currentValueString);
|
|
96
97
|
} catch {
|
|
97
98
|
// Ignore localStorage errors
|
|
98
99
|
}
|
|
@@ -7,6 +7,7 @@ import { AccordionContent, AccordionItem, AccordionTrigger } from '@/vdb/compone
|
|
|
7
7
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
8
8
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
9
9
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
|
|
10
|
+
import { LS_KEY_SHIPPING_TEST_ORDER } from '@/vdb/constants.js';
|
|
10
11
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
11
12
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
12
13
|
import { Trans } from '@lingui/react/macro';
|
|
@@ -38,7 +39,7 @@ export function TestOrderBuilder({ onOrderLinesChange }: Readonly<TestOrderBuild
|
|
|
38
39
|
const { activeChannel } = useChannel();
|
|
39
40
|
const [lines, setLines] = useState<TestOrderLine[]>(() => {
|
|
40
41
|
try {
|
|
41
|
-
const stored = localStorage.getItem(
|
|
42
|
+
const stored = localStorage.getItem(LS_KEY_SHIPPING_TEST_ORDER);
|
|
42
43
|
return stored ? JSON.parse(stored) : [];
|
|
43
44
|
} catch {
|
|
44
45
|
return [];
|
|
@@ -51,7 +52,7 @@ export function TestOrderBuilder({ onOrderLinesChange }: Readonly<TestOrderBuild
|
|
|
51
52
|
|
|
52
53
|
useEffect(() => {
|
|
53
54
|
try {
|
|
54
|
-
localStorage.setItem(
|
|
55
|
+
localStorage.setItem(LS_KEY_SHIPPING_TEST_ORDER, JSON.stringify(lines));
|
|
55
56
|
} catch {
|
|
56
57
|
// Ignore localStorage errors
|
|
57
58
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
2
|
-
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
3
2
|
import { useEffect, useMemo, useState } from 'react';
|
|
4
3
|
import { AffixedInput } from './affixed-input.js';
|
|
5
4
|
|
|
6
5
|
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
7
6
|
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
8
7
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
8
|
+
import { useDisplayLocale } from '@/vdb/hooks/use-display-locale.js';
|
|
9
9
|
|
|
10
10
|
export interface MoneyInputProps extends DashboardFormComponentProps {
|
|
11
11
|
currency?: string;
|
|
@@ -24,9 +24,7 @@ export function MoneyInput(props: Readonly<MoneyInputProps>) {
|
|
|
24
24
|
const { activeChannel } = useChannel();
|
|
25
25
|
const activeCurrency = currency ?? activeChannel?.defaultCurrencyCode;
|
|
26
26
|
const readOnly = isReadonlyField(props.fieldDef);
|
|
27
|
-
const {
|
|
28
|
-
settings: { displayLanguage, displayLocale },
|
|
29
|
-
} = useUserSettings();
|
|
27
|
+
const { bcp47Tag } = useDisplayLocale();
|
|
30
28
|
const { toMajorUnits, toMinorUnits } = useLocalFormat();
|
|
31
29
|
const [displayValue, setDisplayValue] = useState(toMajorUnits(value).toFixed(2));
|
|
32
30
|
|
|
@@ -40,32 +38,30 @@ export function MoneyInput(props: Readonly<MoneyInputProps>) {
|
|
|
40
38
|
if (!activeCurrency) {
|
|
41
39
|
return false;
|
|
42
40
|
}
|
|
43
|
-
const
|
|
44
|
-
const parts = new Intl.NumberFormat(locale, {
|
|
41
|
+
const parts = new Intl.NumberFormat(bcp47Tag, {
|
|
45
42
|
style: 'currency',
|
|
46
43
|
currency: activeCurrency,
|
|
47
44
|
currencyDisplay: 'symbol',
|
|
48
45
|
}).formatToParts();
|
|
49
46
|
const NaNString = parts.find(p => p.type === 'nan')?.value ?? 'NaN';
|
|
50
|
-
const localised = new Intl.NumberFormat(
|
|
47
|
+
const localised = new Intl.NumberFormat(bcp47Tag, {
|
|
51
48
|
style: 'currency',
|
|
52
49
|
currency: activeCurrency,
|
|
53
50
|
currencyDisplay: 'symbol',
|
|
54
51
|
}).format(undefined as any);
|
|
55
52
|
return localised.indexOf(NaNString) > 0;
|
|
56
|
-
}, [activeCurrency,
|
|
53
|
+
}, [activeCurrency, bcp47Tag]);
|
|
57
54
|
|
|
58
55
|
// Get the currency symbol
|
|
59
56
|
const currencySymbol = useMemo(() => {
|
|
60
57
|
if (!activeCurrency) return '';
|
|
61
|
-
const
|
|
62
|
-
const parts = new Intl.NumberFormat(locale, {
|
|
58
|
+
const parts = new Intl.NumberFormat(bcp47Tag, {
|
|
63
59
|
style: 'currency',
|
|
64
60
|
currency: activeCurrency,
|
|
65
61
|
currencyDisplay: 'symbol',
|
|
66
62
|
}).formatToParts();
|
|
67
63
|
return parts.find(p => p.type === 'currency')?.value ?? activeCurrency;
|
|
68
|
-
}, [activeCurrency,
|
|
64
|
+
}, [activeCurrency, bcp47Tag]);
|
|
69
65
|
|
|
70
66
|
return (
|
|
71
67
|
<AffixedInput
|
|
@@ -22,7 +22,12 @@ export function NumberInput({ fieldDef, onChange, ...fieldProps }: Readonly<Dash
|
|
|
22
22
|
const shouldUseAffixedInput = prefix || suffix;
|
|
23
23
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
24
24
|
if (readOnly) return;
|
|
25
|
-
|
|
25
|
+
const numValue = e.target.valueAsNumber;
|
|
26
|
+
if (Number.isNaN(numValue)) {
|
|
27
|
+
onChange(null);
|
|
28
|
+
} else {
|
|
29
|
+
onChange(e.target.valueAsNumber);
|
|
30
|
+
}
|
|
26
31
|
};
|
|
27
32
|
if (shouldUseAffixedInput) {
|
|
28
33
|
return (
|
|
@@ -19,18 +19,21 @@ export function DataTableFilterBadge({
|
|
|
19
19
|
return (
|
|
20
20
|
<Badge
|
|
21
21
|
key={filter.id}
|
|
22
|
-
className="flex gap-1 items-center font-mono cursor-pointer
|
|
22
|
+
className="flex gap-1 flex-wrap items-center font-mono cursor-pointer"
|
|
23
23
|
variant="outline"
|
|
24
24
|
onClick={() => onRemove(filter)}
|
|
25
25
|
>
|
|
26
|
-
<Filter size="12" className="opacity-50" />
|
|
27
|
-
<div
|
|
28
|
-
|
|
26
|
+
<Filter size="12" className="opacity-50 flex-shrink-0" />
|
|
27
|
+
<div className="@xs:overflow-hidden @xs:text-ellipsis @xs:whitespace-nowrap" title={filter.id}>
|
|
28
|
+
{filter.id}
|
|
29
|
+
</div>
|
|
30
|
+
<div className="text-muted-foreground flex-shrink-0">
|
|
29
31
|
<HumanReadableOperator operator={operator as Operator} mode="short" />
|
|
30
32
|
</div>
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
<div className="@xs:overflow-hidden @xs:text-ellipsis @xs:whitespace-nowrap flex flex-col @xl:flex-row @2xl:gap-1">
|
|
34
|
+
<FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
|
|
35
|
+
</div>
|
|
36
|
+
<XIcon className="h-4 flex-shrink-0" />
|
|
34
37
|
</Badge>
|
|
35
38
|
);
|
|
36
39
|
}
|
|
@@ -65,7 +68,11 @@ function FilterValue({
|
|
|
65
68
|
);
|
|
66
69
|
}
|
|
67
70
|
if (typeof value === 'string' && isDateIsoString(value)) {
|
|
68
|
-
return
|
|
71
|
+
return (
|
|
72
|
+
<div title={formatDate(value, { dateStyle: 'short', timeStyle: 'long' })}>
|
|
73
|
+
{formatDate(value, { dateStyle: 'short' })}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
69
76
|
}
|
|
70
77
|
if (typeof value === 'boolean') {
|
|
71
78
|
return <div>{value ? 'true' : 'false'}</div>;
|
|
@@ -243,11 +243,11 @@ export function DataTable<TData>({
|
|
|
243
243
|
|
|
244
244
|
{(pageId && onFilterChange && globalViews.length > 0) ||
|
|
245
245
|
columnFilters.filter(f => !facetedFilters?.[f.id]).length > 0 ? (
|
|
246
|
-
<div className="flex items-center justify-between bg-muted/40 rounded border border-border p-2">
|
|
246
|
+
<div className="flex items-center justify-between bg-muted/40 rounded border border-border p-2 @container">
|
|
247
247
|
<div className="flex items-center">
|
|
248
248
|
{pageId && onFilterChange && <GlobalViewsBar />}
|
|
249
249
|
</div>
|
|
250
|
-
<div className="flex gap-1 items-center">
|
|
250
|
+
<div className="flex gap-1 flex-wrap items-center">
|
|
251
251
|
{columnFilters
|
|
252
252
|
.filter(f => !facetedFilters?.[f.id])
|
|
253
253
|
.map(f => {
|
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import { Bookmark } from 'lucide-react';
|
|
2
|
-
import React, { useState, useMemo } from 'react';
|
|
3
|
-
import { Button } from '../ui/button.js';
|
|
4
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
|
|
5
1
|
import { Trans } from '@lingui/react/macro';
|
|
6
|
-
import {
|
|
2
|
+
import { Bookmark } from 'lucide-react';
|
|
3
|
+
import React, { useMemo, useState } from 'react';
|
|
7
4
|
import { useSavedViews } from '../../hooks/use-saved-views.js';
|
|
8
5
|
import { findMatchingSavedView } from '../../utils/saved-views-utils.js';
|
|
6
|
+
import { Button } from '../ui/button.js';
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
|
|
9
8
|
import { useDataTableContext } from './data-table-context.js';
|
|
9
|
+
import { UserViewsSheet } from './user-views-sheet.js';
|
|
10
10
|
|
|
11
11
|
export const MyViewsButton: React.FC = () => {
|
|
12
12
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
13
|
-
const { userViews } = useSavedViews();
|
|
14
|
-
const { columnFilters, searchTerm
|
|
13
|
+
const { userViews, savedViewsAreAvailable } = useSavedViews();
|
|
14
|
+
const { columnFilters, searchTerm } = useDataTableContext();
|
|
15
15
|
|
|
16
16
|
// Find the active view using centralized utility
|
|
17
17
|
const activeView = useMemo(() => {
|
|
18
18
|
return findMatchingSavedView(columnFilters, searchTerm, userViews);
|
|
19
19
|
}, [userViews, columnFilters, searchTerm]);
|
|
20
20
|
|
|
21
|
+
if (!savedViewsAreAvailable) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
return (
|
|
22
26
|
<>
|
|
23
27
|
<div className="flex items-center gap-2">
|
|
@@ -35,11 +39,7 @@ export const MyViewsButton: React.FC = () => {
|
|
|
35
39
|
<Trans>My saved views</Trans>
|
|
36
40
|
</TooltipContent>
|
|
37
41
|
</Tooltip>
|
|
38
|
-
{activeView &&
|
|
39
|
-
<span className="text-sm text-muted-foreground">
|
|
40
|
-
{activeView.name}
|
|
41
|
-
</span>
|
|
42
|
-
)}
|
|
42
|
+
{activeView && <span className="text-sm text-muted-foreground">{activeView.name}</span>}
|
|
43
43
|
</div>
|
|
44
44
|
<UserViewsSheet open={sheetOpen} onOpenChange={setSheetOpen} />
|
|
45
45
|
</>
|
|
@@ -13,7 +13,7 @@ interface SaveViewButtonProps {
|
|
|
13
13
|
|
|
14
14
|
export const SaveViewButton: React.FC<SaveViewButtonProps> = ({ disabled }) => {
|
|
15
15
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
16
|
-
const { userViews, globalViews } = useSavedViews();
|
|
16
|
+
const { userViews, globalViews, savedViewsAreAvailable } = useSavedViews();
|
|
17
17
|
const { columnFilters, searchTerm } = useDataTableContext();
|
|
18
18
|
|
|
19
19
|
const hasFilters = columnFilters.length > 0 || (searchTerm && searchTerm.length > 0);
|
|
@@ -24,6 +24,10 @@ export const SaveViewButton: React.FC<SaveViewButtonProps> = ({ disabled }) => {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
if (!savedViewsAreAvailable) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
return (
|
|
28
32
|
<>
|
|
29
33
|
<Button variant="outline" size="sm" onClick={() => setDialogOpen(true)} disabled={disabled}>
|