@vendure/dashboard 3.5.2-master-202512180239 → 3.5.2-master-202512190240
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/constants.js +21 -2
- package/package.json +3 -3
- package/src/app/routeTree.gen.ts +1135 -1072
- package/src/app/routes/_authenticated/_facets/components/facet-values-sheet.tsx +4 -1
- package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +1 -1
- package/src/app/routes/_authenticated/_facets/facets.tsx +22 -38
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -1
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +0 -1
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +2 -2
- package/src/app/routes/_authenticated/_products/products.graphql.ts +5 -0
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +24 -1
- package/src/app/routes/_authenticated/_system/components/payload-dialog.tsx +9 -2
- package/src/app/routes/_authenticated/_system/job-queue.tsx +11 -2
- package/src/app/routes/_authenticated/_zones/zones.tsx +1 -0
- package/src/i18n/locales/ar.po +177 -141
- package/src/i18n/locales/cs.po +177 -141
- package/src/i18n/locales/de.po +177 -141
- package/src/i18n/locales/en.po +177 -141
- package/src/i18n/locales/es.po +177 -141
- package/src/i18n/locales/fa.po +177 -141
- package/src/i18n/locales/fr.po +177 -141
- package/src/i18n/locales/he.po +177 -141
- package/src/i18n/locales/hr.po +177 -141
- package/src/i18n/locales/it.po +177 -141
- package/src/i18n/locales/ja.po +177 -141
- package/src/i18n/locales/nb.po +177 -141
- package/src/i18n/locales/ne.po +177 -141
- package/src/i18n/locales/pl.po +177 -141
- package/src/i18n/locales/pt_BR.po +177 -141
- package/src/i18n/locales/pt_PT.po +177 -141
- package/src/i18n/locales/ru.po +177 -141
- package/src/i18n/locales/sv.po +177 -141
- package/src/i18n/locales/tr.po +177 -141
- package/src/i18n/locales/uk.po +177 -141
- package/src/i18n/locales/zh_Hans.po +177 -141
- package/src/i18n/locales/zh_Hant.po +177 -141
- package/src/lib/components/data-table/data-table-context.tsx +18 -3
- package/src/lib/components/data-table/global-views-bar.tsx +1 -1
- package/src/lib/components/data-table/save-view-dialog.tsx +21 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +56 -24
- package/src/lib/components/data-table/views-sheet.tsx +1 -1
- package/src/lib/components/layout/channel-switcher.tsx +7 -5
- package/src/lib/components/layout/nav-main.tsx +2 -2
- package/src/lib/components/shared/alerts.tsx +3 -1
- package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +10 -10
- package/src/lib/components/shared/assign-to-channel-dialog.tsx +1 -1
- package/src/lib/components/shared/assigned-channels.tsx +108 -0
- package/src/lib/components/shared/assigned-facet-values.tsx +5 -7
- package/src/lib/components/shared/channel-chip.tsx +43 -0
- package/src/lib/components/ui/dropdown-menu.tsx +4 -1
- package/src/lib/components/ui/sidebar.tsx +2 -1
- package/src/lib/hooks/use-saved-views.ts +1 -0
- package/src/lib/providers/channel-provider.tsx +7 -1
- package/src/lib/types/saved-views.ts +3 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
|
|
2
|
+
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
3
3
|
import { ColumnFiltersState, SortingState, Table } from '@tanstack/react-table';
|
|
4
4
|
import React, { createContext, ReactNode, useContext } from 'react';
|
|
5
5
|
|
|
6
|
+
export type ColumnConfig = { columnOrder: string[]; columnVisibility: Record<string, boolean> };
|
|
7
|
+
|
|
6
8
|
interface DataTableContextValue {
|
|
7
9
|
columnFilters: ColumnFiltersState;
|
|
8
10
|
setColumnFilters: React.Dispatch<React.SetStateAction<ColumnFiltersState>>;
|
|
@@ -16,7 +18,7 @@ interface DataTableContextValue {
|
|
|
16
18
|
onRefresh?: () => void;
|
|
17
19
|
isLoading?: boolean;
|
|
18
20
|
table?: Table<any>;
|
|
19
|
-
handleApplyView: (filters: ColumnFiltersState, searchTerm?: string) => void;
|
|
21
|
+
handleApplyView: (filters: ColumnFiltersState, columnConfig: ColumnConfig, searchTerm?: string) => void;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const DataTableContext = createContext<DataTableContextValue | undefined>(undefined);
|
|
@@ -52,7 +54,13 @@ export function DataTableProvider({
|
|
|
52
54
|
isLoading,
|
|
53
55
|
table,
|
|
54
56
|
}: DataTableProviderProps) {
|
|
55
|
-
const
|
|
57
|
+
const { setTableSettings } = useUserSettings();
|
|
58
|
+
|
|
59
|
+
const handleApplyView = (
|
|
60
|
+
filters: ColumnFiltersState,
|
|
61
|
+
columnConfig: ColumnConfig,
|
|
62
|
+
viewSearchTerm?: string,
|
|
63
|
+
) => {
|
|
56
64
|
setColumnFilters(filters);
|
|
57
65
|
if (viewSearchTerm !== undefined && onSearchTermChange) {
|
|
58
66
|
setSearchTerm(viewSearchTerm);
|
|
@@ -61,6 +69,13 @@ export function DataTableProvider({
|
|
|
61
69
|
if (onFilterChange && table) {
|
|
62
70
|
onFilterChange(table, filters);
|
|
63
71
|
}
|
|
72
|
+
|
|
73
|
+
if (pageId && columnConfig.columnOrder) {
|
|
74
|
+
setTableSettings(pageId, 'columnOrder', columnConfig.columnOrder);
|
|
75
|
+
}
|
|
76
|
+
if (pageId && columnConfig.columnVisibility) {
|
|
77
|
+
setTableSettings(pageId, 'columnVisibility', columnConfig.columnVisibility);
|
|
78
|
+
}
|
|
64
79
|
};
|
|
65
80
|
|
|
66
81
|
const value: DataTableContextValue = {
|
|
@@ -28,7 +28,7 @@ export const GlobalViewsBar: React.FC = () => {
|
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
const handleViewClick = (view: SavedView) => {
|
|
31
|
-
handleApplyView(view.filters, view.searchTerm);
|
|
31
|
+
handleApplyView(view.filters, view.columnConfig, view.searchTerm);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
const isViewActive = (view: SavedView) => {
|
|
@@ -7,6 +7,8 @@ import { Input } from '../ui/input.js';
|
|
|
7
7
|
import { Label } from '../ui/label.js';
|
|
8
8
|
import { RadioGroup, RadioGroupItem } from '../ui/radio-group.js';
|
|
9
9
|
import { toast } from 'sonner';
|
|
10
|
+
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
11
|
+
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
10
12
|
|
|
11
13
|
interface SaveViewDialogProps {
|
|
12
14
|
open: boolean;
|
|
@@ -25,6 +27,21 @@ export const SaveViewDialog: React.FC<SaveViewDialogProps> = ({
|
|
|
25
27
|
const [scope, setScope] = useState<'user' | 'global'>('user');
|
|
26
28
|
const [saving, setSaving] = useState(false);
|
|
27
29
|
const { saveView, userViews, globalViews, canManageGlobalViews } = useSavedViews();
|
|
30
|
+
const { pageId } = usePage();
|
|
31
|
+
const { settings } = useUserSettings();
|
|
32
|
+
|
|
33
|
+
const defaultVisibility = {
|
|
34
|
+
id: false,
|
|
35
|
+
createdAt: false,
|
|
36
|
+
updatedAt: false,
|
|
37
|
+
type: false,
|
|
38
|
+
currencyCode: false,
|
|
39
|
+
}
|
|
40
|
+
const tableSettings = pageId ? settings.tableSettings?.[pageId] : undefined;
|
|
41
|
+
const columnVisibility = pageId
|
|
42
|
+
? (tableSettings?.columnVisibility ?? defaultVisibility)
|
|
43
|
+
: defaultVisibility;
|
|
44
|
+
const columnOrder = pageId ? (tableSettings?.columnOrder ?? []) : [];
|
|
28
45
|
|
|
29
46
|
const handleSave = async () => {
|
|
30
47
|
if (!name.trim()) {
|
|
@@ -45,6 +62,10 @@ export const SaveViewDialog: React.FC<SaveViewDialogProps> = ({
|
|
|
45
62
|
name: name.trim(),
|
|
46
63
|
scope,
|
|
47
64
|
filters,
|
|
65
|
+
columnConfig : {
|
|
66
|
+
columnVisibility,
|
|
67
|
+
columnOrder,
|
|
68
|
+
},
|
|
48
69
|
searchTerm,
|
|
49
70
|
});
|
|
50
71
|
toast.success(`View "${name}" saved successfully`);
|
|
@@ -16,9 +16,15 @@ import { usePage } from '@/vdb/hooks/use-page.js';
|
|
|
16
16
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
17
17
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
18
18
|
import { useMutation } from '@tanstack/react-query';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
AccessorFnColumnDef,
|
|
21
|
+
AccessorKeyColumnDef,
|
|
22
|
+
CellContext,
|
|
23
|
+
createColumnHelper,
|
|
24
|
+
Row,
|
|
25
|
+
} from '@tanstack/react-table';
|
|
20
26
|
import { EllipsisIcon, TrashIcon } from 'lucide-react';
|
|
21
|
-
import { useMemo } from 'react';
|
|
27
|
+
import { memo, useMemo } from 'react';
|
|
22
28
|
import { toast } from 'sonner';
|
|
23
29
|
import {
|
|
24
30
|
AdditionalColumns,
|
|
@@ -116,6 +122,25 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
116
122
|
const customConfig = customizeColumns?.[fieldInfo.name as unknown as AllItemFieldKeys<T>] ?? {};
|
|
117
123
|
const { header, meta, cell: customCell, ...customConfigRest } = customConfig;
|
|
118
124
|
const enableColumnFilter = fieldInfo.isScalar && !facetedFilters?.[fieldInfo.name];
|
|
125
|
+
const displayComponentId =
|
|
126
|
+
pageId && pageBlock?.blockId
|
|
127
|
+
? generateDisplayComponentKey(pageId, pageBlock.blockId, fieldInfo.name)
|
|
128
|
+
: undefined;
|
|
129
|
+
|
|
130
|
+
// If a custom cell function is provided, use it directly (like additionalColumns does).
|
|
131
|
+
// This preserves the same behavior and prevents cell unmounting issues.
|
|
132
|
+
// Only use CellWrapper for columns without custom cells.
|
|
133
|
+
const cellFn =
|
|
134
|
+
typeof customCell === 'function'
|
|
135
|
+
? customCell
|
|
136
|
+
: (cellContext: CellContext<any, any>) => (
|
|
137
|
+
<CellWrapper
|
|
138
|
+
cellContext={cellContext}
|
|
139
|
+
fieldInfo={fieldInfo}
|
|
140
|
+
isCustomField={isCustomField}
|
|
141
|
+
displayComponentId={displayComponentId}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
119
144
|
|
|
120
145
|
return columnHelper.accessor(fieldInfo.name as any, {
|
|
121
146
|
id: fieldInfo.name,
|
|
@@ -126,28 +151,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
126
151
|
// otherwise the TanStack Table with apply an "auto" function which somehow
|
|
127
152
|
// prevents certain filters from working.
|
|
128
153
|
filterFn: 'equalsString',
|
|
129
|
-
cell:
|
|
130
|
-
const { cell, row } = cellContext;
|
|
131
|
-
const cellValue = cell.getValue();
|
|
132
|
-
const value =
|
|
133
|
-
cellValue ??
|
|
134
|
-
(isCustomField ? row.original?.customFields?.[fieldInfo.name] : undefined);
|
|
135
|
-
const displayComponentId =
|
|
136
|
-
pageId && pageBlock?.blockId
|
|
137
|
-
? generateDisplayComponentKey(pageId, pageBlock.blockId, fieldInfo.name)
|
|
138
|
-
: undefined;
|
|
139
|
-
|
|
140
|
-
const CustomDisplayComponent =
|
|
141
|
-
displayComponentId && getDisplayComponent(displayComponentId);
|
|
142
|
-
|
|
143
|
-
if (CustomDisplayComponent) {
|
|
144
|
-
return <CustomDisplayComponent value={value} {...cellContext} />;
|
|
145
|
-
}
|
|
146
|
-
if (typeof customCell === 'function') {
|
|
147
|
-
return customCell(cellContext);
|
|
148
|
-
}
|
|
149
|
-
return <DefaultDisplayComponent value={value} fieldInfo={fieldInfo} />;
|
|
150
|
-
},
|
|
154
|
+
cell: cellFn,
|
|
151
155
|
header: headerContext => {
|
|
152
156
|
return (
|
|
153
157
|
<DataTableColumnHeader headerContext={headerContext} customConfig={customConfig} />
|
|
@@ -291,6 +295,34 @@ function DefaultDisplayComponent({ value, fieldInfo }: { value: any; fieldInfo:
|
|
|
291
295
|
return value;
|
|
292
296
|
}
|
|
293
297
|
|
|
298
|
+
/**
|
|
299
|
+
* A cell wrapper component for columns without custom cell functions.
|
|
300
|
+
* Handles default display logic including custom display components and field-type-based rendering.
|
|
301
|
+
*/
|
|
302
|
+
const CellWrapper = memo(function CellWrapper({
|
|
303
|
+
cellContext,
|
|
304
|
+
fieldInfo,
|
|
305
|
+
isCustomField,
|
|
306
|
+
displayComponentId,
|
|
307
|
+
}: {
|
|
308
|
+
cellContext: CellContext<any, any>;
|
|
309
|
+
fieldInfo: FieldInfo;
|
|
310
|
+
isCustomField: boolean;
|
|
311
|
+
displayComponentId?: string;
|
|
312
|
+
}) {
|
|
313
|
+
const { cell, row } = cellContext;
|
|
314
|
+
const cellValue = cell.getValue();
|
|
315
|
+
const value =
|
|
316
|
+
cellValue ?? (isCustomField ? (row.original as any)?.customFields?.[fieldInfo.name] : undefined);
|
|
317
|
+
|
|
318
|
+
const CustomDisplayComponent = displayComponentId && getDisplayComponent(displayComponentId);
|
|
319
|
+
|
|
320
|
+
if (CustomDisplayComponent) {
|
|
321
|
+
return <CustomDisplayComponent value={value} {...cellContext} />;
|
|
322
|
+
}
|
|
323
|
+
return <DefaultDisplayComponent value={value} fieldInfo={fieldInfo} />;
|
|
324
|
+
});
|
|
325
|
+
|
|
294
326
|
function DeleteMutationRowAction({
|
|
295
327
|
deleteMutation,
|
|
296
328
|
row,
|
|
@@ -44,7 +44,7 @@ export const ViewsSheet: React.FC<ViewsSheetProps> = ({ open, onOpenChange, type
|
|
|
44
44
|
const isGlobal = type === 'global';
|
|
45
45
|
|
|
46
46
|
const handleViewApply = (view: SavedView) => {
|
|
47
|
-
handleApplyView(view.filters, view.searchTerm);
|
|
47
|
+
handleApplyView(view.filters,view.columnConfig, view.searchTerm);
|
|
48
48
|
const viewName = view.name;
|
|
49
49
|
const message = isGlobal ? t`Applied global view "${viewName}"` : t`Applied view "${viewName}"`;
|
|
50
50
|
toast.success(message);
|
|
@@ -138,7 +138,7 @@ export function ChannelSwitcher() {
|
|
|
138
138
|
</div>
|
|
139
139
|
<ChannelCodeLabel code={channel.code} />
|
|
140
140
|
{channel.id === displayChannel?.id && (
|
|
141
|
-
<span className="
|
|
141
|
+
<span className="ms-auto text-xs text-muted-foreground">
|
|
142
142
|
<Trans context="current channel">Current</Trans>
|
|
143
143
|
</span>
|
|
144
144
|
)}
|
|
@@ -146,9 +146,9 @@ export function ChannelSwitcher() {
|
|
|
146
146
|
{/* Show language sub-menu for the current channel */}
|
|
147
147
|
{channel.id === displayChannel?.id && (
|
|
148
148
|
<DropdownMenuSub>
|
|
149
|
-
<DropdownMenuSubTrigger className="gap-2 p-2
|
|
149
|
+
<DropdownMenuSubTrigger className="gap-2 p-2 ps-4">
|
|
150
150
|
<Languages className="w-4 h-4" />
|
|
151
|
-
<div className="flex gap-1
|
|
151
|
+
<div className="flex gap-1 ms-2">
|
|
152
152
|
<span className="text-muted-foreground">Content: </span>
|
|
153
153
|
{formatLanguageName(contentLanguage)}
|
|
154
154
|
</div>
|
|
@@ -167,7 +167,7 @@ export function ChannelSwitcher() {
|
|
|
167
167
|
</div>
|
|
168
168
|
<span>{label}</span>
|
|
169
169
|
{contentLanguage === languageCode && (
|
|
170
|
-
<span className="
|
|
170
|
+
<span className="ms-auto text-xs text-muted-foreground">
|
|
171
171
|
<Trans context="active language">
|
|
172
172
|
Active
|
|
173
173
|
</Trans>
|
|
@@ -200,7 +200,9 @@ export function ChannelSwitcher() {
|
|
|
200
200
|
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
|
|
201
201
|
<Plus className="size-4" />
|
|
202
202
|
</div>
|
|
203
|
-
<div className="text-muted-foreground font-medium">
|
|
203
|
+
<div className="text-muted-foreground font-medium">
|
|
204
|
+
<Trans>Add channel</Trans>
|
|
205
|
+
</div>
|
|
204
206
|
</Link>
|
|
205
207
|
</DropdownMenuItem>
|
|
206
208
|
</DropdownMenuContent>
|
|
@@ -215,7 +215,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
215
215
|
<SidebarMenuButton tooltip={item.title}>
|
|
216
216
|
{item.icon && <item.icon />}
|
|
217
217
|
<span>{i18n.t(item.title)}</span>
|
|
218
|
-
<ChevronRight className="
|
|
218
|
+
<ChevronRight className="ms-auto transition-transform duration-200 rtl:rotate-180 group-data-[state=open]/collapsible:rotate-90" />
|
|
219
219
|
</SidebarMenuButton>
|
|
220
220
|
</CollapsibleTrigger>
|
|
221
221
|
<CollapsibleContent>
|
|
@@ -280,7 +280,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
280
280
|
<SidebarMenuButton tooltip={i18n.t(item.title)}>
|
|
281
281
|
{item.icon && <item.icon />}
|
|
282
282
|
<span>{i18n.t(item.title)}</span>
|
|
283
|
-
<ChevronRight className="
|
|
283
|
+
<ChevronRight className="ms-auto transition-transform duration-200 rtl:rotate-180 group-data-[state=open]/collapsible:rotate-90" />
|
|
284
284
|
</SidebarMenuButton>
|
|
285
285
|
</CollapsibleTrigger>
|
|
286
286
|
<CollapsibleContent>
|
|
@@ -29,7 +29,9 @@ export function Alerts() {
|
|
|
29
29
|
</Button>
|
|
30
30
|
</DropdownMenuTrigger>
|
|
31
31
|
<DropdownMenuContent align="end" className="max-w-[800px] min-w-96">
|
|
32
|
-
<DropdownMenuLabel>
|
|
32
|
+
<DropdownMenuLabel>
|
|
33
|
+
<Trans>Alerts</Trans>
|
|
34
|
+
</DropdownMenuLabel>
|
|
33
35
|
<DropdownMenuSeparator />
|
|
34
36
|
<ScrollArea className="max-h-[500px]">
|
|
35
37
|
{activeCount > 0 ? (
|
|
@@ -23,16 +23,16 @@ interface AssignToChannelBulkActionProps {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function AssignToChannelBulkAction({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
selection,
|
|
27
|
+
table,
|
|
28
|
+
entityType,
|
|
29
|
+
mutationFn,
|
|
30
|
+
requiredPermissions,
|
|
31
|
+
buildInput,
|
|
32
|
+
additionalFields,
|
|
33
|
+
additionalData = {},
|
|
34
|
+
onSuccess,
|
|
35
|
+
}: Readonly<AssignToChannelBulkActionProps>) {
|
|
36
36
|
const { refetchPaginatedList } = usePaginatedList();
|
|
37
37
|
const { channels } = useChannel();
|
|
38
38
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
@@ -70,7 +70,7 @@ export function AssignToChannelDialog({
|
|
|
70
70
|
onOpenChange(false);
|
|
71
71
|
},
|
|
72
72
|
onError: () => {
|
|
73
|
-
toast.error(`Failed to assign ${entityIdsLength} ${entityType} to channel`);
|
|
73
|
+
toast.error(t`Failed to assign ${entityIdsLength} ${entityType} to channel`);
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
76
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { Plus } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
import { ChannelChip } from '@/vdb/components/shared/channel-chip.js';
|
|
7
|
+
import { AssignToChannelDialog } from '@/vdb/components/shared/assign-to-channel-dialog.js';
|
|
8
|
+
import { usePriceFactor } from '@/vdb/components/shared/assign-to-channel-dialog.js';
|
|
9
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
10
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
11
|
+
import { Trans, useLingui } from '@lingui/react/macro';
|
|
12
|
+
import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
|
|
13
|
+
import type { SimpleChannel } from '@/vdb/providers/channel-provider.js';
|
|
14
|
+
|
|
15
|
+
interface AssignedChannelsProps {
|
|
16
|
+
channels: SimpleChannel[];
|
|
17
|
+
entityId: string;
|
|
18
|
+
canUpdate?: boolean;
|
|
19
|
+
assignMutationFn: (variables: any) => Promise<any>;
|
|
20
|
+
removeMutationFn: (variables: any) => Promise<any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AssignedChannels({
|
|
24
|
+
channels,
|
|
25
|
+
entityId,
|
|
26
|
+
canUpdate = true,
|
|
27
|
+
assignMutationFn,
|
|
28
|
+
removeMutationFn,
|
|
29
|
+
}: AssignedChannelsProps) {
|
|
30
|
+
const { t } = useLingui();
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
const { activeChannel, channels: allChannels } = useChannel();
|
|
33
|
+
const [assignDialogOpen, setAssignDialogOpen] = useState(false);
|
|
34
|
+
const { priceFactor, priceFactorField } = usePriceFactor();
|
|
35
|
+
|
|
36
|
+
const { mutate: removeFromChannel, isPending: isRemoving } = useMutation({
|
|
37
|
+
mutationFn: removeMutationFn,
|
|
38
|
+
onSuccess: () => {
|
|
39
|
+
toast.success(t`Successfully removed product from channel`);
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: ['DetailPage', 'product', { id: entityId }] });
|
|
41
|
+
},
|
|
42
|
+
onError: () => {
|
|
43
|
+
toast.error(t`Failed to remove product from channel`);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function onRemoveHandler(channelId: string) {
|
|
48
|
+
if (channelId === activeChannel?.id) {
|
|
49
|
+
toast.error(t`Cannot remove from active channel`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
removeFromChannel({
|
|
53
|
+
input: {
|
|
54
|
+
productIds: [entityId],
|
|
55
|
+
channelId,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleAssignSuccess = () => {
|
|
61
|
+
queryClient.invalidateQueries({ queryKey: ['DetailPage', 'product', { id: entityId }] });
|
|
62
|
+
setAssignDialogOpen(false);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Only show add button if there are more channels available
|
|
66
|
+
const availableChannels = allChannels.filter(ch => !channels.map(c => c.id).includes(ch.id));
|
|
67
|
+
const showAddButton = canUpdate && availableChannels.length > 0;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<div className="flex flex-wrap gap-1 mb-2">
|
|
72
|
+
{channels.filter(c => c.code !== DEFAULT_CHANNEL_CODE).map((channel: SimpleChannel) => {
|
|
73
|
+
return (
|
|
74
|
+
<ChannelChip key={channel.id} channel={channel} removable={canUpdate && channel.id !== activeChannel?.id} onRemove={onRemoveHandler} />
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
{showAddButton && (
|
|
79
|
+
<>
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
variant="outline"
|
|
83
|
+
size="sm"
|
|
84
|
+
onClick={() => setAssignDialogOpen(true)}
|
|
85
|
+
disabled={isRemoving}
|
|
86
|
+
>
|
|
87
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
88
|
+
<Trans>Assign to channel</Trans>
|
|
89
|
+
</Button>
|
|
90
|
+
<AssignToChannelDialog
|
|
91
|
+
entityType="product"
|
|
92
|
+
open={assignDialogOpen}
|
|
93
|
+
onOpenChange={setAssignDialogOpen}
|
|
94
|
+
entityIds={[entityId]}
|
|
95
|
+
mutationFn={assignMutationFn}
|
|
96
|
+
onSuccess={handleAssignSuccess}
|
|
97
|
+
buildInput={(channelId: string) => ({
|
|
98
|
+
productIds: [entityId],
|
|
99
|
+
channelId,
|
|
100
|
+
priceFactor,
|
|
101
|
+
})}
|
|
102
|
+
additionalFields={priceFactorField}
|
|
103
|
+
/>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -18,17 +18,15 @@ interface AssignedFacetValuesProps {
|
|
|
18
18
|
value?: string[] | null;
|
|
19
19
|
facetValues: FacetValue[];
|
|
20
20
|
canUpdate?: boolean;
|
|
21
|
-
onBlur?: () => void;
|
|
22
21
|
onChange?: (value: string[]) => void;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export function AssignedFacetValues({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}: AssignedFacetValuesProps) {
|
|
25
|
+
value = [],
|
|
26
|
+
facetValues,
|
|
27
|
+
canUpdate = true,
|
|
28
|
+
onChange,
|
|
29
|
+
}: Readonly<AssignedFacetValuesProps>) {
|
|
32
30
|
const [knownFacetValues, setKnownFacetValues] = useState<FacetValue[]>(facetValues);
|
|
33
31
|
|
|
34
32
|
function onSelectHandler(facetValue: FacetValue) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import type { SimpleChannel } from '@/vdb/providers/channel-provider.js';
|
|
4
|
+
|
|
5
|
+
interface ChannelChipProps {
|
|
6
|
+
channel: SimpleChannel;
|
|
7
|
+
removable?: boolean;
|
|
8
|
+
onRemove?: (id: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @description
|
|
13
|
+
* A component for displaying a channel as a chip.
|
|
14
|
+
*
|
|
15
|
+
* @docsCategory components
|
|
16
|
+
* @since 3.5.2
|
|
17
|
+
*/
|
|
18
|
+
export function ChannelChip({
|
|
19
|
+
channel,
|
|
20
|
+
removable = true,
|
|
21
|
+
onRemove,
|
|
22
|
+
}: Readonly<ChannelChipProps>) {
|
|
23
|
+
return (
|
|
24
|
+
<Badge
|
|
25
|
+
variant="secondary"
|
|
26
|
+
className="flex items-center gap-2 py-0.5 pl-2 pr-1 h-6 hover:bg-secondary/80"
|
|
27
|
+
>
|
|
28
|
+
<div className="flex items-center gap-1.5">
|
|
29
|
+
<span className="font-medium">{channel.code}</span>
|
|
30
|
+
</div>
|
|
31
|
+
{removable && (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full hover:bg-muted/30 hover:cursor-pointer"
|
|
35
|
+
onClick={() => onRemove?.(channel.id)}
|
|
36
|
+
aria-label={`Remove ${channel.code} from ${channel.token}`}
|
|
37
|
+
>
|
|
38
|
+
<X className="h-3 w-3" />
|
|
39
|
+
</button>
|
|
40
|
+
)}
|
|
41
|
+
</Badge>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -161,6 +161,8 @@ function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuP
|
|
|
161
161
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
const customDropdownMenuSubTriggerClassNames = 'data-[inset]:ps-8';
|
|
165
|
+
const customChevronIconClassNames = 'ms-auto rtl:rotate-180';
|
|
164
166
|
function DropdownMenuSubTrigger({
|
|
165
167
|
className,
|
|
166
168
|
inset,
|
|
@@ -175,12 +177,13 @@ function DropdownMenuSubTrigger({
|
|
|
175
177
|
data-inset={inset}
|
|
176
178
|
className={cn(
|
|
177
179
|
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
|
180
|
+
customDropdownMenuSubTriggerClassNames,
|
|
178
181
|
className,
|
|
179
182
|
)}
|
|
180
183
|
{...props}
|
|
181
184
|
>
|
|
182
185
|
{children}
|
|
183
|
-
<ChevronRightIcon className=
|
|
186
|
+
<ChevronRightIcon className={cn('size-4', customChevronIconClassNames)} />
|
|
184
187
|
</DropdownMenuPrimitive.SubTrigger>
|
|
185
188
|
);
|
|
186
189
|
}
|
|
@@ -443,8 +443,9 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
|
|
|
443
443
|
);
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
const customLogicalPropsClassNames = 'text-start group-has-data-[sidebar=menu-action]/menu-item:pe-8';
|
|
446
447
|
const sidebarMenuButtonVariants = cva(
|
|
447
|
-
|
|
448
|
+
`peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 ${customLogicalPropsClassNames}`,
|
|
448
449
|
{
|
|
449
450
|
variants: {
|
|
450
451
|
variant: {
|
|
@@ -113,6 +113,7 @@ export function useSavedViews() {
|
|
|
113
113
|
scope: input.scope,
|
|
114
114
|
filters: input.filters,
|
|
115
115
|
searchTerm: input.searchTerm,
|
|
116
|
+
columnConfig: input.columnConfig,
|
|
116
117
|
pageId,
|
|
117
118
|
blockId: blockId === 'default' ? undefined : blockId,
|
|
118
119
|
createdAt: new Date().toISOString(),
|
|
@@ -51,7 +51,13 @@ const channelsDocument = graphql(
|
|
|
51
51
|
|
|
52
52
|
// Define the type for a channel
|
|
53
53
|
type ActiveChannel = ResultOf<typeof activeChannelDocument>['activeChannel'];
|
|
54
|
-
type Channel = ResultOf<typeof channelFragment>;
|
|
54
|
+
export type Channel = ResultOf<typeof channelFragment>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Simplified channel type with only the basic fields (id, code, token)
|
|
58
|
+
* Used in components that don't need the full channel information
|
|
59
|
+
*/
|
|
60
|
+
export type SimpleChannel = Pick<Channel, 'id' | 'code' | 'token'>;
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
63
|
* @description
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ColumnFiltersState } from '@tanstack/react-table';
|
|
2
|
+
import { ColumnConfig } from '../components/data-table/data-table-context.js';
|
|
2
3
|
|
|
3
4
|
export interface SavedView {
|
|
4
5
|
id: string;
|
|
5
6
|
name: string;
|
|
6
7
|
scope: 'user' | 'global';
|
|
7
8
|
filters: ColumnFiltersState;
|
|
9
|
+
columnConfig: ColumnConfig;
|
|
8
10
|
searchTerm?: string;
|
|
9
11
|
pageId?: string;
|
|
10
12
|
blockId?: string;
|
|
@@ -28,6 +30,7 @@ export interface SaveViewInput {
|
|
|
28
30
|
name: string;
|
|
29
31
|
scope: 'user' | 'global';
|
|
30
32
|
filters: ColumnFiltersState;
|
|
33
|
+
columnConfig: ColumnConfig;
|
|
31
34
|
searchTerm?: string;
|
|
32
35
|
}
|
|
33
36
|
|