@vendure/dashboard 3.4.2-master-202509020230 → 3.4.2-master-202509030226
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +3 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +7 -7
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +3 -3
- package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -4
- package/src/app/routes/_authenticated/_products/components/add-option-group-dialog.tsx +127 -0
- package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +41 -39
- package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +1 -33
- package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +7 -42
- package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +38 -134
- package/src/app/routes/_authenticated/_products/components/option-groups-editor.tsx +180 -0
- package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +9 -39
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_products/products.graphql.ts +136 -0
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -9
- package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +405 -0
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +2 -2
- package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +3 -3
- package/src/lib/components/data-input/rich-text-input.tsx +8 -4
- package/src/lib/components/layout/channel-switcher.tsx +27 -6
- package/src/lib/components/layout/manage-languages-dialog.tsx +2 -2
- package/src/lib/components/shared/asset/asset-gallery.tsx +20 -2
- package/src/lib/components/shared/asset/asset-picker-dialog.tsx +5 -5
- package/src/lib/components/shared/assign-to-channel-dialog.tsx +2 -2
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -2
- package/src/lib/graphql/api.ts +3 -1
- package/src/lib/hooks/use-permissions.ts +4 -4
- package/src/lib/providers/auth.tsx +8 -0
- package/src/lib/providers/channel-provider.tsx +48 -57
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.4.2-master-
|
|
4
|
+
"version": "3.4.2-master-202509030226",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"@types/react": "^19.0.10",
|
|
101
101
|
"@types/react-dom": "^19.0.4",
|
|
102
102
|
"@uidotdev/usehooks": "^2.4.1",
|
|
103
|
-
"@vendure/common": "^3.4.2-master-
|
|
104
|
-
"@vendure/core": "^3.4.2-master-
|
|
103
|
+
"@vendure/common": "^3.4.2-master-202509030226",
|
|
104
|
+
"@vendure/core": "^3.4.2-master-202509030226",
|
|
105
105
|
"@vitejs/plugin-react": "^4.3.4",
|
|
106
106
|
"acorn": "^8.11.3",
|
|
107
107
|
"acorn-walk": "^8.3.2",
|
|
@@ -152,5 +152,5 @@
|
|
|
152
152
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
153
153
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
154
154
|
},
|
|
155
|
-
"gitHead": "
|
|
155
|
+
"gitHead": "ea17999f2c068b9d5c96d81f4646393acb5baedf"
|
|
156
156
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@/vdb/framework/layout-engine/page-layout.js';
|
|
23
23
|
import { detailPageRouteLoader } from '@/vdb/framework/page/detail-page-route-loader.js';
|
|
24
24
|
import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
25
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
25
26
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
26
27
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
27
28
|
import { toast } from 'sonner';
|
|
@@ -49,6 +50,7 @@ function ChannelDetailPage() {
|
|
|
49
50
|
const navigate = useNavigate();
|
|
50
51
|
const creatingNewEntity = params.id === NEW_ENTITY_PATH;
|
|
51
52
|
const { i18n } = useLingui();
|
|
53
|
+
const { refreshChannels } = useChannel();
|
|
52
54
|
|
|
53
55
|
const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
|
|
54
56
|
pageId,
|
|
@@ -81,6 +83,7 @@ function ChannelDetailPage() {
|
|
|
81
83
|
onSuccess: async data => {
|
|
82
84
|
if (data.__typename === 'Channel') {
|
|
83
85
|
toast(i18n.t('Successfully updated channel'));
|
|
86
|
+
refreshChannels();
|
|
84
87
|
resetForm();
|
|
85
88
|
if (creatingNewEntity) {
|
|
86
89
|
await navigate({ to: `../$id`, params: { id: data.id } });
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/react-query';
|
|
2
|
-
import { useState } from 'react';
|
|
3
2
|
import { FolderTree } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
6
6
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
7
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
7
8
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
8
|
-
import { api } from '@/vdb/graphql/api.js';
|
|
9
9
|
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
10
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
10
11
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
11
|
-
import {
|
|
12
|
-
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
12
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
13
13
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
14
14
|
import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
|
|
15
15
|
import {
|
|
@@ -41,7 +41,7 @@ export const AssignCollectionsToChannelBulkAction: BulkActionComponent<any> = ({
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
export const RemoveCollectionsFromChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
44
|
-
const {
|
|
44
|
+
const { activeChannel } = useChannel();
|
|
45
45
|
const queryClient = useQueryClient();
|
|
46
46
|
|
|
47
47
|
return (
|
|
@@ -53,7 +53,7 @@ export const RemoveCollectionsFromChannelBulkAction: BulkActionComponent<any> =
|
|
|
53
53
|
requiredPermissions={['UpdateCatalog', 'UpdateCollection']}
|
|
54
54
|
buildInput={() => ({
|
|
55
55
|
collectionIds: selection.map(s => s.id),
|
|
56
|
-
channelId:
|
|
56
|
+
channelId: activeChannel?.id,
|
|
57
57
|
})}
|
|
58
58
|
onSuccess={() => {
|
|
59
59
|
queryClient.invalidateQueries({ queryKey: ['childCollections'] });
|
|
@@ -2,9 +2,9 @@ import { toast } from 'sonner';
|
|
|
2
2
|
|
|
3
3
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
4
4
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
5
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
5
6
|
import { api } from '@/vdb/graphql/api.js';
|
|
6
7
|
import { ResultOf } from '@/vdb/graphql/graphql.js';
|
|
7
|
-
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
8
8
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
9
9
|
import { useLingui } from '@/vdb/lib/trans.js';
|
|
10
10
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
@@ -45,7 +45,7 @@ export const AssignFacetsToChannelBulkAction: BulkActionComponent<any> = ({ sele
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
export const RemoveFacetsFromChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
48
|
-
const {
|
|
48
|
+
const { activeChannel } = useChannel();
|
|
49
49
|
const { i18n } = useLingui();
|
|
50
50
|
|
|
51
51
|
return (
|
|
@@ -57,7 +57,7 @@ export const RemoveFacetsFromChannelBulkAction: BulkActionComponent<any> = ({ se
|
|
|
57
57
|
requiredPermissions={['UpdateCatalog', 'UpdateFacet']}
|
|
58
58
|
buildInput={() => ({
|
|
59
59
|
facetIds: selection.map(s => s.id),
|
|
60
|
-
channelId:
|
|
60
|
+
channelId: activeChannel?.id,
|
|
61
61
|
})}
|
|
62
62
|
onSuccess={result => {
|
|
63
63
|
const typedResult = result as ResultOf<typeof removeFacetsFromChannelDocument>;
|
package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx
CHANGED
|
@@ -40,7 +40,7 @@ export const AssignPaymentMethodsToChannelBulkAction: BulkActionComponent<any> =
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
export const RemovePaymentMethodsFromChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
|
|
43
|
-
const {
|
|
43
|
+
const { activeChannel } = useChannel();
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<RemoveFromChannelBulkAction
|
|
@@ -51,7 +51,7 @@ export const RemovePaymentMethodsFromChannelBulkAction: BulkActionComponent<any>
|
|
|
51
51
|
requiredPermissions={['UpdatePaymentMethod']}
|
|
52
52
|
buildInput={() => ({
|
|
53
53
|
paymentMethodIds: selection.map(s => s.id),
|
|
54
|
-
channelId:
|
|
54
|
+
channelId: activeChannel?.id,
|
|
55
55
|
})}
|
|
56
56
|
/>
|
|
57
57
|
);
|
package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx
CHANGED
|
@@ -4,11 +4,11 @@ import { useState } from 'react';
|
|
|
4
4
|
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
5
5
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
6
6
|
import { usePriceFactor } from '@/vdb/components/shared/assign-to-channel-dialog.js';
|
|
7
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
7
8
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
8
|
-
import { api } from '@/vdb/graphql/api.js';
|
|
9
9
|
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
10
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
10
11
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
11
|
-
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
12
12
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
13
13
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
14
14
|
|
|
@@ -58,7 +58,7 @@ export const RemoveProductVariantsFromChannelBulkAction: BulkActionComponent<any
|
|
|
58
58
|
selection,
|
|
59
59
|
table,
|
|
60
60
|
}) => {
|
|
61
|
-
const {
|
|
61
|
+
const { activeChannel } = useChannel();
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
64
|
<RemoveFromChannelBulkAction
|
|
@@ -69,7 +69,7 @@ export const RemoveProductVariantsFromChannelBulkAction: BulkActionComponent<any
|
|
|
69
69
|
requiredPermissions={['UpdateCatalog', 'UpdateProduct']}
|
|
70
70
|
buildInput={() => ({
|
|
71
71
|
productVariantIds: selection.map(s => s.id),
|
|
72
|
-
channelId:
|
|
72
|
+
channelId: activeChannel?.id,
|
|
73
73
|
})}
|
|
74
74
|
/>
|
|
75
75
|
);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogFooter,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
DialogTrigger,
|
|
9
|
+
} from '@/vdb/components/ui/dialog.js';
|
|
10
|
+
import { Form } from '@/vdb/components/ui/form.js';
|
|
11
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
12
|
+
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
13
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
14
|
+
import { useMutation } from '@tanstack/react-query';
|
|
15
|
+
import { Plus, Save } from 'lucide-react';
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { useForm } from 'react-hook-form';
|
|
18
|
+
import { toast } from 'sonner';
|
|
19
|
+
import { addOptionGroupToProductDocument, createProductOptionGroupDocument } from '../products.graphql.js';
|
|
20
|
+
import { OptionGroup, optionGroupSchema, SingleOptionGroupEditor } from './option-groups-editor.js';
|
|
21
|
+
|
|
22
|
+
export function AddOptionGroupDialog({
|
|
23
|
+
productId,
|
|
24
|
+
onSuccess,
|
|
25
|
+
}: Readonly<{
|
|
26
|
+
productId: string;
|
|
27
|
+
onSuccess?: () => void;
|
|
28
|
+
}>) {
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
const { i18n } = useLingui();
|
|
31
|
+
|
|
32
|
+
const form = useForm<OptionGroup>({
|
|
33
|
+
resolver: zodResolver(optionGroupSchema),
|
|
34
|
+
defaultValues: {
|
|
35
|
+
name: '',
|
|
36
|
+
values: [],
|
|
37
|
+
},
|
|
38
|
+
mode: 'onChange',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const createOptionGroupMutation = useMutation({
|
|
42
|
+
mutationFn: api.mutate(createProductOptionGroupDocument),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const addOptionGroupToProductMutation = useMutation({
|
|
46
|
+
mutationFn: api.mutate(addOptionGroupToProductDocument),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const handleSave = async () => {
|
|
50
|
+
const formValue = form.getValues();
|
|
51
|
+
if (!formValue.name || formValue.values.length === 0) return;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const createResult = await createOptionGroupMutation.mutateAsync({
|
|
55
|
+
input: {
|
|
56
|
+
code: formValue.name.toLowerCase().replace(/\s+/g, '-'),
|
|
57
|
+
translations: [
|
|
58
|
+
{
|
|
59
|
+
languageCode: 'en',
|
|
60
|
+
name: formValue.name,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
options: formValue.values.map(value => ({
|
|
64
|
+
code: value.value.toLowerCase().replace(/\s+/g, '-'),
|
|
65
|
+
translations: [
|
|
66
|
+
{
|
|
67
|
+
languageCode: 'en',
|
|
68
|
+
name: value.value,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
})),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (createResult?.createProductOptionGroup) {
|
|
76
|
+
await addOptionGroupToProductMutation.mutateAsync({
|
|
77
|
+
productId,
|
|
78
|
+
optionGroupId: createResult.createProductOptionGroup.id,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
toast.success(i18n.t('Successfully created option group'));
|
|
83
|
+
setOpen(false);
|
|
84
|
+
onSuccess?.();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
toast.error(i18n.t('Failed to create option group'), {
|
|
87
|
+
description: error instanceof Error ? error.message : i18n.t('Unknown error'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
94
|
+
<DialogTrigger asChild>
|
|
95
|
+
<Button variant="outline">
|
|
96
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
97
|
+
<Trans>Add option group</Trans>
|
|
98
|
+
</Button>
|
|
99
|
+
</DialogTrigger>
|
|
100
|
+
<DialogContent className="max-w-2xl" aria-description={'Add option group'}>
|
|
101
|
+
<DialogHeader>
|
|
102
|
+
<DialogTitle>
|
|
103
|
+
<Trans>Add option group to product</Trans>
|
|
104
|
+
</DialogTitle>
|
|
105
|
+
</DialogHeader>
|
|
106
|
+
<div className="space-y-4">
|
|
107
|
+
<Form {...form}>
|
|
108
|
+
<SingleOptionGroupEditor control={form.control} fieldArrayPath={''} />
|
|
109
|
+
</Form>
|
|
110
|
+
</div>
|
|
111
|
+
<DialogFooter>
|
|
112
|
+
<Button
|
|
113
|
+
onClick={handleSave}
|
|
114
|
+
disabled={
|
|
115
|
+
!form.formState.isValid ||
|
|
116
|
+
createOptionGroupMutation.isPending ||
|
|
117
|
+
addOptionGroupToProductMutation.isPending
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
<Save className="mr-2 h-4 w-4" />
|
|
121
|
+
<Trans>Save option group</Trans>
|
|
122
|
+
</Button>
|
|
123
|
+
</DialogFooter>
|
|
124
|
+
</DialogContent>
|
|
125
|
+
</Dialog>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -22,6 +22,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
22
22
|
import { useForm } from 'react-hook-form';
|
|
23
23
|
import { toast } from 'sonner';
|
|
24
24
|
import * as z from 'zod';
|
|
25
|
+
import { createProductOptionDocument } from '../products.graphql.js';
|
|
25
26
|
import { CreateProductOptionsDialog } from './create-product-options-dialog.js';
|
|
26
27
|
import { ProductOptionSelect } from './product-option-select.js';
|
|
27
28
|
|
|
@@ -63,43 +64,6 @@ const createProductVariantDocument = graphql(`
|
|
|
63
64
|
}
|
|
64
65
|
`);
|
|
65
66
|
|
|
66
|
-
const createProductOptionDocument = graphql(`
|
|
67
|
-
mutation CreateProductOption($input: CreateProductOptionInput!) {
|
|
68
|
-
createProductOption(input: $input) {
|
|
69
|
-
id
|
|
70
|
-
code
|
|
71
|
-
name
|
|
72
|
-
groupId
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
`);
|
|
76
|
-
|
|
77
|
-
const createProductOptionGroupDocument = graphql(`
|
|
78
|
-
mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
|
|
79
|
-
createProductOptionGroup(input: $input) {
|
|
80
|
-
id
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
`);
|
|
84
|
-
|
|
85
|
-
const addOptionGroupToProductDocument = graphql(`
|
|
86
|
-
mutation AddOptionGroupToProduct($productId: ID!, $optionGroupId: ID!) {
|
|
87
|
-
addOptionGroupToProduct(productId: $productId, optionGroupId: $optionGroupId) {
|
|
88
|
-
id
|
|
89
|
-
optionGroups {
|
|
90
|
-
id
|
|
91
|
-
code
|
|
92
|
-
name
|
|
93
|
-
options {
|
|
94
|
-
id
|
|
95
|
-
code
|
|
96
|
-
name
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
`);
|
|
102
|
-
|
|
103
67
|
const formSchema = z.object({
|
|
104
68
|
name: z.string().min(1, 'Name is required'),
|
|
105
69
|
sku: z.string().min(1, 'SKU is required'),
|
|
@@ -256,8 +220,8 @@ export function AddProductVariantDialog({
|
|
|
256
220
|
[createProductVariantMutation, productData?.product, duplicateVariantError, productId],
|
|
257
221
|
);
|
|
258
222
|
|
|
259
|
-
// If there are no option groups, show the create options dialog instead
|
|
260
|
-
if (productData?.product?.optionGroups.length === 0) {
|
|
223
|
+
// If there are no option groups and no variants, show the create options dialog instead
|
|
224
|
+
if (productData?.product?.optionGroups.length === 0 && productData?.product?.variants.length === 0) {
|
|
261
225
|
return (
|
|
262
226
|
<CreateProductOptionsDialog
|
|
263
227
|
productId={productId}
|
|
@@ -269,6 +233,35 @@ export function AddProductVariantDialog({
|
|
|
269
233
|
);
|
|
270
234
|
}
|
|
271
235
|
|
|
236
|
+
// If there are no option groups but there are existing variants, show a different UI
|
|
237
|
+
if (productData?.product?.optionGroups.length === 0 && productData?.product?.variants.length > 0) {
|
|
238
|
+
return (
|
|
239
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
240
|
+
<DialogTrigger asChild>
|
|
241
|
+
<Button variant="outline">
|
|
242
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
243
|
+
<Trans>Add variant</Trans>
|
|
244
|
+
</Button>
|
|
245
|
+
</DialogTrigger>
|
|
246
|
+
<DialogContent>
|
|
247
|
+
<DialogHeader>
|
|
248
|
+
<DialogTitle>
|
|
249
|
+
<Trans>Add product options first</Trans>
|
|
250
|
+
</DialogTitle>
|
|
251
|
+
</DialogHeader>
|
|
252
|
+
<div className="space-y-4">
|
|
253
|
+
<p className="text-sm text-muted-foreground">
|
|
254
|
+
<Trans>
|
|
255
|
+
This product has existing variants but no option groups defined. You need to
|
|
256
|
+
add option groups before creating new variants.
|
|
257
|
+
</Trans>
|
|
258
|
+
</p>
|
|
259
|
+
</div>
|
|
260
|
+
</DialogContent>
|
|
261
|
+
</Dialog>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
272
265
|
return (
|
|
273
266
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
274
267
|
<DialogTrigger asChild>
|
|
@@ -291,6 +284,15 @@ export function AddProductVariantDialog({
|
|
|
291
284
|
}}
|
|
292
285
|
className="space-y-4"
|
|
293
286
|
>
|
|
287
|
+
{productData?.product?.optionGroups.length && (
|
|
288
|
+
<div className="flex flex-col gap-2">
|
|
289
|
+
<div className="flex justify-between items-center">
|
|
290
|
+
<label className="text-sm font-medium">
|
|
291
|
+
<Trans>Product options</Trans>
|
|
292
|
+
</label>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
294
296
|
<div className="grid grid-cols-2 gap-4">
|
|
295
297
|
{productData?.product?.optionGroups.map(group => (
|
|
296
298
|
<ProductOptionSelect
|
package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import { useState } from 'react';
|
|
|
20
20
|
import { useForm } from 'react-hook-form';
|
|
21
21
|
import { toast } from 'sonner';
|
|
22
22
|
import * as z from 'zod';
|
|
23
|
+
import { addOptionGroupToProductDocument, createProductOptionGroupDocument } from '../products.graphql.js';
|
|
23
24
|
|
|
24
25
|
const getProductDocument = graphql(`
|
|
25
26
|
query GetProduct($productId: ID!) {
|
|
@@ -51,39 +52,6 @@ const getProductDocument = graphql(`
|
|
|
51
52
|
}
|
|
52
53
|
`);
|
|
53
54
|
|
|
54
|
-
const createProductOptionGroupDocument = graphql(`
|
|
55
|
-
mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
|
|
56
|
-
createProductOptionGroup(input: $input) {
|
|
57
|
-
id
|
|
58
|
-
code
|
|
59
|
-
name
|
|
60
|
-
options {
|
|
61
|
-
id
|
|
62
|
-
code
|
|
63
|
-
name
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
`);
|
|
68
|
-
|
|
69
|
-
const addOptionGroupToProductDocument = graphql(`
|
|
70
|
-
mutation AddOptionGroupToProduct($productId: ID!, $optionGroupId: ID!) {
|
|
71
|
-
addOptionGroupToProduct(productId: $productId, optionGroupId: $optionGroupId) {
|
|
72
|
-
id
|
|
73
|
-
optionGroups {
|
|
74
|
-
id
|
|
75
|
-
code
|
|
76
|
-
name
|
|
77
|
-
options {
|
|
78
|
-
id
|
|
79
|
-
code
|
|
80
|
-
name
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
`);
|
|
86
|
-
|
|
87
55
|
const updateProductVariantDocument = graphql(`
|
|
88
56
|
mutation UpdateProductVariant($input: UpdateProductVariantInput!) {
|
|
89
57
|
updateProductVariant(input: $input) {
|
package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx
CHANGED
|
@@ -9,54 +9,19 @@ import {
|
|
|
9
9
|
DialogTrigger,
|
|
10
10
|
} from '@/vdb/components/ui/dialog.js';
|
|
11
11
|
import { api } from '@/vdb/graphql/api.js';
|
|
12
|
-
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
13
12
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
14
13
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
15
14
|
import { normalizeString } from '@/vdb/lib/utils.js';
|
|
16
15
|
import { useMutation } from '@tanstack/react-query';
|
|
17
16
|
import { Plus } from 'lucide-react';
|
|
18
17
|
import { useCallback, useState } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
addOptionGroupToProductDocument,
|
|
20
|
+
createProductOptionGroupDocument,
|
|
21
|
+
createProductVariantsDocument,
|
|
22
|
+
} from '../products.graphql.js';
|
|
19
23
|
import { CreateProductVariants, VariantConfiguration } from './create-product-variants.js';
|
|
20
24
|
|
|
21
|
-
const createProductOptionsMutation = graphql(`
|
|
22
|
-
mutation CreateOptionGroups($input: CreateProductOptionGroupInput!) {
|
|
23
|
-
createProductOptionGroup(input: $input) {
|
|
24
|
-
id
|
|
25
|
-
name
|
|
26
|
-
options {
|
|
27
|
-
id
|
|
28
|
-
code
|
|
29
|
-
name
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
`);
|
|
34
|
-
|
|
35
|
-
export const addOptionGroupToProductDocument = graphql(`
|
|
36
|
-
mutation AddOptionGroupToProduct($productId: ID!, $optionGroupId: ID!) {
|
|
37
|
-
addOptionGroupToProduct(productId: $productId, optionGroupId: $optionGroupId) {
|
|
38
|
-
id
|
|
39
|
-
optionGroups {
|
|
40
|
-
id
|
|
41
|
-
code
|
|
42
|
-
options {
|
|
43
|
-
id
|
|
44
|
-
code
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
`);
|
|
50
|
-
|
|
51
|
-
export const createProductVariantsDocument = graphql(`
|
|
52
|
-
mutation CreateProductVariants($input: [CreateProductVariantInput!]!) {
|
|
53
|
-
createProductVariants(input: $input) {
|
|
54
|
-
id
|
|
55
|
-
name
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
`);
|
|
59
|
-
|
|
60
25
|
export function CreateProductVariantsDialog({
|
|
61
26
|
productId,
|
|
62
27
|
productName,
|
|
@@ -71,7 +36,7 @@ export function CreateProductVariantsDialog({
|
|
|
71
36
|
const [open, setOpen] = useState(false);
|
|
72
37
|
|
|
73
38
|
const createOptionGroupMutation = useMutation({
|
|
74
|
-
mutationFn: api.mutate(
|
|
39
|
+
mutationFn: api.mutate(createProductOptionGroupDocument),
|
|
75
40
|
});
|
|
76
41
|
|
|
77
42
|
const addOptionGroupToProductMutation = useMutation({
|
|
@@ -180,7 +145,7 @@ export function CreateProductVariantsDialog({
|
|
|
180
145
|
</Button>
|
|
181
146
|
</DialogTrigger>
|
|
182
147
|
|
|
183
|
-
<DialogContent>
|
|
148
|
+
<DialogContent className="max-w-90vw">
|
|
184
149
|
<DialogHeader>
|
|
185
150
|
<DialogTitle>
|
|
186
151
|
<Trans>Create Variants</Trans>
|