@vendure/dashboard 3.3.6-master-202507010922 → 3.3.6-master-202507020234
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 +131 -131
- package/src/app/common/delete-bulk-action.tsx +147 -0
- package/src/app/common/duplicate-bulk-action.tsx +1 -1
- package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +7 -0
- package/src/app/routes/_authenticated/_administrators/components/administrator-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +11 -0
- package/src/app/routes/_authenticated/_assets/assets.tsx +10 -2
- package/src/app/routes/_authenticated/_assets/components/asset-bulk-actions.tsx +45 -0
- package/src/app/routes/_authenticated/_channels/channels.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_channels/channels.tsx +7 -0
- package/src/app/routes/_authenticated/_channels/components/channel-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +39 -110
- package/src/app/routes/_authenticated/_countries/components/country-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_countries/countries.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_countries/countries.tsx +7 -0
- package/src/app/routes/_authenticated/_customer-groups/components/customer-group-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +7 -0
- package/src/app/routes/_authenticated/_customers/components/customer-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +9 -1
- package/src/app/routes/_authenticated/_customers/customers.tsx +7 -0
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +104 -0
- package/src/app/routes/_authenticated/_facets/facets.graphql.ts +30 -0
- package/src/app/routes/_authenticated/_facets/facets.tsx +24 -0
- package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +58 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +27 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +30 -8
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +4 -1
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +36 -110
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +36 -105
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +82 -0
- package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +25 -0
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +24 -0
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_roles/components/role-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_roles/roles.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_roles/roles.tsx +7 -0
- package/src/app/routes/_authenticated/_sellers/components/seller-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_sellers/sellers.tsx +7 -0
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +61 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +27 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +19 -0
- package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +58 -0
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +25 -0
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +19 -0
- package/src/app/routes/_authenticated/_tax-categories/components/tax-category-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +7 -0
- package/src/app/routes/_authenticated/_tax-rates/components/tax-rate-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +7 -0
- package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +15 -0
- package/src/app/routes/_authenticated/_zones/zones.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_zones/zones.tsx +7 -0
- package/src/lib/components/shared/asset/asset-bulk-actions.tsx +90 -0
- package/src/lib/components/shared/asset/asset-gallery.tsx +12 -7
- package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +70 -0
- package/src/{app/routes/_authenticated/_products/components → lib/components/shared}/assign-to-channel-dialog.tsx +48 -30
- package/src/lib/components/shared/detail-page-button.tsx +1 -1
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +89 -0
- package/src/lib/framework/extension-api/use-dashboard-extensions.ts +2 -1
- package/src/lib/framework/form-engine/use-generated-form.tsx +5 -8
- package/src/lib/framework/page/use-detail-page.ts +4 -4
- package/src/lib/framework/page/use-extended-router.tsx +4 -5
- package/src/lib/hooks/use-channel.ts +2 -1
- package/src/lib/hooks/use-extended-detail-query.ts +2 -1
- package/src/lib/hooks/use-extended-list-query.ts +3 -2
- package/src/lib/hooks/use-grouped-permissions.ts +4 -2
- package/src/lib/hooks/use-page-block.tsx +1 -1
- package/src/lib/hooks/use-page.tsx +2 -2
- package/src/lib/hooks/use-permissions.ts +3 -2
- package/src/lib/hooks/use-server-config.ts +2 -1
- package/src/lib/hooks/use-theme.ts +2 -1
- package/src/lib/providers/auth.tsx +34 -11
- package/src/app/routes/_authenticated/_collections/components/assign-collections-to-channel-dialog.tsx +0 -110
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMutation } from '@tanstack/react-query';
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { ReactNode, useState } from 'react';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
|
|
5
5
|
import { ChannelCodeLabel } from '@/components/shared/channel-code-label.js';
|
|
@@ -23,9 +23,24 @@ interface AssignToChannelDialogProps {
|
|
|
23
23
|
open: boolean;
|
|
24
24
|
onOpenChange: (open: boolean) => void;
|
|
25
25
|
entityIds: string[];
|
|
26
|
-
entityType:
|
|
26
|
+
entityType: string;
|
|
27
27
|
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
28
28
|
onSuccess?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Function to build the input object for the mutation
|
|
31
|
+
* @param channelId - The selected channel ID
|
|
32
|
+
* @param additionalData - Any additional data (like priceFactor for products)
|
|
33
|
+
* @returns The input object for the mutation
|
|
34
|
+
*/
|
|
35
|
+
buildInput: (channelId: string, additionalData?: Record<string, any>) => Record<string, any>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional additional form fields to render
|
|
38
|
+
*/
|
|
39
|
+
additionalFields?: ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Optional additional data to pass to buildInput
|
|
42
|
+
*/
|
|
43
|
+
additionalData?: Record<string, any>;
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
export function AssignToChannelDialog({
|
|
@@ -35,10 +50,12 @@ export function AssignToChannelDialog({
|
|
|
35
50
|
entityType,
|
|
36
51
|
mutationFn,
|
|
37
52
|
onSuccess,
|
|
38
|
-
|
|
53
|
+
buildInput,
|
|
54
|
+
additionalFields,
|
|
55
|
+
additionalData = {},
|
|
56
|
+
}: Readonly<AssignToChannelDialogProps>) {
|
|
39
57
|
const { i18n } = useLingui();
|
|
40
58
|
const [selectedChannelId, setSelectedChannelId] = useState<string>('');
|
|
41
|
-
const [priceFactor, setPriceFactor] = useState<number>(1);
|
|
42
59
|
const { channels, selectedChannel } = useChannel();
|
|
43
60
|
|
|
44
61
|
// Filter out the currently selected channel from available options
|
|
@@ -62,19 +79,7 @@ export function AssignToChannelDialog({
|
|
|
62
79
|
return;
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
const input =
|
|
66
|
-
entityType === 'products'
|
|
67
|
-
? {
|
|
68
|
-
productIds: entityIds,
|
|
69
|
-
channelId: selectedChannelId,
|
|
70
|
-
priceFactor,
|
|
71
|
-
}
|
|
72
|
-
: {
|
|
73
|
-
productVariantIds: entityIds,
|
|
74
|
-
channelId: selectedChannelId,
|
|
75
|
-
priceFactor,
|
|
76
|
-
};
|
|
77
|
-
|
|
82
|
+
const input = buildInput(selectedChannelId, additionalData);
|
|
78
83
|
mutate({ input });
|
|
79
84
|
};
|
|
80
85
|
|
|
@@ -109,19 +114,7 @@ export function AssignToChannelDialog({
|
|
|
109
114
|
</SelectContent>
|
|
110
115
|
</Select>
|
|
111
116
|
</div>
|
|
112
|
-
|
|
113
|
-
<label className="text-sm font-medium">
|
|
114
|
-
<Trans>Price conversion factor</Trans>
|
|
115
|
-
</label>
|
|
116
|
-
<Input
|
|
117
|
-
type="number"
|
|
118
|
-
min="0"
|
|
119
|
-
max="99999"
|
|
120
|
-
step="0.01"
|
|
121
|
-
value={priceFactor}
|
|
122
|
-
onChange={e => setPriceFactor(parseFloat(e.target.value) || 1)}
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
117
|
+
{additionalFields}
|
|
125
118
|
</div>
|
|
126
119
|
<DialogFooter>
|
|
127
120
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
@@ -135,3 +128,28 @@ export function AssignToChannelDialog({
|
|
|
135
128
|
</Dialog>
|
|
136
129
|
);
|
|
137
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Hook for managing price factor state in assign-to-channel dialogs
|
|
134
|
+
*/
|
|
135
|
+
export function usePriceFactor() {
|
|
136
|
+
const [priceFactor, setPriceFactor] = useState<number>(1);
|
|
137
|
+
|
|
138
|
+
const priceFactorField = (
|
|
139
|
+
<div className="grid gap-2">
|
|
140
|
+
<label className="text-sm font-medium">
|
|
141
|
+
<Trans>Price conversion factor</Trans>
|
|
142
|
+
</label>
|
|
143
|
+
<Input
|
|
144
|
+
type="number"
|
|
145
|
+
min="0"
|
|
146
|
+
max="99999"
|
|
147
|
+
step="0.01"
|
|
148
|
+
value={priceFactor}
|
|
149
|
+
onChange={e => setPriceFactor(parseFloat(e.target.value) || 1)}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return { priceFactor, priceFactorField };
|
|
155
|
+
}
|
|
@@ -20,7 +20,7 @@ export function DetailPageButton({
|
|
|
20
20
|
}
|
|
21
21
|
return (
|
|
22
22
|
<Button asChild variant="ghost" disabled={disabled}>
|
|
23
|
-
<Link to={href ?? `./${id}`} search={search ?? {}}>
|
|
23
|
+
<Link to={href ?? `./${id}`} search={search ?? {}} preload={false}>
|
|
24
24
|
{label}
|
|
25
25
|
{!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
|
|
26
26
|
</Link>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { LayersIcon } from 'lucide-react';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
|
|
5
|
+
import { DataTableBulkActionItem } from '@/components/data-table/data-table-bulk-action-item.js';
|
|
6
|
+
import { ResultOf } from '@/graphql/graphql.js';
|
|
7
|
+
import { useChannel, usePaginatedList } from '@/index.js';
|
|
8
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
9
|
+
|
|
10
|
+
interface RemoveFromChannelBulkActionProps {
|
|
11
|
+
selection: any[];
|
|
12
|
+
table: any;
|
|
13
|
+
entityType: string;
|
|
14
|
+
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
15
|
+
requiredPermissions: string[];
|
|
16
|
+
buildInput: () => Record<string, any>;
|
|
17
|
+
/**
|
|
18
|
+
* Additional callback to run on success, after the standard refetch and reset
|
|
19
|
+
* @param result - The result from the mutation
|
|
20
|
+
*/
|
|
21
|
+
onSuccess?: (result?: ResultOf<any>) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Custom success message. If not provided, a default message will be used.
|
|
24
|
+
*/
|
|
25
|
+
successMessage?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Custom error message. If not provided, a default message will be used.
|
|
28
|
+
*/
|
|
29
|
+
errorMessage?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function RemoveFromChannelBulkAction({
|
|
33
|
+
selection,
|
|
34
|
+
table,
|
|
35
|
+
entityType,
|
|
36
|
+
mutationFn,
|
|
37
|
+
requiredPermissions,
|
|
38
|
+
buildInput,
|
|
39
|
+
onSuccess,
|
|
40
|
+
successMessage,
|
|
41
|
+
errorMessage,
|
|
42
|
+
}: Readonly<RemoveFromChannelBulkActionProps>) {
|
|
43
|
+
const { refetchPaginatedList } = usePaginatedList();
|
|
44
|
+
const { selectedChannel } = useChannel();
|
|
45
|
+
const { i18n } = useLingui();
|
|
46
|
+
const { mutate } = useMutation({
|
|
47
|
+
mutationFn,
|
|
48
|
+
onSuccess: result => {
|
|
49
|
+
const message =
|
|
50
|
+
successMessage ||
|
|
51
|
+
i18n.t(`Successfully removed ${selection.length} ${entityType} from channel`);
|
|
52
|
+
toast.success(message);
|
|
53
|
+
refetchPaginatedList();
|
|
54
|
+
table.resetRowSelection();
|
|
55
|
+
onSuccess?.(result);
|
|
56
|
+
},
|
|
57
|
+
onError: error => {
|
|
58
|
+
const message =
|
|
59
|
+
errorMessage ||
|
|
60
|
+
`Failed to remove ${selection.length} ${entityType} from channel: ${error.message}`;
|
|
61
|
+
toast.error(message);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!selectedChannel) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleRemove = () => {
|
|
70
|
+
mutate({
|
|
71
|
+
input: buildInput(),
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<DataTableBulkActionItem
|
|
77
|
+
requiresPermission={requiredPermissions}
|
|
78
|
+
onClick={handleRemove}
|
|
79
|
+
label={<Trans>Remove from current channel</Trans>}
|
|
80
|
+
confirmationText={
|
|
81
|
+
<Trans>
|
|
82
|
+
Are you sure you want to remove {selection.length} {entityType} from the current channel?
|
|
83
|
+
</Trans>
|
|
84
|
+
}
|
|
85
|
+
icon={LayersIcon}
|
|
86
|
+
className="text-warning"
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { onExtensionSourceChange } from '@/framework/extension-api/define-dashboard-extension.js';
|
|
2
1
|
import { useEffect, useState } from 'react';
|
|
3
2
|
import { runDashboardExtensions } from 'virtual:dashboard-extensions';
|
|
4
3
|
|
|
4
|
+
import { onExtensionSourceChange } from './define-dashboard-extension.js';
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* @description
|
|
7
8
|
* This hook is used to load dashboard extensions via the `virtual:dashboard-extensions` module,
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { getOperationVariablesFields } from '@/framework/document-introspection/get-document-structure.js';
|
|
2
|
-
import {
|
|
3
|
-
createFormSchemaFromFields,
|
|
4
|
-
getDefaultValuesFromFields,
|
|
5
|
-
} from '@/framework/form-engine/form-schema-tools.js';
|
|
6
|
-
import { transformRelationFields } from '@/framework/form-engine/utils.js';
|
|
7
|
-
import { useChannel } from '@/hooks/use-channel.js';
|
|
8
|
-
import { useServerConfig } from '@/hooks/use-server-config.js';
|
|
9
1
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
10
2
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
11
3
|
import { VariablesOf } from 'gql.tada';
|
|
12
4
|
import { FormEvent } from 'react';
|
|
13
5
|
import { useForm } from 'react-hook-form';
|
|
6
|
+
import { useChannel } from '../../hooks/use-channel.js';
|
|
7
|
+
import { useServerConfig } from '../../hooks/use-server-config.js';
|
|
8
|
+
import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
|
|
9
|
+
import { createFormSchemaFromFields, getDefaultValuesFromFields } from './form-schema-tools.js';
|
|
10
|
+
import { transformRelationFields } from './utils.js';
|
|
14
11
|
|
|
15
12
|
export interface GeneratedFormOptions<
|
|
16
13
|
T extends TypedDocumentNode<any, any>,
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { NEW_ENTITY_PATH } from '@/constants.js';
|
|
2
|
-
import { api, Variables } from '@/graphql/api.js';
|
|
3
|
-
import { useCustomFieldConfig } from '@/hooks/use-custom-field-config.js';
|
|
4
|
-
import { useExtendedDetailQuery } from '@/hooks/use-extended-detail-query.js';
|
|
5
1
|
import { removeReadonlyCustomFields } from '@/lib/utils.js';
|
|
6
2
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
7
3
|
import {
|
|
@@ -16,6 +12,10 @@ import { DocumentNode } from 'graphql';
|
|
|
16
12
|
import { FormEvent } from 'react';
|
|
17
13
|
import { UseFormReturn } from 'react-hook-form';
|
|
18
14
|
|
|
15
|
+
import { NEW_ENTITY_PATH } from '../../constants.js';
|
|
16
|
+
import { api, Variables } from '../../graphql/api.js';
|
|
17
|
+
import { useCustomFieldConfig } from '../../hooks/use-custom-field-config.js';
|
|
18
|
+
import { useExtendedDetailQuery } from '../../hooks/use-extended-detail-query.js';
|
|
19
19
|
import { addCustomFields } from '../document-introspection/add-custom-fields.js';
|
|
20
20
|
import {
|
|
21
21
|
getEntityName,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { ErrorPage } from '@/components/shared/error-page.js';
|
|
2
|
-
import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
|
|
3
|
-
import { ListPage } from '@/framework/page/list-page.js';
|
|
4
|
-
import { extensionRoutes } from '@/framework/page/page-api.js';
|
|
5
|
-
import { AUTHENTICATED_ROUTE_PREFIX } from '@/constants.js';
|
|
6
1
|
import { AnyRoute, createRoute, Router } from '@tanstack/react-router';
|
|
7
2
|
import { useMemo } from 'react';
|
|
3
|
+
import { ErrorPage } from '../../components/shared/error-page.js';
|
|
4
|
+
import { AUTHENTICATED_ROUTE_PREFIX } from '../../constants.js';
|
|
5
|
+
import { useDashboardExtensions } from '../extension-api/use-dashboard-extensions.js';
|
|
6
|
+
import { extensionRoutes } from './page-api.js';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Extends the TanStack Router with additional routes for each dashboard
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { extendDetailFormQuery } from '@/framework/document-extension/extend-detail-form-query.js';
|
|
2
1
|
import { useLingui } from '@/lib/trans.js';
|
|
3
2
|
import { DocumentNode } from 'graphql';
|
|
4
3
|
import { useEffect, useMemo, useRef } from 'react';
|
|
5
4
|
import { toast } from 'sonner';
|
|
6
5
|
|
|
6
|
+
import { extendDetailFormQuery } from '../framework/document-extension/extend-detail-form-query.js';
|
|
7
|
+
|
|
7
8
|
/**
|
|
8
9
|
* @description
|
|
9
10
|
* Extends a detail page query document with any registered extensions provided by
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { getListQueryDocuments } from '@/framework/data-table/data-table-extensions.js';
|
|
2
|
-
import { extendDocument } from '@/framework/document-extension/extend-document.js';
|
|
3
1
|
import { useLingui } from '@/lib/trans.js';
|
|
4
2
|
import { DocumentNode } from 'graphql';
|
|
5
3
|
import { useEffect, useMemo, useRef } from 'react';
|
|
6
4
|
import { toast } from 'sonner';
|
|
7
5
|
|
|
6
|
+
import { getListQueryDocuments } from '../framework/data-table/data-table-extensions.js';
|
|
7
|
+
import { extendDocument } from '../framework/document-extension/extend-document.js';
|
|
8
|
+
|
|
8
9
|
import { usePageBlock } from './use-page-block.js';
|
|
9
10
|
import { usePage } from './use-page.js';
|
|
10
11
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { useServerConfig } from '@/hooks/use-server-config.js';
|
|
2
|
-
import { ServerConfig } from '@/providers/server-config.js';
|
|
3
1
|
import { useMemo } from 'react';
|
|
4
2
|
|
|
3
|
+
import { ServerConfig } from '../providers/server-config.js';
|
|
4
|
+
|
|
5
|
+
import { useServerConfig } from './use-server-config.js';
|
|
6
|
+
|
|
5
7
|
export function useGroupedPermissions() {
|
|
6
8
|
const serverConfig = useServerConfig();
|
|
7
9
|
const permissionDefinitions = serverConfig?.permissions ?? [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PageBlockContext } from '@/framework/layout-engine/page-block-provider.js';
|
|
2
1
|
import { useContext } from 'react';
|
|
2
|
+
import { PageBlockContext } from '../framework/layout-engine/page-block-provider.js';
|
|
3
3
|
|
|
4
4
|
export function usePageBlock() {
|
|
5
5
|
const pageBlock = useContext(PageBlockContext);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useContext } from
|
|
2
|
-
import { PageContext } from '
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { PageContext } from '../framework/layout-engine/page-provider.js';
|
|
3
3
|
|
|
4
4
|
export function usePage() {
|
|
5
5
|
const page = useContext(PageContext);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useAuth } from '@/hooks/use-auth.js';
|
|
2
|
-
import { useChannel } from '@/hooks/use-channel.js';
|
|
3
1
|
import { Permission } from '@vendure/common/lib/generated-types';
|
|
4
2
|
|
|
3
|
+
import { useAuth } from './use-auth.js';
|
|
4
|
+
import { useChannel } from './use-channel.js';
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* @description
|
|
7
8
|
* **Status: Developer Preview**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ThemeProviderContext } from '@/providers/theme-provider.js';
|
|
2
1
|
import { useContext } from 'react';
|
|
3
2
|
|
|
3
|
+
import { ThemeProviderContext } from '../providers/theme-provider.js';
|
|
4
|
+
|
|
4
5
|
export const useTheme = () => {
|
|
5
6
|
const context = useContext(ThemeProviderContext);
|
|
6
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '@/graphql/api.js';
|
|
2
|
-
import {
|
|
2
|
+
import { graphql, ResultOf } from '@/graphql/graphql.js';
|
|
3
3
|
import { useUserSettings } from '@/hooks/use-user-settings.js';
|
|
4
4
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
5
5
|
import * as React from 'react';
|
|
@@ -14,7 +14,7 @@ import * as React from 'react';
|
|
|
14
14
|
* @since 3.3.0
|
|
15
15
|
*/
|
|
16
16
|
export interface AuthContext {
|
|
17
|
-
status: 'authenticated' | 'verifying' | 'unauthenticated';
|
|
17
|
+
status: 'initial' | 'authenticated' | 'verifying' | 'unauthenticated';
|
|
18
18
|
authenticationError?: string;
|
|
19
19
|
isAuthenticated: boolean;
|
|
20
20
|
login: (username: string, password: string, onSuccess?: () => void) => void;
|
|
@@ -71,8 +71,9 @@ const CurrentUserQuery = graphql(`
|
|
|
71
71
|
export const AuthContext = React.createContext<AuthContext | null>(null);
|
|
72
72
|
|
|
73
73
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
74
|
-
const [status, setStatus] = React.useState<AuthContext['status']>('
|
|
74
|
+
const [status, setStatus] = React.useState<AuthContext['status']>('initial');
|
|
75
75
|
const [authenticationError, setAuthenticationError] = React.useState<string | undefined>();
|
|
76
|
+
const [isLoginLogoutInProgress, setIsLoginLogoutInProgress] = React.useState(false);
|
|
76
77
|
const { settings, setActiveChannelId } = useUserSettings();
|
|
77
78
|
const queryClient = useQueryClient();
|
|
78
79
|
|
|
@@ -84,7 +85,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
84
85
|
refetch: refetchCurrentUser,
|
|
85
86
|
} = useQuery({
|
|
86
87
|
queryKey: ['currentUser'],
|
|
87
|
-
queryFn: () =>
|
|
88
|
+
queryFn: () => {
|
|
89
|
+
return api.query(CurrentUserQuery);
|
|
90
|
+
},
|
|
91
|
+
retry: false, // Disable retries to avoid waiting for multiple attempts
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
// Set active channel if needed
|
|
@@ -97,6 +101,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
97
101
|
// Auth actions
|
|
98
102
|
const login = React.useCallback(
|
|
99
103
|
(username: string, password: string, onLoginSuccess?: () => void) => {
|
|
104
|
+
setIsLoginLogoutInProgress(true);
|
|
100
105
|
setStatus('verifying');
|
|
101
106
|
api.mutate(LoginMutation)({ username, password })
|
|
102
107
|
.then(async data => {
|
|
@@ -106,15 +111,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
106
111
|
// Invalidate all queries to ensure fresh data after login
|
|
107
112
|
await queryClient.invalidateQueries();
|
|
108
113
|
setStatus('authenticated');
|
|
114
|
+
setIsLoginLogoutInProgress(false);
|
|
109
115
|
onLoginSuccess?.();
|
|
110
116
|
} else {
|
|
111
117
|
setAuthenticationError(data?.login.message);
|
|
112
118
|
setStatus('unauthenticated');
|
|
119
|
+
setIsLoginLogoutInProgress(false);
|
|
113
120
|
}
|
|
114
121
|
})
|
|
115
122
|
.catch(error => {
|
|
116
123
|
setAuthenticationError(error.message);
|
|
117
124
|
setStatus('unauthenticated');
|
|
125
|
+
setIsLoginLogoutInProgress(false);
|
|
118
126
|
});
|
|
119
127
|
},
|
|
120
128
|
[refetchCurrentUser, queryClient],
|
|
@@ -122,6 +130,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
122
130
|
|
|
123
131
|
const logout = React.useCallback(
|
|
124
132
|
async (onLogoutSuccess?: () => void) => {
|
|
133
|
+
setIsLoginLogoutInProgress(true);
|
|
125
134
|
setStatus('verifying');
|
|
126
135
|
api.mutate(LogOutMutation)({}).then(async data => {
|
|
127
136
|
if (data?.logout.success) {
|
|
@@ -131,6 +140,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
131
140
|
localStorage.removeItem('vendure-selected-channel');
|
|
132
141
|
localStorage.removeItem('vendure-selected-channel-token');
|
|
133
142
|
setStatus('unauthenticated');
|
|
143
|
+
setIsLoginLogoutInProgress(false);
|
|
134
144
|
onLogoutSuccess?.();
|
|
135
145
|
}
|
|
136
146
|
});
|
|
@@ -141,15 +151,28 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
141
151
|
// Determine isAuthenticated from currentUserData
|
|
142
152
|
const isAuthenticated = !!currentUserData?.me?.id;
|
|
143
153
|
|
|
144
|
-
//
|
|
154
|
+
// Handle status transitions based on query state
|
|
145
155
|
React.useEffect(() => {
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
156
|
+
// Don't change status if we're in the middle of login/logout
|
|
157
|
+
if (isLoginLogoutInProgress) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If query is loading and we haven't started verifying yet, set to verifying
|
|
162
|
+
if (isLoading && status === 'initial') {
|
|
163
|
+
setStatus('verifying');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If query has completed (not loading) and we're in verifying state, determine final status
|
|
168
|
+
if (!isLoading && status === 'verifying') {
|
|
169
|
+
if (currentUserError || !currentUserData?.me?.id) {
|
|
170
|
+
setStatus('unauthenticated');
|
|
171
|
+
} else {
|
|
172
|
+
setStatus('authenticated');
|
|
173
|
+
}
|
|
151
174
|
}
|
|
152
|
-
}, [currentUserData, currentUserError]);
|
|
175
|
+
}, [isLoading, currentUserData, currentUserError, status, isLoginLogoutInProgress]);
|
|
153
176
|
|
|
154
177
|
return (
|
|
155
178
|
<AuthContext.Provider
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { useMutation } from '@tanstack/react-query';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { toast } from 'sonner';
|
|
4
|
-
|
|
5
|
-
import { ChannelCodeLabel } from '@/components/shared/channel-code-label.js';
|
|
6
|
-
import { Button } from '@/components/ui/button.js';
|
|
7
|
-
import {
|
|
8
|
-
Dialog,
|
|
9
|
-
DialogContent,
|
|
10
|
-
DialogDescription,
|
|
11
|
-
DialogFooter,
|
|
12
|
-
DialogHeader,
|
|
13
|
-
DialogTitle,
|
|
14
|
-
} from '@/components/ui/dialog.js';
|
|
15
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
|
|
16
|
-
import { ResultOf } from '@/graphql/graphql.js';
|
|
17
|
-
import { Trans, useLingui } from '@/lib/trans.js';
|
|
18
|
-
|
|
19
|
-
import { useChannel } from '@/hooks/use-channel.js';
|
|
20
|
-
|
|
21
|
-
interface AssignCollectionsToChannelDialogProps {
|
|
22
|
-
open: boolean;
|
|
23
|
-
onOpenChange: (open: boolean) => void;
|
|
24
|
-
entityIds: string[];
|
|
25
|
-
mutationFn: (variables: any) => Promise<ResultOf<any>>;
|
|
26
|
-
onSuccess?: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function AssignCollectionsToChannelDialog({
|
|
30
|
-
open,
|
|
31
|
-
onOpenChange,
|
|
32
|
-
entityIds,
|
|
33
|
-
mutationFn,
|
|
34
|
-
onSuccess,
|
|
35
|
-
}: AssignCollectionsToChannelDialogProps) {
|
|
36
|
-
const { i18n } = useLingui();
|
|
37
|
-
const [selectedChannelId, setSelectedChannelId] = useState<string>('');
|
|
38
|
-
const { channels, selectedChannel } = useChannel();
|
|
39
|
-
|
|
40
|
-
// Filter out the currently selected channel from available options
|
|
41
|
-
const availableChannels = channels.filter(channel => channel.id !== selectedChannel?.id);
|
|
42
|
-
|
|
43
|
-
const { mutate, isPending } = useMutation({
|
|
44
|
-
mutationFn,
|
|
45
|
-
onSuccess: () => {
|
|
46
|
-
toast.success(i18n.t(`Successfully assigned ${entityIds.length} collections to channel`));
|
|
47
|
-
onSuccess?.();
|
|
48
|
-
onOpenChange(false);
|
|
49
|
-
},
|
|
50
|
-
onError: () => {
|
|
51
|
-
toast.error(`Failed to assign ${entityIds.length} collections to channel`);
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const handleAssign = () => {
|
|
56
|
-
if (!selectedChannelId) {
|
|
57
|
-
toast.error('Please select a channel');
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const input = {
|
|
62
|
-
collectionIds: entityIds,
|
|
63
|
-
channelId: selectedChannelId,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
mutate({ input });
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
71
|
-
<DialogContent className="sm:max-w-[425px]">
|
|
72
|
-
<DialogHeader>
|
|
73
|
-
<DialogTitle>
|
|
74
|
-
<Trans>Assign collections to channel</Trans>
|
|
75
|
-
</DialogTitle>
|
|
76
|
-
<DialogDescription>
|
|
77
|
-
<Trans>Select a channel to assign {entityIds.length} collections to</Trans>
|
|
78
|
-
</DialogDescription>
|
|
79
|
-
</DialogHeader>
|
|
80
|
-
<div className="grid gap-4 py-4">
|
|
81
|
-
<div className="grid gap-2">
|
|
82
|
-
<label className="text-sm font-medium">
|
|
83
|
-
<Trans>Channel</Trans>
|
|
84
|
-
</label>
|
|
85
|
-
<Select value={selectedChannelId} onValueChange={setSelectedChannelId}>
|
|
86
|
-
<SelectTrigger>
|
|
87
|
-
<SelectValue placeholder={i18n.t('Select a channel')} />
|
|
88
|
-
</SelectTrigger>
|
|
89
|
-
<SelectContent>
|
|
90
|
-
{availableChannels.map(channel => (
|
|
91
|
-
<SelectItem key={channel.id} value={channel.id}>
|
|
92
|
-
<ChannelCodeLabel code={channel.code} />
|
|
93
|
-
</SelectItem>
|
|
94
|
-
))}
|
|
95
|
-
</SelectContent>
|
|
96
|
-
</Select>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
<DialogFooter>
|
|
100
|
-
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
101
|
-
<Trans>Cancel</Trans>
|
|
102
|
-
</Button>
|
|
103
|
-
<Button onClick={handleAssign} disabled={!selectedChannelId || isPending}>
|
|
104
|
-
<Trans>Assign</Trans>
|
|
105
|
-
</Button>
|
|
106
|
-
</DialogFooter>
|
|
107
|
-
</DialogContent>
|
|
108
|
-
</Dialog>
|
|
109
|
-
);
|
|
110
|
-
}
|