@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.
Files changed (54) hide show
  1. package/dist/plugin/constants.js +21 -2
  2. package/package.json +3 -3
  3. package/src/app/routeTree.gen.ts +1135 -1072
  4. package/src/app/routes/_authenticated/_facets/components/facet-values-sheet.tsx +4 -1
  5. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +1 -1
  6. package/src/app/routes/_authenticated/_facets/facets.tsx +22 -38
  7. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -1
  8. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +0 -1
  9. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +2 -2
  10. package/src/app/routes/_authenticated/_products/products.graphql.ts +5 -0
  11. package/src/app/routes/_authenticated/_products/products_.$id.tsx +24 -1
  12. package/src/app/routes/_authenticated/_system/components/payload-dialog.tsx +9 -2
  13. package/src/app/routes/_authenticated/_system/job-queue.tsx +11 -2
  14. package/src/app/routes/_authenticated/_zones/zones.tsx +1 -0
  15. package/src/i18n/locales/ar.po +177 -141
  16. package/src/i18n/locales/cs.po +177 -141
  17. package/src/i18n/locales/de.po +177 -141
  18. package/src/i18n/locales/en.po +177 -141
  19. package/src/i18n/locales/es.po +177 -141
  20. package/src/i18n/locales/fa.po +177 -141
  21. package/src/i18n/locales/fr.po +177 -141
  22. package/src/i18n/locales/he.po +177 -141
  23. package/src/i18n/locales/hr.po +177 -141
  24. package/src/i18n/locales/it.po +177 -141
  25. package/src/i18n/locales/ja.po +177 -141
  26. package/src/i18n/locales/nb.po +177 -141
  27. package/src/i18n/locales/ne.po +177 -141
  28. package/src/i18n/locales/pl.po +177 -141
  29. package/src/i18n/locales/pt_BR.po +177 -141
  30. package/src/i18n/locales/pt_PT.po +177 -141
  31. package/src/i18n/locales/ru.po +177 -141
  32. package/src/i18n/locales/sv.po +177 -141
  33. package/src/i18n/locales/tr.po +177 -141
  34. package/src/i18n/locales/uk.po +177 -141
  35. package/src/i18n/locales/zh_Hans.po +177 -141
  36. package/src/i18n/locales/zh_Hant.po +177 -141
  37. package/src/lib/components/data-table/data-table-context.tsx +18 -3
  38. package/src/lib/components/data-table/global-views-bar.tsx +1 -1
  39. package/src/lib/components/data-table/save-view-dialog.tsx +21 -0
  40. package/src/lib/components/data-table/use-generated-columns.tsx +56 -24
  41. package/src/lib/components/data-table/views-sheet.tsx +1 -1
  42. package/src/lib/components/layout/channel-switcher.tsx +7 -5
  43. package/src/lib/components/layout/nav-main.tsx +2 -2
  44. package/src/lib/components/shared/alerts.tsx +3 -1
  45. package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +10 -10
  46. package/src/lib/components/shared/assign-to-channel-dialog.tsx +1 -1
  47. package/src/lib/components/shared/assigned-channels.tsx +108 -0
  48. package/src/lib/components/shared/assigned-facet-values.tsx +5 -7
  49. package/src/lib/components/shared/channel-chip.tsx +43 -0
  50. package/src/lib/components/ui/dropdown-menu.tsx +4 -1
  51. package/src/lib/components/ui/sidebar.tsx +2 -1
  52. package/src/lib/hooks/use-saved-views.ts +1 -0
  53. package/src/lib/providers/channel-provider.tsx +7 -1
  54. 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 handleApplyView = (filters: ColumnFiltersState, viewSearchTerm?: string) => {
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 { AccessorFnColumnDef, AccessorKeyColumnDef, createColumnHelper, Row } from '@tanstack/react-table';
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: cellContext => {
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="ml-auto text-xs text-muted-foreground">
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 pl-4">
149
+ <DropdownMenuSubTrigger className="gap-2 p-2 ps-4">
150
150
  <Languages className="w-4 h-4" />
151
- <div className="flex gap-1 ml-2">
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="ml-auto text-xs text-muted-foreground">
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">Add channel</div>
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="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
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="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
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>Alerts</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
- selection,
27
- table,
28
- entityType,
29
- mutationFn,
30
- requiredPermissions,
31
- buildInput,
32
- additionalFields,
33
- additionalData = {},
34
- onSuccess,
35
- }: Readonly<AssignToChannelBulkActionProps>) {
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
- value = [],
27
- facetValues,
28
- canUpdate = true,
29
- onBlur,
30
- onChange,
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="ml-auto size-4" />
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
- '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',
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