@vendure/dashboard 3.3.2 → 3.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/utils/config-loader.d.ts +12 -1
- package/dist/plugin/utils/config-loader.js +25 -7
- package/dist/plugin/vite-plugin-vendure-dashboard.d.ts +8 -0
- package/dist/plugin/vite-plugin-vendure-dashboard.js +5 -1
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +1 -4
- package/src/app/routes/_authenticated/_channels/channels.tsx +18 -0
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +1 -5
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -4
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +1 -4
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +1 -4
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -5
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +56 -74
- package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +369 -0
- package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +435 -0
- package/src/app/routes/_authenticated/_products/components/product-option-select.tsx +117 -0
- package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +4 -2
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +17 -3
- package/src/app/routes/_authenticated/_profile/profile.tsx +1 -4
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -4
- package/src/lib/components/data-table/data-table-view-options.tsx +12 -2
- package/src/lib/components/data-table/data-table.tsx +9 -0
- package/src/lib/components/layout/channel-switcher.tsx +1 -2
- package/src/lib/components/shared/assigned-facet-values.tsx +13 -14
- package/src/lib/components/shared/entity-assets.tsx +140 -70
- package/src/lib/components/shared/paginated-list-data-table.tsx +10 -0
- package/src/lib/components/ui/button.tsx +1 -1
- package/src/lib/framework/form-engine/use-generated-form.tsx +1 -0
- package/src/lib/framework/page/list-page.tsx +2 -2
- package/src/lib/framework/page/use-detail-page.ts +7 -0
- package/src/lib/graphql/api.ts +10 -1
- package/src/lib/hooks/use-permissions.ts +4 -4
- package/src/lib/providers/auth.tsx +9 -3
- package/src/lib/providers/channel-provider.tsx +64 -24
- package/src/lib/providers/server-config.tsx +2 -2
- package/vite/utils/config-loader.ts +48 -13
- package/vite/vite-plugin-vendure-dashboard.ts +14 -4
|
@@ -6,15 +6,16 @@ import {
|
|
|
6
6
|
} from '@dnd-kit/modifiers';
|
|
7
7
|
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
8
8
|
import { CSS } from '@dnd-kit/utilities';
|
|
9
|
-
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
|
10
9
|
import { Table } from '@tanstack/react-table';
|
|
11
10
|
import { GripVertical, Settings2 } from 'lucide-react';
|
|
12
11
|
|
|
13
12
|
import { Button } from '@/components/ui/button.js';
|
|
14
13
|
import {
|
|
14
|
+
DropdownMenuSeparator, DropdownMenuTrigger,
|
|
15
15
|
DropdownMenu,
|
|
16
16
|
DropdownMenuCheckboxItem,
|
|
17
|
-
DropdownMenuContent
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuItem
|
|
18
19
|
} from '@/components/ui/dropdown-menu.js';
|
|
19
20
|
import { usePage } from '@/hooks/use-page.js';
|
|
20
21
|
import { useUserSettings } from '@/hooks/use-user-settings.js';
|
|
@@ -69,6 +70,13 @@ export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps
|
|
|
69
70
|
}
|
|
70
71
|
};
|
|
71
72
|
|
|
73
|
+
const handleReset = () => {
|
|
74
|
+
if (page?.pageId) {
|
|
75
|
+
setTableSettings(page.pageId, 'columnOrder', undefined);
|
|
76
|
+
setTableSettings(page.pageId, 'columnVisibility', undefined);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
72
80
|
return (
|
|
73
81
|
<div className="flex items-center gap-2">
|
|
74
82
|
<DropdownMenu>
|
|
@@ -95,6 +103,8 @@ export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps
|
|
|
95
103
|
))}
|
|
96
104
|
</SortableContext>
|
|
97
105
|
</DndContext>
|
|
106
|
+
<DropdownMenuSeparator />
|
|
107
|
+
<DropdownMenuItem onClick={handleReset}>Reset</DropdownMenuItem>
|
|
98
108
|
</DropdownMenuContent>
|
|
99
109
|
</DropdownMenu>
|
|
100
110
|
</div>
|
|
@@ -86,6 +86,15 @@ export function DataTable<TData>({
|
|
|
86
86
|
defaultColumnVisibility ?? {},
|
|
87
87
|
);
|
|
88
88
|
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
// If the defaultColumnVisibility changes externally (e.g. the user reset the table settings),
|
|
91
|
+
// we want to reset the column visibility to the default.
|
|
92
|
+
if (defaultColumnVisibility && JSON.stringify(defaultColumnVisibility) !== JSON.stringify(columnVisibility)) {
|
|
93
|
+
setColumnVisibility(defaultColumnVisibility);
|
|
94
|
+
}
|
|
95
|
+
// We intentionally do not include `columnVisibility` in the dependency array
|
|
96
|
+
}, [defaultColumnVisibility]);
|
|
97
|
+
|
|
89
98
|
let tableOptions: TableOptions<TData> = {
|
|
90
99
|
data,
|
|
91
100
|
columns,
|
|
@@ -61,13 +61,12 @@ export function ChannelSwitcher() {
|
|
|
61
61
|
onClick={() => setSelectedChannel(channel.id)}
|
|
62
62
|
className="gap-2 p-2"
|
|
63
63
|
>
|
|
64
|
-
<div className="flex size-
|
|
64
|
+
<div className="flex size-8 items-center justify-center rounded border">
|
|
65
65
|
<span className="truncate font-semibold text-xs">
|
|
66
66
|
{channel.defaultCurrencyCode}
|
|
67
67
|
</span>
|
|
68
68
|
</div>
|
|
69
69
|
<ChannelCodeLabel code={channel.code} />
|
|
70
|
-
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
|
71
70
|
</DropdownMenuItem>
|
|
72
71
|
))}
|
|
73
72
|
<DropdownMenuSeparator />
|
|
@@ -23,19 +23,19 @@ interface AssignedFacetValuesProps {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function AssignedFacetValues({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}: AssignedFacetValuesProps) {
|
|
26
|
+
value = [],
|
|
27
|
+
facetValues,
|
|
28
|
+
canUpdate = true,
|
|
29
|
+
onBlur,
|
|
30
|
+
onChange,
|
|
31
|
+
}: AssignedFacetValuesProps) {
|
|
32
32
|
const [knownFacetValues, setKnownFacetValues] = useState<FacetValue[]>(facetValues);
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
function onSelectHandler(facetValue: FacetValue) {
|
|
35
35
|
setKnownFacetValues(prev => [...prev, facetValue]);
|
|
36
36
|
onChange?.([...new Set([...(value ?? []), facetValue.id])]);
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
function onRemoveHandler(id: string) {
|
|
40
40
|
onChange?.(value?.filter(fvId => fvId !== id) ?? []);
|
|
41
41
|
}
|
|
@@ -47,12 +47,11 @@ export function AssignedFacetValues({
|
|
|
47
47
|
const facetValue = knownFacetValues.find(fv => fv.id === id);
|
|
48
48
|
if (!facetValue) return null;
|
|
49
49
|
return (
|
|
50
|
-
<div className="mb-2 mr-1">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
onRemove={onRemoveHandler}
|
|
50
|
+
<div className="mb-2 mr-1" key={facetValue.id}>
|
|
51
|
+
<FacetValueChip
|
|
52
|
+
facetValue={facetValue}
|
|
53
|
+
removable={canUpdate}
|
|
54
|
+
onRemove={onRemoveHandler}
|
|
56
55
|
/>
|
|
57
56
|
</div>
|
|
58
57
|
);
|
|
@@ -47,16 +47,55 @@ interface EntityAssetsProps {
|
|
|
47
47
|
onChange?: (change: EntityAssetValue) => void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// FeaturedAsset component
|
|
51
|
+
interface FeaturedAssetProps {
|
|
52
|
+
featuredAsset?: Asset | null;
|
|
53
|
+
compact?: boolean;
|
|
54
|
+
onSelectAssets: () => void;
|
|
55
|
+
onPreviewAsset: (asset: Asset) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function FeaturedAsset({
|
|
59
|
+
featuredAsset,
|
|
60
|
+
compact = false,
|
|
61
|
+
onSelectAssets,
|
|
62
|
+
onPreviewAsset,
|
|
63
|
+
}: FeaturedAssetProps) {
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
className={`flex items-center justify-center ${compact ? 'h-40' : 'h-64'} border border-dashed rounded-md`}
|
|
67
|
+
>
|
|
68
|
+
{featuredAsset ? (
|
|
69
|
+
<VendureImage
|
|
70
|
+
asset={featuredAsset}
|
|
71
|
+
mode="crop"
|
|
72
|
+
preset="small"
|
|
73
|
+
onClick={() => onPreviewAsset(featuredAsset)}
|
|
74
|
+
className="max-w-full max-h-full object-contain cursor-pointer"
|
|
75
|
+
/>
|
|
76
|
+
) : (
|
|
77
|
+
<div
|
|
78
|
+
className="flex flex-col items-center justify-center text-muted-foreground cursor-pointer"
|
|
79
|
+
onClick={onSelectAssets}
|
|
80
|
+
>
|
|
81
|
+
<ImageIcon className={compact ? 'h-10 w-10' : 'h-16 w-16'} />
|
|
82
|
+
{!compact && <div className="mt-2">No featured asset</div>}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
// Sortable asset item component
|
|
51
90
|
function SortableAsset({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}: {
|
|
91
|
+
asset,
|
|
92
|
+
compact,
|
|
93
|
+
isFeatured,
|
|
94
|
+
updatePermissions,
|
|
95
|
+
onPreview,
|
|
96
|
+
onSetAsFeatured,
|
|
97
|
+
onRemove,
|
|
98
|
+
}: {
|
|
60
99
|
asset: Asset;
|
|
61
100
|
compact: boolean;
|
|
62
101
|
isFeatured: boolean;
|
|
@@ -122,13 +161,13 @@ function SortableAsset({
|
|
|
122
161
|
}
|
|
123
162
|
|
|
124
163
|
export function EntityAssets({
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}: EntityAssetsProps) {
|
|
164
|
+
assets: initialAssets = [],
|
|
165
|
+
featuredAsset: initialFeaturedAsset,
|
|
166
|
+
compact = false,
|
|
167
|
+
updatePermissions = true,
|
|
168
|
+
multiSelect = true,
|
|
169
|
+
onChange,
|
|
170
|
+
}: EntityAssetsProps) {
|
|
132
171
|
const [assets, setAssets] = useState<Asset[]>([...initialAssets]);
|
|
133
172
|
const [featuredAsset, setFeaturedAsset] = useState<Asset | undefined | null>(initialFeaturedAsset);
|
|
134
173
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
|
@@ -234,57 +273,6 @@ export function EntityAssets({
|
|
|
234
273
|
},
|
|
235
274
|
[featuredAsset],
|
|
236
275
|
);
|
|
237
|
-
|
|
238
|
-
const renderAssetList = () => (
|
|
239
|
-
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
240
|
-
<div className={`${compact ? 'max-h-32' : ''} overflow-auto p-1`}>
|
|
241
|
-
<SortableContext
|
|
242
|
-
items={assets.map(asset => asset.id)}
|
|
243
|
-
strategy={horizontalListSortingStrategy}
|
|
244
|
-
>
|
|
245
|
-
<div className="flex flex-wrap gap-2">
|
|
246
|
-
{assets.map(asset => (
|
|
247
|
-
<SortableAsset
|
|
248
|
-
key={asset.id}
|
|
249
|
-
asset={asset}
|
|
250
|
-
compact={compact}
|
|
251
|
-
isFeatured={isFeatured(asset)}
|
|
252
|
-
updatePermissions={updatePermissions}
|
|
253
|
-
onPreview={setPreviewAsset}
|
|
254
|
-
onSetAsFeatured={handleSetAsFeatured}
|
|
255
|
-
onRemove={handleRemoveAsset}
|
|
256
|
-
/>
|
|
257
|
-
))}
|
|
258
|
-
</div>
|
|
259
|
-
</SortableContext>
|
|
260
|
-
</div>
|
|
261
|
-
</DndContext>
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
const FeaturedAsset = () => (
|
|
265
|
-
<div
|
|
266
|
-
className={`flex items-center justify-center ${compact ? 'h-40' : 'h-64'} border border-dashed rounded-md`}
|
|
267
|
-
>
|
|
268
|
-
{featuredAsset ? (
|
|
269
|
-
<VendureImage
|
|
270
|
-
asset={featuredAsset}
|
|
271
|
-
mode="crop"
|
|
272
|
-
preset="small"
|
|
273
|
-
onClick={() => setPreviewAsset(featuredAsset)}
|
|
274
|
-
className="max-w-full max-h-full object-contain cursor-pointer"
|
|
275
|
-
/>
|
|
276
|
-
) : (
|
|
277
|
-
<div
|
|
278
|
-
className="flex flex-col items-center justify-center text-muted-foreground cursor-pointer"
|
|
279
|
-
onClick={handleSelectAssets}
|
|
280
|
-
>
|
|
281
|
-
<ImageIcon className={compact ? 'h-10 w-10' : 'h-16 w-16'} />
|
|
282
|
-
{!compact && <div className="mt-2">No featured asset</div>}
|
|
283
|
-
</div>
|
|
284
|
-
)}
|
|
285
|
-
</div>
|
|
286
|
-
);
|
|
287
|
-
|
|
288
276
|
// AddAssetButton component
|
|
289
277
|
const AddAssetButton = () =>
|
|
290
278
|
updatePermissions && (
|
|
@@ -303,15 +291,45 @@ export function EntityAssets({
|
|
|
303
291
|
<>
|
|
304
292
|
{compact ? (
|
|
305
293
|
<div className="flex flex-col gap-3">
|
|
306
|
-
<FeaturedAsset
|
|
307
|
-
|
|
294
|
+
<FeaturedAsset
|
|
295
|
+
featuredAsset={featuredAsset}
|
|
296
|
+
compact={compact}
|
|
297
|
+
onSelectAssets={handleSelectAssets}
|
|
298
|
+
onPreviewAsset={setPreviewAsset}
|
|
299
|
+
/>
|
|
300
|
+
<AssetList
|
|
301
|
+
assets={assets}
|
|
302
|
+
compact={compact}
|
|
303
|
+
sensors={sensors}
|
|
304
|
+
updatePermissions={updatePermissions}
|
|
305
|
+
isFeatured={isFeatured}
|
|
306
|
+
onPreview={setPreviewAsset}
|
|
307
|
+
onSetAsFeatured={handleSetAsFeatured}
|
|
308
|
+
onRemove={handleRemoveAsset}
|
|
309
|
+
onDragEnd={handleDragEnd}
|
|
310
|
+
/>
|
|
308
311
|
<AddAssetButton />
|
|
309
312
|
</div>
|
|
310
313
|
) : (
|
|
311
314
|
<div className="grid grid-cols-1 md:grid-cols-[256px_1fr] gap-4">
|
|
312
|
-
<FeaturedAsset
|
|
315
|
+
<FeaturedAsset
|
|
316
|
+
featuredAsset={featuredAsset}
|
|
317
|
+
compact={compact}
|
|
318
|
+
onSelectAssets={handleSelectAssets}
|
|
319
|
+
onPreviewAsset={setPreviewAsset}
|
|
320
|
+
/>
|
|
313
321
|
<div className="flex flex-col gap-4">
|
|
314
|
-
|
|
322
|
+
<AssetList
|
|
323
|
+
assets={assets}
|
|
324
|
+
compact={compact}
|
|
325
|
+
sensors={sensors}
|
|
326
|
+
updatePermissions={updatePermissions}
|
|
327
|
+
isFeatured={isFeatured}
|
|
328
|
+
onPreview={setPreviewAsset}
|
|
329
|
+
onSetAsFeatured={handleSetAsFeatured}
|
|
330
|
+
onRemove={handleRemoveAsset}
|
|
331
|
+
onDragEnd={handleDragEnd}
|
|
332
|
+
/>
|
|
315
333
|
<AddAssetButton />
|
|
316
334
|
</div>
|
|
317
335
|
</div>
|
|
@@ -338,3 +356,55 @@ export function EntityAssets({
|
|
|
338
356
|
</>
|
|
339
357
|
);
|
|
340
358
|
}
|
|
359
|
+
|
|
360
|
+
// AssetList component
|
|
361
|
+
interface AssetListProps {
|
|
362
|
+
assets: Asset[];
|
|
363
|
+
compact: boolean;
|
|
364
|
+
sensors: ReturnType<typeof useSensors>;
|
|
365
|
+
updatePermissions: boolean;
|
|
366
|
+
isFeatured: (asset: Asset) => boolean;
|
|
367
|
+
onPreview: (asset: Asset) => void;
|
|
368
|
+
onSetAsFeatured: (asset: Asset) => void;
|
|
369
|
+
onRemove: (asset: Asset) => void;
|
|
370
|
+
onDragEnd: (event: DragEndEvent) => void;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
function AssetList({
|
|
375
|
+
assets,
|
|
376
|
+
compact,
|
|
377
|
+
sensors,
|
|
378
|
+
updatePermissions,
|
|
379
|
+
isFeatured,
|
|
380
|
+
onPreview,
|
|
381
|
+
onSetAsFeatured,
|
|
382
|
+
onRemove,
|
|
383
|
+
onDragEnd,
|
|
384
|
+
}: AssetListProps) {
|
|
385
|
+
return (
|
|
386
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
|
387
|
+
<div className={`${compact ? 'max-h-32' : ''} overflow-auto p-1`}>
|
|
388
|
+
<SortableContext
|
|
389
|
+
items={assets.map(asset => asset.id)}
|
|
390
|
+
strategy={horizontalListSortingStrategy}
|
|
391
|
+
>
|
|
392
|
+
<div className="flex flex-wrap gap-2">
|
|
393
|
+
{assets.map(asset => (
|
|
394
|
+
<SortableAsset
|
|
395
|
+
key={asset.id}
|
|
396
|
+
asset={asset}
|
|
397
|
+
compact={compact}
|
|
398
|
+
isFeatured={isFeatured(asset)}
|
|
399
|
+
updatePermissions={updatePermissions}
|
|
400
|
+
onPreview={onPreview}
|
|
401
|
+
onSetAsFeatured={onSetAsFeatured}
|
|
402
|
+
onRemove={onRemove}
|
|
403
|
+
/>
|
|
404
|
+
))}
|
|
405
|
+
</div>
|
|
406
|
+
</SortableContext>
|
|
407
|
+
</div>
|
|
408
|
+
</DndContext>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
@@ -174,6 +174,8 @@ export interface RowAction<T> {
|
|
|
174
174
|
onClick?: (row: Row<T>) => void;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
export type PaginatedListRefresherRegisterFn = (refreshFn: () => void) => void;
|
|
178
|
+
|
|
177
179
|
export interface PaginatedListDataTableProps<
|
|
178
180
|
T extends TypedDocumentNode<U, V>,
|
|
179
181
|
U extends any,
|
|
@@ -202,6 +204,12 @@ export interface PaginatedListDataTableProps<
|
|
|
202
204
|
disableViewOptions?: boolean;
|
|
203
205
|
transformData?: (data: PaginatedListItemFields<T>[]) => PaginatedListItemFields<T>[];
|
|
204
206
|
setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
|
|
207
|
+
/**
|
|
208
|
+
* Register a function that allows you to assign a refresh function for
|
|
209
|
+
* this list. The function can be assigned to a ref and then called when
|
|
210
|
+
* the list needs to be refreshed.
|
|
211
|
+
*/
|
|
212
|
+
registerRefresher?: PaginatedListRefresherRegisterFn;
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
export const PaginatedListDataTableKey = 'PaginatedListDataTable';
|
|
@@ -234,6 +242,7 @@ export function PaginatedListDataTable<
|
|
|
234
242
|
disableViewOptions,
|
|
235
243
|
setTableOptions,
|
|
236
244
|
transformData,
|
|
245
|
+
registerRefresher,
|
|
237
246
|
}: PaginatedListDataTableProps<T, U, V, AC>) {
|
|
238
247
|
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
|
239
248
|
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
|
@@ -274,6 +283,7 @@ export function PaginatedListDataTable<
|
|
|
274
283
|
function refetchPaginatedList() {
|
|
275
284
|
queryClient.invalidateQueries({ queryKey });
|
|
276
285
|
}
|
|
286
|
+
registerRefresher?.(refetchPaginatedList);
|
|
277
287
|
|
|
278
288
|
const { data } = useQuery({
|
|
279
289
|
queryFn: () => {
|
|
@@ -43,7 +43,7 @@ export interface ButtonProps
|
|
|
43
43
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
44
44
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
45
45
|
const Comp = asChild ? Slot : 'button';
|
|
46
|
-
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
|
46
|
+
return <Comp className={cn(buttonVariants({ variant, size, className }))} type={props.type ?? 'button'} ref={ref} {...props} />;
|
|
47
47
|
},
|
|
48
48
|
);
|
|
49
49
|
Button.displayName = 'Button';
|
|
@@ -110,8 +110,8 @@ export function ListPage<
|
|
|
110
110
|
itemsPerPage: routeSearch.perPage ? parseInt(routeSearch.perPage) : tableSettings?.pageSize ?? 10,
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
-
const columnVisibility = pageId ? tableSettings?.columnVisibility : defaultVisibility;
|
|
114
|
-
const columnOrder = pageId ? tableSettings?.columnOrder : defaultColumnOrder;
|
|
113
|
+
const columnVisibility = pageId ? tableSettings?.columnVisibility ?? defaultVisibility : defaultVisibility;
|
|
114
|
+
const columnOrder = pageId ? tableSettings?.columnOrder ?? defaultColumnOrder : defaultColumnOrder;
|
|
115
115
|
const columnFilters = pageId ? tableSettings?.columnFilters : routeSearch.filters;
|
|
116
116
|
|
|
117
117
|
const sorting: SortingState = (routeSearch.sort ?? '')
|
|
@@ -288,6 +288,13 @@ export function useDetailPage<
|
|
|
288
288
|
},
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
+
// A kind of ridiculous workaround to ensure that the `isDirty` and `isValid` properties
|
|
292
|
+
// are always up-to-date when used by the consuming component. This seems to be necessary
|
|
293
|
+
// due to the way that `react-hook-form` uses a Proxy object for the form state.
|
|
294
|
+
// See https://react-hook-form.com/docs/useform/formstate
|
|
295
|
+
// noinspection JSUnusedLocalSymbols
|
|
296
|
+
const { isDirty, isValid } = form.formState;
|
|
297
|
+
|
|
291
298
|
return {
|
|
292
299
|
form: form as any,
|
|
293
300
|
submitHandler,
|
package/src/lib/graphql/api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
2
2
|
import { AwesomeGraphQLClient } from 'awesome-graphql-client';
|
|
3
|
-
import { DocumentNode,
|
|
3
|
+
import { DocumentNode, print } from 'graphql';
|
|
4
4
|
import { uiConfig } from 'virtual:vendure-ui-config';
|
|
5
5
|
|
|
6
6
|
const API_URL = uiConfig.apiHost + (uiConfig.apiPort !== 'auto' ? `:${uiConfig.apiPort}` : '') + '/admin-api';
|
|
@@ -11,8 +11,17 @@ export type RequestDocument = string | DocumentNode;
|
|
|
11
11
|
const awesomeClient = new AwesomeGraphQLClient({
|
|
12
12
|
endpoint: API_URL,
|
|
13
13
|
fetch: async (url: string, options: RequestInit = {}) => {
|
|
14
|
+
// Get the active channel token from localStorage
|
|
15
|
+
const channelToken = localStorage.getItem('vendure-selected-channel-token');
|
|
16
|
+
const headers = new Headers(options.headers);
|
|
17
|
+
|
|
18
|
+
if (channelToken) {
|
|
19
|
+
headers.set('vendure-token', channelToken);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
return fetch(url, {
|
|
15
23
|
...options,
|
|
24
|
+
headers,
|
|
16
25
|
credentials: 'include',
|
|
17
26
|
mode: 'cors',
|
|
18
27
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useAuth } from '@/hooks/use-auth.js';
|
|
2
|
+
import { useChannel } from '@/hooks/use-channel.js';
|
|
2
3
|
import { Permission } from '@vendure/common/lib/generated-types';
|
|
3
4
|
|
|
4
|
-
import { useUserSettings } from './use-user-settings.js';
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* @description
|
|
8
7
|
* **Status: Developer Preview**
|
|
@@ -18,13 +17,14 @@ import { useUserSettings } from './use-user-settings.js';
|
|
|
18
17
|
*/
|
|
19
18
|
export function usePermissions() {
|
|
20
19
|
const { channels } = useAuth();
|
|
21
|
-
const {
|
|
20
|
+
const { selectedChannelId } = useChannel();
|
|
22
21
|
|
|
23
22
|
function hasPermissions(permissions: string[]) {
|
|
24
23
|
if (permissions.length === 0) {
|
|
25
24
|
return true;
|
|
26
25
|
}
|
|
27
|
-
|
|
26
|
+
// Use the selected channel instead of settings.activeChannelId
|
|
27
|
+
const activeChannel = (channels ?? []).find(channel => channel.id === selectedChannelId);
|
|
28
28
|
if (!activeChannel) {
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
@@ -103,6 +103,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
103
103
|
if (data.login.__typename === 'CurrentUser') {
|
|
104
104
|
setAuthenticationError(undefined);
|
|
105
105
|
await refetchCurrentUser();
|
|
106
|
+
// Invalidate all queries to ensure fresh data after login
|
|
107
|
+
await queryClient.invalidateQueries();
|
|
106
108
|
setStatus('authenticated');
|
|
107
109
|
onLoginSuccess?.();
|
|
108
110
|
} else {
|
|
@@ -115,7 +117,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
115
117
|
setStatus('unauthenticated');
|
|
116
118
|
});
|
|
117
119
|
},
|
|
118
|
-
[refetchCurrentUser],
|
|
120
|
+
[refetchCurrentUser, queryClient],
|
|
119
121
|
);
|
|
120
122
|
|
|
121
123
|
const logout = React.useCallback(
|
|
@@ -123,13 +125,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
123
125
|
setStatus('verifying');
|
|
124
126
|
api.mutate(LogOutMutation)({}).then(async data => {
|
|
125
127
|
if (data?.logout.success) {
|
|
126
|
-
|
|
128
|
+
// Clear all cached queries to prevent stale data
|
|
129
|
+
queryClient.clear();
|
|
130
|
+
// Clear selected channel from localStorage
|
|
131
|
+
localStorage.removeItem('vendure-selected-channel');
|
|
132
|
+
localStorage.removeItem('vendure-selected-channel-token');
|
|
127
133
|
setStatus('unauthenticated');
|
|
128
134
|
onLogoutSuccess?.();
|
|
129
135
|
}
|
|
130
136
|
});
|
|
131
137
|
},
|
|
132
|
-
[
|
|
138
|
+
[queryClient],
|
|
133
139
|
);
|
|
134
140
|
|
|
135
141
|
// Determine isAuthenticated from currentUserData
|