@vendure/dashboard 3.3.8-master-202507300243 → 3.3.8-master-202508010249
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/_global-settings/global-settings.tsx +16 -8
- package/src/app/routes/_authenticated/_system/components/payload-dialog.tsx +1 -1
- package/src/app/routes/_authenticated/_system/job-queue.graphql.ts +11 -0
- package/src/app/routes/_authenticated/_system/job-queue.tsx +99 -5
- package/src/lib/components/layout/manage-languages-dialog.tsx +368 -0
- package/src/lib/components/shared/custom-fields-form.tsx +3 -3
- package/src/lib/framework/page/list-page.tsx +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.3.8-master-
|
|
4
|
+
"version": "3.3.8-master-202508010249",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
"@types/react-dom": "^19.0.4",
|
|
96
96
|
"@types/react-grid-layout": "^1.3.5",
|
|
97
97
|
"@uidotdev/usehooks": "^2.4.1",
|
|
98
|
-
"@vendure/common": "^3.3.8-master-
|
|
99
|
-
"@vendure/core": "^3.3.8-master-
|
|
98
|
+
"@vendure/common": "^3.3.8-master-202508010249",
|
|
99
|
+
"@vendure/core": "^3.3.8-master-202508010249",
|
|
100
100
|
"@vitejs/plugin-react": "^4.3.4",
|
|
101
101
|
"acorn": "^8.11.3",
|
|
102
102
|
"acorn-walk": "^8.3.2",
|
|
@@ -146,5 +146,5 @@
|
|
|
146
146
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
147
147
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
148
148
|
},
|
|
149
|
-
"gitHead": "
|
|
149
|
+
"gitHead": "9d2ad2874a19007af173d45bd1c00d09906ac2c1"
|
|
150
150
|
}
|
|
@@ -6,6 +6,8 @@ import { Button } from '@/vdb/components/ui/button.js';
|
|
|
6
6
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
7
7
|
import { Switch } from '@/vdb/components/ui/switch.js';
|
|
8
8
|
import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
|
|
9
|
+
import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
|
|
10
|
+
import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
|
|
9
11
|
import {
|
|
10
12
|
CustomFieldsPageBlock,
|
|
11
13
|
DetailFormGrid,
|
|
@@ -16,20 +18,25 @@ import {
|
|
|
16
18
|
PageLayout,
|
|
17
19
|
PageTitle,
|
|
18
20
|
} from '@/vdb/framework/layout-engine/page-layout.js';
|
|
19
|
-
import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
20
|
-
import { api } from '@/vdb/graphql/api.js';
|
|
21
|
+
import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
21
22
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
22
23
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
23
24
|
import { toast } from 'sonner';
|
|
24
25
|
import { globalSettingsDocument, updateGlobalSettingsDocument } from './global-settings.graphql.js';
|
|
25
26
|
|
|
27
|
+
const pageId = 'global-settings';
|
|
28
|
+
|
|
26
29
|
export const Route = createFileRoute('/_authenticated/_global-settings/global-settings')({
|
|
27
30
|
component: GlobalSettingsPage,
|
|
28
31
|
loader: async ({ context }) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const { extendedQuery: extendedQueryDocument } = extendDetailFormQuery(
|
|
33
|
+
addCustomFields(globalSettingsDocument),
|
|
34
|
+
pageId,
|
|
35
|
+
);
|
|
36
|
+
await context.queryClient.ensureQueryData(
|
|
37
|
+
getDetailQueryOptions(extendedQueryDocument, { id: '' }),
|
|
38
|
+
{},
|
|
39
|
+
);
|
|
33
40
|
return {
|
|
34
41
|
breadcrumb: [{ path: '/global-settings', label: <Trans>Global settings</Trans> }],
|
|
35
42
|
};
|
|
@@ -45,8 +52,9 @@ function GlobalSettingsPage() {
|
|
|
45
52
|
|
|
46
53
|
const { form, submitHandler, entity, isPending } = useDetailPage({
|
|
47
54
|
queryDocument: globalSettingsDocument,
|
|
48
|
-
|
|
55
|
+
entityName: 'GlobalSettings',
|
|
49
56
|
updateDocument: updateGlobalSettingsDocument,
|
|
57
|
+
pageId,
|
|
50
58
|
setValuesForUpdate: entity => {
|
|
51
59
|
return {
|
|
52
60
|
id: entity.id,
|
|
@@ -78,7 +86,7 @@ function GlobalSettingsPage() {
|
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
return (
|
|
81
|
-
<Page pageId=
|
|
89
|
+
<Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
|
|
82
90
|
<PageTitle>
|
|
83
91
|
<Trans>Global settings</Trans>
|
|
84
92
|
</PageTitle>
|
|
@@ -26,7 +26,7 @@ export function PayloadDialog({ payload, trigger, title, description }: Readonly
|
|
|
26
26
|
<DialogDescription>{description}</DialogDescription>
|
|
27
27
|
</DialogHeader>
|
|
28
28
|
<ScrollArea className="max-h-[600px]">
|
|
29
|
-
<JsonEditor viewOnly data={payload} collapse />
|
|
29
|
+
<JsonEditor viewOnly data={payload} collapse={1} rootFontSize={12} />
|
|
30
30
|
</ScrollArea>
|
|
31
31
|
</DialogContent>
|
|
32
32
|
</Dialog>
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
2
2
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
9
|
+
import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
|
|
3
10
|
import { ListPage } from '@/vdb/framework/page/list-page.js';
|
|
4
11
|
import { api } from '@/vdb/graphql/api.js';
|
|
5
12
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
13
|
+
import { useMutation } from '@tanstack/react-query';
|
|
6
14
|
import { createFileRoute } from '@tanstack/react-router';
|
|
7
15
|
import { formatRelative } from 'date-fns';
|
|
8
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Ban,
|
|
18
|
+
CheckCircle2Icon,
|
|
19
|
+
ChevronDown,
|
|
20
|
+
CircleXIcon,
|
|
21
|
+
ClockIcon,
|
|
22
|
+
LoaderIcon,
|
|
23
|
+
MoreVertical,
|
|
24
|
+
RefreshCw,
|
|
25
|
+
RotateCcw,
|
|
26
|
+
} from 'lucide-react';
|
|
27
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
28
|
import { PayloadDialog } from './components/payload-dialog.js';
|
|
10
|
-
import { jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
|
|
29
|
+
import { cancelJobDocument, jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
|
|
11
30
|
|
|
12
31
|
export const Route = createFileRoute('/_authenticated/_system/job-queue')({
|
|
13
32
|
component: JobQueuePage,
|
|
@@ -47,7 +66,30 @@ const STATES = [
|
|
|
47
66
|
},
|
|
48
67
|
];
|
|
49
68
|
|
|
69
|
+
const REFRESH_INTERVALS = [
|
|
70
|
+
{ label: <Trans>Off</Trans>, value: 0 },
|
|
71
|
+
{ label: <Trans>Every 5 seconds</Trans>, value: 5000 },
|
|
72
|
+
{ label: <Trans>Every 10 seconds</Trans>, value: 10000 },
|
|
73
|
+
{ label: <Trans>Every 30 seconds</Trans>, value: 30000 },
|
|
74
|
+
{ label: <Trans>Every 60 seconds</Trans>, value: 60000 },
|
|
75
|
+
];
|
|
76
|
+
|
|
50
77
|
function JobQueuePage() {
|
|
78
|
+
const refreshRef = useRef<() => void>(() => {});
|
|
79
|
+
const [refreshInterval, setRefreshInterval] = useState(10000);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (refreshInterval === 0) return;
|
|
83
|
+
|
|
84
|
+
const interval = setInterval(() => {
|
|
85
|
+
refreshRef.current();
|
|
86
|
+
}, refreshInterval);
|
|
87
|
+
|
|
88
|
+
return () => clearInterval(interval);
|
|
89
|
+
}, [refreshInterval]);
|
|
90
|
+
|
|
91
|
+
const currentInterval = REFRESH_INTERVALS.find(i => i.value === refreshInterval);
|
|
92
|
+
|
|
51
93
|
return (
|
|
52
94
|
<ListPage
|
|
53
95
|
pageId="job-queue-list"
|
|
@@ -105,9 +147,14 @@ function JobQueuePage() {
|
|
|
105
147
|
},
|
|
106
148
|
state: {
|
|
107
149
|
header: 'State',
|
|
108
|
-
cell: ({ row }) => {
|
|
150
|
+
cell: ({ row, table }) => {
|
|
151
|
+
const cancelJobMutation = useMutation({
|
|
152
|
+
mutationFn: (jobId: string) => api.mutate(cancelJobDocument, { jobId }),
|
|
153
|
+
onSuccess: () => {
|
|
154
|
+
refreshRef.current();
|
|
155
|
+
},
|
|
156
|
+
});
|
|
109
157
|
const state = STATES.find(s => s.value === row.original.state);
|
|
110
|
-
|
|
111
158
|
return (
|
|
112
159
|
<Badge
|
|
113
160
|
variant={
|
|
@@ -122,6 +169,27 @@ function JobQueuePage() {
|
|
|
122
169
|
>
|
|
123
170
|
{state && <state.icon />}
|
|
124
171
|
{row.original.state}
|
|
172
|
+
{row.original.state === 'RUNNING' ? (
|
|
173
|
+
<div className="flex items-center gap-2">
|
|
174
|
+
<DropdownMenu>
|
|
175
|
+
<DropdownMenuTrigger asChild>
|
|
176
|
+
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
|
177
|
+
<MoreVertical className="h-4 w-4" />
|
|
178
|
+
</Button>
|
|
179
|
+
</DropdownMenuTrigger>
|
|
180
|
+
<DropdownMenuContent align="end">
|
|
181
|
+
<DropdownMenuItem
|
|
182
|
+
onClick={() => cancelJobMutation.mutate(row.original.id)}
|
|
183
|
+
disabled={cancelJobMutation.isPending}
|
|
184
|
+
className="text-destructive focus:text-destructive"
|
|
185
|
+
>
|
|
186
|
+
<Ban className="mr-2 h-4 w-4" />
|
|
187
|
+
<Trans>Cancel Job</Trans>
|
|
188
|
+
</DropdownMenuItem>
|
|
189
|
+
</DropdownMenuContent>
|
|
190
|
+
</DropdownMenu>
|
|
191
|
+
</div>
|
|
192
|
+
) : null}
|
|
125
193
|
</Badge>
|
|
126
194
|
);
|
|
127
195
|
},
|
|
@@ -159,6 +227,32 @@ function JobQueuePage() {
|
|
|
159
227
|
options: STATES,
|
|
160
228
|
},
|
|
161
229
|
}}
|
|
162
|
-
|
|
230
|
+
registerRefresher={refresher => {
|
|
231
|
+
refreshRef.current = refresher;
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<PageActionBarRight>
|
|
235
|
+
<DropdownMenu>
|
|
236
|
+
<DropdownMenuTrigger asChild>
|
|
237
|
+
<Button variant="outline" size="sm" className="gap-2">
|
|
238
|
+
<RefreshCw className="h-4 w-4" />
|
|
239
|
+
<span>Auto refresh: {currentInterval?.label}</span>
|
|
240
|
+
<ChevronDown className="h-4 w-4" />
|
|
241
|
+
</Button>
|
|
242
|
+
</DropdownMenuTrigger>
|
|
243
|
+
<DropdownMenuContent align="end">
|
|
244
|
+
{REFRESH_INTERVALS.map(interval => (
|
|
245
|
+
<DropdownMenuItem
|
|
246
|
+
key={interval.value}
|
|
247
|
+
onClick={() => setRefreshInterval(interval.value)}
|
|
248
|
+
className={refreshInterval === interval.value ? 'bg-accent' : ''}
|
|
249
|
+
>
|
|
250
|
+
{interval.label}
|
|
251
|
+
</DropdownMenuItem>
|
|
252
|
+
))}
|
|
253
|
+
</DropdownMenuContent>
|
|
254
|
+
</DropdownMenu>
|
|
255
|
+
</PageActionBarRight>
|
|
256
|
+
</ListPage>
|
|
163
257
|
);
|
|
164
258
|
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { LanguageSelector } from '@/vdb/components/shared/language-selector.js';
|
|
2
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from '@/vdb/components/ui/dialog.js';
|
|
11
|
+
import { Label } from '@/vdb/components/ui/label.js';
|
|
12
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
|
|
13
|
+
import { Separator } from '@/vdb/components/ui/separator.js';
|
|
14
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
15
|
+
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
16
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
17
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
18
|
+
import { usePermissions } from '@/vdb/hooks/use-permissions.js';
|
|
19
|
+
import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
|
|
20
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
21
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
22
|
+
import { AlertCircle, Lock } from 'lucide-react';
|
|
23
|
+
import { useEffect, useState } from 'react';
|
|
24
|
+
import { toast } from 'sonner';
|
|
25
|
+
|
|
26
|
+
// GraphQL queries
|
|
27
|
+
const globalSettingsLanguagesDocument = graphql(`
|
|
28
|
+
query GlobalSettingsLanguages {
|
|
29
|
+
globalSettings {
|
|
30
|
+
id
|
|
31
|
+
availableLanguages
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
const updateGlobalSettingsLanguagesDocument = graphql(`
|
|
37
|
+
mutation UpdateGlobalSettingsLanguages($input: UpdateGlobalSettingsInput!) {
|
|
38
|
+
updateGlobalSettings(input: $input) {
|
|
39
|
+
__typename
|
|
40
|
+
... on GlobalSettings {
|
|
41
|
+
id
|
|
42
|
+
availableLanguages
|
|
43
|
+
}
|
|
44
|
+
... on ErrorResult {
|
|
45
|
+
errorCode
|
|
46
|
+
message
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
const updateChannelDocument = graphql(`
|
|
53
|
+
mutation UpdateChannelLanguages($input: UpdateChannelInput!) {
|
|
54
|
+
updateChannel(input: $input) {
|
|
55
|
+
__typename
|
|
56
|
+
... on Channel {
|
|
57
|
+
id
|
|
58
|
+
code
|
|
59
|
+
defaultLanguageCode
|
|
60
|
+
availableLanguageCodes
|
|
61
|
+
}
|
|
62
|
+
... on ErrorResult {
|
|
63
|
+
errorCode
|
|
64
|
+
message
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
// All possible language codes for global settings - includes more than what might be globally enabled
|
|
71
|
+
const ALL_LANGUAGE_CODES = [
|
|
72
|
+
'en', 'es', 'fr', 'de', 'it', 'pt', 'nl', 'pl', 'ru', 'ja', 'zh', 'ko', 'ar', 'hi', 'sv', 'da', 'no', 'fi'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
interface ManageLanguagesDialogProps {
|
|
76
|
+
open: boolean;
|
|
77
|
+
onClose: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogProps) {
|
|
81
|
+
const { formatLanguageName } = useLocalFormat();
|
|
82
|
+
const { activeChannel, selectedChannel } = useChannel();
|
|
83
|
+
const { hasPermissions } = usePermissions();
|
|
84
|
+
const queryClient = useQueryClient();
|
|
85
|
+
|
|
86
|
+
const displayChannel = selectedChannel || activeChannel;
|
|
87
|
+
|
|
88
|
+
// Permission checks
|
|
89
|
+
const canReadGlobalSettings = hasPermissions(['ReadSettings']) || hasPermissions(['ReadGlobalSettings']);
|
|
90
|
+
const canUpdateGlobalSettings = hasPermissions(['UpdateSettings']) || hasPermissions(['UpdateGlobalSettings']);
|
|
91
|
+
const canReadChannel = hasPermissions(['ReadChannel']);
|
|
92
|
+
const canUpdateChannel = hasPermissions(['UpdateChannel']);
|
|
93
|
+
|
|
94
|
+
// State for managing changes
|
|
95
|
+
const [globalLanguages, setGlobalLanguages] = useState<string[]>([]);
|
|
96
|
+
const [channelLanguages, setChannelLanguages] = useState<string[]>([]);
|
|
97
|
+
const [channelDefaultLanguage, setChannelDefaultLanguage] = useState<string>('');
|
|
98
|
+
|
|
99
|
+
// Queries
|
|
100
|
+
const {
|
|
101
|
+
data: globalSettingsData,
|
|
102
|
+
isLoading: globalSettingsLoading,
|
|
103
|
+
error: globalSettingsError
|
|
104
|
+
} = useQuery({
|
|
105
|
+
queryKey: ['globalSettings', 'languages'],
|
|
106
|
+
queryFn: () => api.query(globalSettingsLanguagesDocument),
|
|
107
|
+
enabled: open && canReadGlobalSettings,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Mutations
|
|
111
|
+
const updateGlobalSettingsMutation = useMutation({
|
|
112
|
+
mutationFn: (input: { availableLanguages: string[] }) =>
|
|
113
|
+
api.mutate(updateGlobalSettingsLanguagesDocument, { input }),
|
|
114
|
+
onSuccess: () => {
|
|
115
|
+
queryClient.invalidateQueries({ queryKey: ['globalSettings'] });
|
|
116
|
+
toast.success('Global language settings updated successfully');
|
|
117
|
+
},
|
|
118
|
+
onError: (error: any) => {
|
|
119
|
+
toast.error(`Failed to update global settings: ${error.message}`);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const updateChannelMutation = useMutation({
|
|
124
|
+
mutationFn: (input: { id: string; availableLanguageCodes?: string[]; defaultLanguageCode?: string }) =>
|
|
125
|
+
api.mutate(updateChannelDocument, { input }),
|
|
126
|
+
onSuccess: () => {
|
|
127
|
+
queryClient.invalidateQueries({ queryKey: ['channels'] });
|
|
128
|
+
toast.success('Channel language settings updated successfully');
|
|
129
|
+
},
|
|
130
|
+
onError: (error: any) => {
|
|
131
|
+
toast.error(`Failed to update channel settings: ${error.message}`);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Initialize state when dialog opens
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (open && globalSettingsData) {
|
|
138
|
+
setGlobalLanguages(globalSettingsData.globalSettings.availableLanguages || []);
|
|
139
|
+
}
|
|
140
|
+
if (open && displayChannel) {
|
|
141
|
+
setChannelLanguages(displayChannel.availableLanguageCodes || []);
|
|
142
|
+
setChannelDefaultLanguage(displayChannel.defaultLanguageCode || '');
|
|
143
|
+
}
|
|
144
|
+
}, [open, globalSettingsData, displayChannel]);
|
|
145
|
+
|
|
146
|
+
const handleGlobalLanguagesChange = (newLanguages: string[]) => {
|
|
147
|
+
setGlobalLanguages(newLanguages);
|
|
148
|
+
|
|
149
|
+
// Remove channel languages that are no longer in global languages
|
|
150
|
+
const updatedChannelLanguages = channelLanguages.filter(lang => newLanguages.includes(lang));
|
|
151
|
+
setChannelLanguages(updatedChannelLanguages);
|
|
152
|
+
|
|
153
|
+
// If the default language is no longer available, reset it
|
|
154
|
+
if (!newLanguages.includes(channelDefaultLanguage)) {
|
|
155
|
+
setChannelDefaultLanguage(updatedChannelLanguages[0] || '');
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleChannelLanguagesChange = (newLanguages: string[]) => {
|
|
160
|
+
setChannelLanguages(newLanguages);
|
|
161
|
+
|
|
162
|
+
// If the default language is no longer available, reset it
|
|
163
|
+
if (!newLanguages.includes(channelDefaultLanguage)) {
|
|
164
|
+
setChannelDefaultLanguage(newLanguages[0] || '');
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleSave = async () => {
|
|
169
|
+
const promises = [];
|
|
170
|
+
|
|
171
|
+
// Update global settings if changed and permissions allow
|
|
172
|
+
if (canUpdateGlobalSettings && globalSettingsData) {
|
|
173
|
+
const currentGlobalLanguages = globalSettingsData.globalSettings.availableLanguages || [];
|
|
174
|
+
if (JSON.stringify(currentGlobalLanguages.sort()) !== JSON.stringify(globalLanguages.sort())) {
|
|
175
|
+
promises.push(updateGlobalSettingsMutation.mutateAsync({ availableLanguages: globalLanguages }));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Update channel settings if changed and permissions allow
|
|
180
|
+
if (canUpdateChannel && displayChannel) {
|
|
181
|
+
const currentChannelLanguages = displayChannel.availableLanguageCodes || [];
|
|
182
|
+
const currentChannelDefault = displayChannel.defaultLanguageCode || '';
|
|
183
|
+
|
|
184
|
+
const languagesChanged = JSON.stringify(currentChannelLanguages.sort()) !== JSON.stringify(channelLanguages.sort());
|
|
185
|
+
const defaultChanged = currentChannelDefault !== channelDefaultLanguage;
|
|
186
|
+
|
|
187
|
+
if (languagesChanged || defaultChanged) {
|
|
188
|
+
promises.push(updateChannelMutation.mutateAsync({
|
|
189
|
+
id: displayChannel.id,
|
|
190
|
+
availableLanguageCodes: channelLanguages,
|
|
191
|
+
defaultLanguageCode: channelDefaultLanguage,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await Promise.all(promises);
|
|
198
|
+
onClose();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// Error handling is done in mutation callbacks
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const hasChanges = () => {
|
|
205
|
+
if (globalSettingsData && canUpdateGlobalSettings) {
|
|
206
|
+
const currentGlobal = globalSettingsData.globalSettings.availableLanguages || [];
|
|
207
|
+
if (JSON.stringify(currentGlobal.sort()) !== JSON.stringify(globalLanguages.sort())) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (displayChannel && canUpdateChannel) {
|
|
213
|
+
const currentChannelLangs = displayChannel.availableLanguageCodes || [];
|
|
214
|
+
const currentChannelDefault = displayChannel.defaultLanguageCode || '';
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
JSON.stringify(currentChannelLangs.sort()) !== JSON.stringify(channelLanguages.sort()) ||
|
|
218
|
+
currentChannelDefault !== channelDefaultLanguage
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return false;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const isLoading = updateGlobalSettingsMutation.isPending || updateChannelMutation.isPending;
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Dialog open={open} onOpenChange={onClose}>
|
|
229
|
+
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
230
|
+
<DialogHeader>
|
|
231
|
+
<DialogTitle><Trans>Manage Languages</Trans></DialogTitle>
|
|
232
|
+
<DialogDescription>
|
|
233
|
+
<Trans>Configure available languages for your store and channels</Trans>
|
|
234
|
+
</DialogDescription>
|
|
235
|
+
</DialogHeader>
|
|
236
|
+
|
|
237
|
+
<div className="space-y-6">
|
|
238
|
+
{/* Global Settings Section */}
|
|
239
|
+
<div>
|
|
240
|
+
<div className="flex items-center gap-2 mb-3">
|
|
241
|
+
<h3 className="text-lg font-semibold"><Trans>Global Languages</Trans></h3>
|
|
242
|
+
{!canReadGlobalSettings && <Lock className="h-4 w-4 text-muted-foreground" />}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{!canReadGlobalSettings ? (
|
|
246
|
+
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
|
|
247
|
+
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
|
248
|
+
<span className="text-sm text-muted-foreground">
|
|
249
|
+
<Trans>You don't have permission to view global language settings</Trans>
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
) : globalSettingsLoading ? (
|
|
253
|
+
<div className="text-sm text-muted-foreground">
|
|
254
|
+
<Trans>Loading global settings...</Trans>
|
|
255
|
+
</div>
|
|
256
|
+
) : globalSettingsError ? (
|
|
257
|
+
<div className="flex items-center gap-2 p-3 bg-destructive/10 rounded-md">
|
|
258
|
+
<AlertCircle className="h-4 w-4 text-destructive" />
|
|
259
|
+
<span className="text-sm text-destructive">
|
|
260
|
+
<Trans>Failed to load global settings</Trans>
|
|
261
|
+
</span>
|
|
262
|
+
</div>
|
|
263
|
+
) : (
|
|
264
|
+
<div className="space-y-2">
|
|
265
|
+
<Label className="text-sm font-medium">
|
|
266
|
+
<Trans>Select Available Languages</Trans>
|
|
267
|
+
</Label>
|
|
268
|
+
<div className={!canUpdateGlobalSettings ? 'pointer-events-none opacity-50' : ''}>
|
|
269
|
+
<LanguageSelector
|
|
270
|
+
value={globalLanguages}
|
|
271
|
+
onChange={handleGlobalLanguagesChange}
|
|
272
|
+
multiple={true}
|
|
273
|
+
availableLanguageCodes={ALL_LANGUAGE_CODES}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
<p className="text-xs text-muted-foreground">
|
|
277
|
+
<Trans>These languages will be available for all channels to use</Trans>
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<Separator />
|
|
284
|
+
|
|
285
|
+
{/* Channel Settings Section */}
|
|
286
|
+
<div>
|
|
287
|
+
<div className="flex items-center gap-2 mb-3">
|
|
288
|
+
<h3 className="text-lg font-semibold">
|
|
289
|
+
<Trans>Channel Languages</Trans> - <ChannelCodeLabel code={displayChannel?.code} />
|
|
290
|
+
</h3>
|
|
291
|
+
{!canReadChannel && <Lock className="h-4 w-4 text-muted-foreground" />}
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{!canReadChannel ? (
|
|
295
|
+
<div className="flex items-center gap-2 p-3 bg-muted rounded-md">
|
|
296
|
+
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
|
297
|
+
<span className="text-sm text-muted-foreground">
|
|
298
|
+
<Trans>You don't have permission to view channel settings</Trans>
|
|
299
|
+
</span>
|
|
300
|
+
</div>
|
|
301
|
+
) : (
|
|
302
|
+
<div className="space-y-4">
|
|
303
|
+
<div className="space-y-2">
|
|
304
|
+
<Label className="text-sm font-medium">
|
|
305
|
+
<Trans>Available Languages</Trans>
|
|
306
|
+
</Label>
|
|
307
|
+
<div className={!canUpdateChannel ? 'pointer-events-none opacity-50' : ''}>
|
|
308
|
+
<LanguageSelector
|
|
309
|
+
value={channelLanguages}
|
|
310
|
+
onChange={handleChannelLanguagesChange}
|
|
311
|
+
multiple={true}
|
|
312
|
+
availableLanguageCodes={globalLanguages}
|
|
313
|
+
/>
|
|
314
|
+
</div>
|
|
315
|
+
{globalLanguages.length === 0 ? (
|
|
316
|
+
<p className="text-xs text-muted-foreground">
|
|
317
|
+
<Trans>No global languages configured</Trans>
|
|
318
|
+
</p>
|
|
319
|
+
) : (
|
|
320
|
+
<p className="text-xs text-muted-foreground">
|
|
321
|
+
<Trans>Select from globally available languages for this channel</Trans>
|
|
322
|
+
</p>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{channelLanguages.length > 0 && (
|
|
327
|
+
<div>
|
|
328
|
+
<Label className="text-sm font-medium mb-2 block">
|
|
329
|
+
<Trans>Default Language</Trans>
|
|
330
|
+
</Label>
|
|
331
|
+
<Select
|
|
332
|
+
value={channelDefaultLanguage}
|
|
333
|
+
onValueChange={setChannelDefaultLanguage}
|
|
334
|
+
disabled={!canUpdateChannel}
|
|
335
|
+
>
|
|
336
|
+
<SelectTrigger className="w-[200px]">
|
|
337
|
+
<SelectValue placeholder="Select default language" />
|
|
338
|
+
</SelectTrigger>
|
|
339
|
+
<SelectContent>
|
|
340
|
+
{channelLanguages.map(languageCode => (
|
|
341
|
+
<SelectItem key={languageCode} value={languageCode}>
|
|
342
|
+
{formatLanguageName(languageCode)} ({languageCode.toUpperCase()})
|
|
343
|
+
</SelectItem>
|
|
344
|
+
))}
|
|
345
|
+
</SelectContent>
|
|
346
|
+
</Select>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<DialogFooter>
|
|
355
|
+
<Button variant="outline" onClick={onClose} disabled={isLoading}>
|
|
356
|
+
<Trans>Cancel</Trans>
|
|
357
|
+
</Button>
|
|
358
|
+
<Button
|
|
359
|
+
onClick={handleSave}
|
|
360
|
+
disabled={!hasChanges() || isLoading}
|
|
361
|
+
>
|
|
362
|
+
{isLoading ? <Trans>Saving...</Trans> : <Trans>Save Changes</Trans>}
|
|
363
|
+
</Button>
|
|
364
|
+
</DialogFooter>
|
|
365
|
+
</DialogContent>
|
|
366
|
+
</Dialog>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
@@ -229,7 +229,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
|
|
|
229
229
|
name={fieldName}
|
|
230
230
|
render={({ field }) => (
|
|
231
231
|
<FormItem>
|
|
232
|
-
<FormLabel>{getTranslation(fieldDef.label) ??
|
|
232
|
+
<FormLabel>{getTranslation(fieldDef.label) ?? fieldDef.name}</FormLabel>
|
|
233
233
|
<FormControl>
|
|
234
234
|
<CustomFieldListInput
|
|
235
235
|
field={field}
|
|
@@ -263,7 +263,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
|
|
|
263
263
|
name={fieldName}
|
|
264
264
|
render={({ field }) => (
|
|
265
265
|
<FormItem>
|
|
266
|
-
<FormLabel>{getTranslation(fieldDef.label) ??
|
|
266
|
+
<FormLabel>{getTranslation(fieldDef.label) ?? fieldDef.name}</FormLabel>
|
|
267
267
|
<FormControl>
|
|
268
268
|
<StructFormInput
|
|
269
269
|
field={field}
|
|
@@ -291,7 +291,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
|
|
|
291
291
|
<CustomFieldFormItem
|
|
292
292
|
fieldDef={fieldDef}
|
|
293
293
|
getTranslation={getTranslation}
|
|
294
|
-
fieldName={
|
|
294
|
+
fieldName={fieldDef.name}
|
|
295
295
|
>
|
|
296
296
|
<FormInputForType fieldDef={fieldDef} field={field} />
|
|
297
297
|
</CustomFieldFormItem>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ListQueryOptionsShape,
|
|
8
8
|
ListQueryShape,
|
|
9
9
|
PaginatedListDataTable,
|
|
10
|
+
PaginatedListRefresherRegisterFn,
|
|
10
11
|
RowAction,
|
|
11
12
|
} from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
12
13
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
@@ -53,6 +54,12 @@ export interface ListPageProps<
|
|
|
53
54
|
transformData?: (data: any[]) => any[];
|
|
54
55
|
setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
|
|
55
56
|
bulkActions?: BulkAction[];
|
|
57
|
+
/**
|
|
58
|
+
* Register a function that allows you to assign a refresh function for
|
|
59
|
+
* this list. The function can be assigned to a ref and then called when
|
|
60
|
+
* the list needs to be refreshed.
|
|
61
|
+
*/
|
|
62
|
+
registerRefresher?: PaginatedListRefresherRegisterFn;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/**
|
|
@@ -90,6 +97,7 @@ export function ListPage<
|
|
|
90
97
|
transformData,
|
|
91
98
|
setTableOptions,
|
|
92
99
|
bulkActions,
|
|
100
|
+
registerRefresher,
|
|
93
101
|
}: Readonly<ListPageProps<T, U, V, AC>>) {
|
|
94
102
|
const route = typeof routeOrFn === 'function' ? routeOrFn() : routeOrFn;
|
|
95
103
|
const routeSearch = route.useSearch();
|
|
@@ -191,6 +199,7 @@ export function ListPage<
|
|
|
191
199
|
bulkActions={bulkActions}
|
|
192
200
|
setTableOptions={setTableOptions}
|
|
193
201
|
transformData={transformData}
|
|
202
|
+
registerRefresher={registerRefresher}
|
|
194
203
|
/>
|
|
195
204
|
</FullWidthPageBlock>
|
|
196
205
|
</PageLayout>
|