@vendure/dashboard 3.4.3-master-202509240228 → 3.4.3-master-202509250229

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 (22) hide show
  1. package/index.html +11 -12
  2. package/package.json +4 -4
  3. package/src/app/common/use-page-title.test.ts +263 -0
  4. package/src/app/common/use-page-title.ts +86 -0
  5. package/src/app/routes/__root.tsx +4 -4
  6. package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +9 -4
  7. package/src/app/routes/_authenticated/_shipping-methods/components/metadata-badges.tsx +15 -0
  8. package/src/app/routes/_authenticated/_shipping-methods/components/price-display.tsx +21 -0
  9. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-test-result-wrapper.tsx +87 -0
  10. package/src/app/routes/_authenticated/_shipping-methods/components/test-address-form.tsx +255 -0
  11. package/src/app/routes/_authenticated/_shipping-methods/components/test-order-builder.tsx +243 -0
  12. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-result.tsx +97 -0
  13. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-sheet.tsx +41 -0
  14. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods.tsx +74 -0
  15. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-method-result.tsx +90 -0
  16. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method-sheet.tsx +56 -0
  17. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method.tsx +82 -0
  18. package/src/app/routes/_authenticated/_shipping-methods/components/use-shipping-method-test-state.ts +67 -0
  19. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +27 -0
  20. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +2 -2
  21. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +24 -4
  22. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-method-dialog.tsx +0 -32
@@ -0,0 +1,74 @@
1
+ import { Accordion } from '@/vdb/components/ui/accordion.js';
2
+ import { api } from '@/vdb/graphql/api.js';
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { testEligibleShippingMethodsDocument } from '../shipping-methods.graphql.js';
5
+ import { TestAddressForm } from './test-address-form.js';
6
+ import { TestOrderBuilder } from './test-order-builder.js';
7
+ import { TestShippingMethodsResult } from './test-shipping-methods-result.js';
8
+ import { useShippingMethodTestState } from './use-shipping-method-test-state.js';
9
+
10
+ export function TestShippingMethods() {
11
+ const {
12
+ testAddress,
13
+ testOrderLines,
14
+ testDataUpdated,
15
+ hasTestedOnce,
16
+ expandedAccordions,
17
+ setExpandedAccordions,
18
+ allTestDataPresent,
19
+ handleAddressChange,
20
+ handleOrderLinesChange,
21
+ markTestRun,
22
+ } = useShippingMethodTestState();
23
+
24
+ const { data, isLoading, refetch } = useQuery({
25
+ queryKey: ['testEligibleShippingMethods', testAddress, testOrderLines],
26
+ queryFn: async () => {
27
+ if (!testAddress || !testOrderLines.length) {
28
+ return { testEligibleShippingMethods: [] };
29
+ }
30
+ return api.query(testEligibleShippingMethodsDocument, {
31
+ input: {
32
+ shippingAddress: testAddress,
33
+ lines: testOrderLines.map(l => ({
34
+ productVariantId: l.id,
35
+ quantity: l.quantity,
36
+ })),
37
+ },
38
+ });
39
+ },
40
+ enabled: false,
41
+ });
42
+
43
+ const testResult = data?.testEligibleShippingMethods || [];
44
+
45
+ const runTest = () => {
46
+ if (allTestDataPresent) {
47
+ markTestRun();
48
+ refetch();
49
+ }
50
+ };
51
+
52
+ return (
53
+ <div className="space-y-6 overflow-y-auto max-h-[calc(100vh-200px)] px-4">
54
+ <Accordion
55
+ type="multiple"
56
+ value={expandedAccordions}
57
+ onValueChange={setExpandedAccordions}
58
+ className="w-full"
59
+ >
60
+ <TestOrderBuilder onOrderLinesChange={handleOrderLinesChange} />
61
+ <TestAddressForm onAddressChange={handleAddressChange} />
62
+ </Accordion>
63
+
64
+ <TestShippingMethodsResult
65
+ testResult={testResult}
66
+ okToRun={allTestDataPresent}
67
+ testDataUpdated={testDataUpdated}
68
+ hasTestedOnce={hasTestedOnce}
69
+ onRunTest={runTest}
70
+ loading={isLoading}
71
+ />
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,90 @@
1
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
2
+ import { Trans } from '@/vdb/lib/trans.js';
3
+ import { ResultOf } from 'gql.tada';
4
+ import { Check } from 'lucide-react';
5
+ import { testShippingMethodDocument } from '../shipping-methods.graphql.js';
6
+ import { MetadataBadges } from './metadata-badges.js';
7
+ import { PriceDisplay } from './price-display.js';
8
+ import { ShippingMethodTestResultWrapper } from './shipping-method-test-result-wrapper.js';
9
+
10
+ export type TestShippingMethodResult = ResultOf<typeof testShippingMethodDocument>['testShippingMethod'];
11
+
12
+ interface TestSingleMethodResultProps {
13
+ testResult?: TestShippingMethodResult;
14
+ okToRun: boolean;
15
+ testDataUpdated: boolean;
16
+ hasTestedOnce: boolean;
17
+ onRunTest: () => void;
18
+ loading?: boolean;
19
+ }
20
+
21
+ export function TestSingleMethodResult({
22
+ testResult,
23
+ okToRun,
24
+ testDataUpdated,
25
+ hasTestedOnce,
26
+ onRunTest,
27
+ loading = false,
28
+ }: Readonly<TestSingleMethodResultProps>) {
29
+ const { activeChannel } = useChannel();
30
+ const currencyCode = activeChannel?.defaultCurrencyCode ?? 'USD';
31
+ const showEmptyState = testResult === undefined && hasTestedOnce && !testDataUpdated && !loading;
32
+
33
+ return (
34
+ <ShippingMethodTestResultWrapper
35
+ okToRun={okToRun}
36
+ testDataUpdated={testDataUpdated}
37
+ hasTestedOnce={hasTestedOnce}
38
+ onRunTest={onRunTest}
39
+ loading={loading}
40
+ showEmptyState={showEmptyState}
41
+ emptyState={
42
+ <div className="text-center py-8 text-muted-foreground">
43
+ <Trans>Click "Run Test" to test this shipping method.</Trans>
44
+ </div>
45
+ }
46
+ loadingLabel={<Trans>Testing shipping method...</Trans>}
47
+ >
48
+ {testResult && (
49
+ <div className="space-y-4">
50
+ {testResult.eligible ? (
51
+ <div className="space-y-3">
52
+ {testResult.quote && (
53
+ <div className="p-3 border rounded-lg bg-muted/50">
54
+ <div className="flex justify-between items-center">
55
+ <div className="flex items-center gap-2">
56
+ <Check className="h-5 w-5 text-success" />
57
+ <span className="text-sm">
58
+ <Trans>Shipping method is eligible for this order</Trans>
59
+ </span>
60
+ </div>
61
+ <PriceDisplay
62
+ price={testResult.quote.price}
63
+ priceWithTax={testResult.quote.priceWithTax}
64
+ currencyCode={currencyCode}
65
+ />
66
+ </div>
67
+ <div className="flex-1">
68
+ <MetadataBadges metadata={testResult.quote.metadata} />
69
+ </div>
70
+ </div>
71
+ )}
72
+ </div>
73
+ ) : (
74
+ <div className="text-center py-8">
75
+ <p className="text-destructive">
76
+ <Trans>Shipping method is not eligible for this order</Trans>
77
+ </p>
78
+ <p className="text-sm text-muted-foreground mt-2">
79
+ <Trans>
80
+ This shipping method's eligibility checker conditions are not met for the
81
+ current order and shipping address.
82
+ </Trans>
83
+ </p>
84
+ </div>
85
+ )}
86
+ </div>
87
+ )}
88
+ </ShippingMethodTestResultWrapper>
89
+ );
90
+ }
@@ -0,0 +1,56 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import {
3
+ Sheet,
4
+ SheetContent,
5
+ SheetDescription,
6
+ SheetHeader,
7
+ SheetTitle,
8
+ SheetTrigger,
9
+ } from '@/vdb/components/ui/sheet.js';
10
+ import { Trans } from '@/vdb/lib/trans.js';
11
+ import { VariablesOf } from 'gql.tada';
12
+ import { FlaskConical } from 'lucide-react';
13
+ import { useState } from 'react';
14
+ import { testShippingMethodDocument } from '../shipping-methods.graphql.js';
15
+ import { TestSingleShippingMethod } from './test-single-shipping-method.js';
16
+
17
+ interface TestSingleShippingMethodDialogProps {
18
+ checker?: VariablesOf<typeof testShippingMethodDocument>['input']['checker'];
19
+ calculator?: VariablesOf<typeof testShippingMethodDocument>['input']['calculator'];
20
+ }
21
+
22
+ export function TestSingleShippingMethodSheet({
23
+ checker,
24
+ calculator,
25
+ }: Readonly<TestSingleShippingMethodDialogProps>) {
26
+ const [open, setOpen] = useState(false);
27
+
28
+ return (
29
+ <Sheet open={open} onOpenChange={setOpen}>
30
+ <SheetTrigger asChild>
31
+ <Button variant="secondary">
32
+ <FlaskConical />
33
+ <Trans>Test</Trans>
34
+ </Button>
35
+ </SheetTrigger>
36
+ <SheetContent className="w-[800px] sm:max-w-[800px]">
37
+ <SheetHeader>
38
+ <SheetTitle>
39
+ <Trans>Test Shipping Method</Trans>
40
+ </SheetTitle>
41
+ <SheetDescription>
42
+ <Trans>
43
+ Test this shipping method by simulating an order to see if it's eligible and what
44
+ the shipping cost would be.
45
+ </Trans>
46
+ </SheetDescription>
47
+ </SheetHeader>
48
+ <div className="mt-6">
49
+ {checker && calculator ? (
50
+ <TestSingleShippingMethod checker={checker} calculator={calculator} />
51
+ ) : null}
52
+ </div>
53
+ </SheetContent>
54
+ </Sheet>
55
+ );
56
+ }
@@ -0,0 +1,82 @@
1
+ import { Accordion } from '@/vdb/components/ui/accordion.js';
2
+ import { api } from '@/vdb/graphql/api.js';
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { VariablesOf } from 'gql.tada';
5
+ import { testShippingMethodDocument } from '../shipping-methods.graphql.js';
6
+ import { TestAddressForm } from './test-address-form.js';
7
+ import { TestOrderBuilder } from './test-order-builder.js';
8
+ import { TestSingleMethodResult } from './test-single-method-result.js';
9
+ import { useShippingMethodTestState } from './use-shipping-method-test-state.js';
10
+
11
+ interface TestSingleShippingMethodProps {
12
+ checker: VariablesOf<typeof testShippingMethodDocument>['input']['checker'];
13
+ calculator: VariablesOf<typeof testShippingMethodDocument>['input']['calculator'];
14
+ }
15
+
16
+ export function TestSingleShippingMethod({ checker, calculator }: Readonly<TestSingleShippingMethodProps>) {
17
+ const {
18
+ testAddress,
19
+ testOrderLines,
20
+ testDataUpdated,
21
+ hasTestedOnce,
22
+ expandedAccordions,
23
+ setExpandedAccordions,
24
+ allTestDataPresent,
25
+ handleAddressChange,
26
+ handleOrderLinesChange,
27
+ markTestRun,
28
+ } = useShippingMethodTestState();
29
+
30
+ const { data, isLoading, refetch } = useQuery({
31
+ queryKey: ['testShippingMethod', testAddress, testOrderLines, checker, calculator],
32
+ queryFn: async () => {
33
+ if (!testAddress || !testOrderLines.length) {
34
+ return { testShippingMethod: undefined };
35
+ }
36
+ return api.query(testShippingMethodDocument, {
37
+ input: {
38
+ shippingAddress: testAddress,
39
+ lines: testOrderLines.map(l => ({
40
+ productVariantId: l.id,
41
+ quantity: l.quantity,
42
+ })),
43
+ checker,
44
+ calculator,
45
+ },
46
+ });
47
+ },
48
+ enabled: false,
49
+ });
50
+
51
+ const testResult = data?.testShippingMethod;
52
+
53
+ const runTest = () => {
54
+ if (allTestDataPresent) {
55
+ markTestRun();
56
+ refetch();
57
+ }
58
+ };
59
+
60
+ return (
61
+ <div className="space-y-6 overflow-y-auto max-h-[calc(100vh-200px)] px-4">
62
+ <Accordion
63
+ type="multiple"
64
+ value={expandedAccordions}
65
+ onValueChange={setExpandedAccordions}
66
+ className="w-full"
67
+ >
68
+ <TestOrderBuilder onOrderLinesChange={handleOrderLinesChange} />
69
+ <TestAddressForm onAddressChange={handleAddressChange} />
70
+ </Accordion>
71
+
72
+ <TestSingleMethodResult
73
+ testResult={testResult}
74
+ okToRun={allTestDataPresent}
75
+ testDataUpdated={testDataUpdated}
76
+ hasTestedOnce={hasTestedOnce}
77
+ onRunTest={runTest}
78
+ loading={isLoading}
79
+ />
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,67 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import { TestAddress } from './test-address-form.js';
4
+ import { TestOrderLine } from './test-order-builder.js';
5
+
6
+ export function useShippingMethodTestState() {
7
+ const [testAddress, setTestAddress] = useState<TestAddress | null>(null);
8
+ const [testOrderLines, setTestOrderLines] = useState<TestOrderLine[]>([]);
9
+ const [testDataUpdated, setTestDataUpdated] = useState(true);
10
+ const [hasTestedOnce, setHasTestedOnce] = useState(false);
11
+ const [expandedAccordions, setExpandedAccordions] = useState<string[]>([
12
+ 'test-order',
13
+ 'shipping-address',
14
+ ]);
15
+ const [lastTestedAddress, setLastTestedAddress] = useState<TestAddress | null>(null);
16
+ const [lastTestedOrderLines, setLastTestedOrderLines] = useState<TestOrderLine[]>([]);
17
+
18
+ const allTestDataPresent = !!(testAddress && testOrderLines && testOrderLines.length > 0);
19
+
20
+ const handleAddressChange = useCallback(
21
+ (address: TestAddress) => {
22
+ setTestAddress(address);
23
+ if (hasTestedOnce && JSON.stringify(address) !== JSON.stringify(lastTestedAddress)) {
24
+ setTestDataUpdated(true);
25
+ }
26
+ },
27
+ [hasTestedOnce, lastTestedAddress],
28
+ );
29
+
30
+ const handleOrderLinesChange = useCallback(
31
+ (lines: TestOrderLine[]) => {
32
+ setTestOrderLines(lines);
33
+ if (hasTestedOnce && JSON.stringify(lines) !== JSON.stringify(lastTestedOrderLines)) {
34
+ setTestDataUpdated(true);
35
+ }
36
+ },
37
+ [hasTestedOnce, lastTestedOrderLines],
38
+ );
39
+
40
+ // runTest now only updates state; actual query logic is handled in the component
41
+ const markTestRun = () => {
42
+ setTestDataUpdated(false);
43
+ setHasTestedOnce(true);
44
+ setLastTestedAddress(testAddress);
45
+ setLastTestedOrderLines(testOrderLines);
46
+ setExpandedAccordions([]); // Collapse all accordions
47
+ };
48
+
49
+ return {
50
+ testAddress,
51
+ setTestAddress,
52
+ testOrderLines,
53
+ setTestOrderLines,
54
+ testDataUpdated,
55
+ setTestDataUpdated,
56
+ hasTestedOnce,
57
+ setHasTestedOnce,
58
+ expandedAccordions,
59
+ setExpandedAccordions,
60
+ lastTestedAddress,
61
+ lastTestedOrderLines,
62
+ allTestDataPresent,
63
+ handleAddressChange,
64
+ handleOrderLinesChange,
65
+ markTestRun,
66
+ };
67
+ }
@@ -108,3 +108,30 @@ export const removeShippingMethodsFromChannelDocument = graphql(`
108
108
  }
109
109
  }
110
110
  `);
111
+
112
+ export const testEligibleShippingMethodsDocument = graphql(`
113
+ query TestEligibleShippingMethods($input: TestEligibleShippingMethodsInput!) {
114
+ testEligibleShippingMethods(input: $input) {
115
+ id
116
+ name
117
+ code
118
+ description
119
+ price
120
+ priceWithTax
121
+ metadata
122
+ }
123
+ }
124
+ `);
125
+
126
+ export const testShippingMethodDocument = graphql(`
127
+ query TestShippingMethod($input: TestShippingMethodInput!) {
128
+ testShippingMethod(input: $input) {
129
+ eligible
130
+ quote {
131
+ price
132
+ priceWithTax
133
+ metadata
134
+ }
135
+ }
136
+ }
137
+ `);
@@ -11,7 +11,7 @@ import {
11
11
  DeleteShippingMethodsBulkAction,
12
12
  RemoveShippingMethodsFromChannelBulkAction,
13
13
  } from './components/shipping-method-bulk-actions.js';
14
- import { TestShippingMethodDialog } from './components/test-shipping-method-dialog.js';
14
+ import { TestShippingMethodsSheet } from './components/test-shipping-methods-sheet.js';
15
15
  import { shippingMethodListQuery } from './shipping-methods.graphql.js';
16
16
 
17
17
  export const Route = createFileRoute('/_authenticated/_shipping-methods/shipping-methods')({
@@ -58,6 +58,7 @@ function ShippingMethodListPage() {
58
58
  ]}
59
59
  >
60
60
  <PageActionBarRight>
61
+ <TestShippingMethodsSheet />
61
62
  <PermissionGuard requires={['CreateShippingMethod']}>
62
63
  <Button asChild>
63
64
  <Link to="./new">
@@ -66,7 +67,6 @@ function ShippingMethodListPage() {
66
67
  </Link>
67
68
  </Button>
68
69
  </PermissionGuard>
69
- <TestShippingMethodDialog />
70
70
  </PageActionBarRight>
71
71
  </ListPage>
72
72
  );
@@ -24,6 +24,7 @@ import { toast } from 'sonner';
24
24
  import { FulfillmentHandlerSelector } from './components/fulfillment-handler-selector.js';
25
25
  import { ShippingCalculatorSelector } from './components/shipping-calculator-selector.js';
26
26
  import { ShippingEligibilityCheckerSelector } from './components/shipping-eligibility-checker-selector.js';
27
+ import { TestSingleShippingMethodSheet } from './components/test-single-shipping-method-sheet.js';
27
28
  import {
28
29
  createShippingMethodDocument,
29
30
  shippingMethodDetailDocument,
@@ -84,19 +85,35 @@ function ShippingMethodDetailPage() {
84
85
  },
85
86
  params: { id: params.id },
86
87
  onSuccess: async data => {
87
- toast.success(i18n.t(creatingNewEntity ? 'Successfully created shipping method' : 'Successfully updated shipping method'));
88
+ toast.success(
89
+ i18n.t(
90
+ creatingNewEntity
91
+ ? 'Successfully created shipping method'
92
+ : 'Successfully updated shipping method',
93
+ ),
94
+ );
88
95
  resetForm();
89
96
  if (creatingNewEntity) {
90
97
  await navigate({ to: `../$id`, params: { id: data.id } });
91
98
  }
92
99
  },
93
100
  onError: err => {
94
- toast.error(i18n.t(creatingNewEntity ? 'Failed to create shipping method' : 'Failed to update shipping method'), {
95
- description: err instanceof Error ? err.message : 'Unknown error',
96
- });
101
+ toast.error(
102
+ i18n.t(
103
+ creatingNewEntity
104
+ ? 'Failed to create shipping method'
105
+ : 'Failed to update shipping method',
106
+ ),
107
+ {
108
+ description: err instanceof Error ? err.message : 'Unknown error',
109
+ },
110
+ );
97
111
  },
98
112
  });
99
113
 
114
+ const checker = form.watch('checker');
115
+ const calculator = form.watch('calculator');
116
+
100
117
  return (
101
118
  <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
102
119
  <PageTitle>
@@ -104,6 +121,9 @@ function ShippingMethodDetailPage() {
104
121
  </PageTitle>
105
122
  <PageActionBar>
106
123
  <PageActionBarRight>
124
+ {!creatingNewEntity && entity && (
125
+ <TestSingleShippingMethodSheet checker={checker} calculator={calculator} />
126
+ )}
107
127
  <PermissionGuard requires={['UpdateShippingMethod']}>
108
128
  <Button
109
129
  type="submit"
@@ -1,32 +0,0 @@
1
- import { Button } from '@/vdb/components/ui/button.js';
2
- import {
3
- Dialog,
4
- DialogContent,
5
- DialogDescription,
6
- DialogHeader,
7
- DialogTitle,
8
- DialogTrigger,
9
- } from '@/vdb/components/ui/dialog.js';
10
- import { Trans } from '@/vdb/lib/trans.js';
11
- import { TestTube } from 'lucide-react';
12
-
13
- export function TestShippingMethodDialog() {
14
- return (
15
- <Dialog>
16
- <DialogTrigger asChild>
17
- <Button variant="secondary">
18
- <TestTube />
19
- <Trans>Test shipping method</Trans>
20
- </Button>
21
- </DialogTrigger>
22
- <DialogContent className="min-w-[800px]">
23
- <DialogHeader>
24
- <DialogTitle>Test shipping method</DialogTitle>
25
- <DialogDescription>
26
- Test your shipping method by simulating a new order.
27
- </DialogDescription>
28
- </DialogHeader>
29
- </DialogContent>
30
- </Dialog>
31
- );
32
- }