Просмотр исходного кода

feat(dashboard): Collection detail view

Michael Bromley 10 месяцев назад
Родитель
Сommit
77d3386a77
26 измененных файлов с 693 добавлено и 220 удалено
  1. 2 0
      packages/dashboard/package.json
  2. 0 0
      packages/dashboard/src/components/data-display/asset.tsx
  3. 0 0
      packages/dashboard/src/components/data-display/boolean.tsx
  4. 0 0
      packages/dashboard/src/components/data-display/date-time.tsx
  5. 15 0
      packages/dashboard/src/components/data-display/money.tsx
  6. 152 0
      packages/dashboard/src/components/data-input/datetime-input.tsx
  7. 68 0
      packages/dashboard/src/components/data-input/facet-value-input.tsx
  8. 26 20
      packages/dashboard/src/components/data-input/money-input.tsx
  9. 9 8
      packages/dashboard/src/components/shared/configurable-operation-arg-input.tsx
  10. 19 7
      packages/dashboard/src/components/shared/configurable-operation-input.tsx
  11. 10 15
      packages/dashboard/src/components/shared/facet-value-selector.tsx
  12. 11 12
      packages/dashboard/src/components/shared/paginated-list-data-table.tsx
  13. 73 0
      packages/dashboard/src/components/ui/calendar.tsx
  14. 57 83
      packages/dashboard/src/framework/component-registry/component-registry.tsx
  15. 0 35
      packages/dashboard/src/framework/component-registry/delegate.tsx
  16. 58 0
      packages/dashboard/src/framework/component-registry/dynamic-component.tsx
  17. 7 0
      packages/dashboard/src/routes/_authenticated/_collections/collections.graphql.ts
  18. 27 19
      packages/dashboard/src/routes/_authenticated/_collections/collections_.$id.tsx
  19. 127 0
      packages/dashboard/src/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx
  20. 1 1
      packages/dashboard/src/routes/_authenticated/_collections/components/collection-contents-sheet.tsx
  21. 26 15
      packages/dashboard/src/routes/_authenticated/_collections/components/collection-filters-select.tsx
  22. 1 1
      packages/dashboard/src/routes/_authenticated/_facets/components/facet-values-sheet.tsx
  23. 1 1
      packages/dashboard/src/routes/_authenticated/_product-variants/components/variant-price-detail.tsx
  24. 1 1
      packages/dashboard/src/routes/_authenticated/_product-variants/product-variants.tsx
  25. 1 1
      packages/dashboard/src/routes/_authenticated/_product-variants/product-variants_.$id.tsx
  26. 1 1
      packages/dashboard/src/routes/_authenticated/_products/components/product-variants-table.tsx

+ 2 - 0
packages/dashboard/package.json

@@ -51,12 +51,14 @@
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
     "cmdk": "^1.0.0",
+    "date-fns": "^4.1.0",
     "gql.tada": "^1.8.10",
     "graphql": "~16.10.0",
     "graphql-request": "^7.1.2",
     "lucide-react": "^0.475.0",
     "next-themes": "^0.4.6",
     "react": "^19.0.0",
+    "react-day-picker": "^8.10.1",
     "react-dom": "^19.0.0",
     "react-hook-form": "^7.54.2",
     "sonner": "^2.0.1",

+ 0 - 0
packages/dashboard/src/components/data-type-components/asset.tsx → packages/dashboard/src/components/data-display/asset.tsx


+ 0 - 0
packages/dashboard/src/components/data-type-components/boolean.tsx → packages/dashboard/src/components/data-display/boolean.tsx


+ 0 - 0
packages/dashboard/src/components/data-type-components/date-time.tsx → packages/dashboard/src/components/data-display/date-time.tsx


+ 15 - 0
packages/dashboard/src/components/data-display/money.tsx

@@ -0,0 +1,15 @@
+import { useLocalFormat } from "@/hooks/use-local-format.js";
+
+// Original component
+function MoneyInternal({ value, currency }: { value: number, currency: string }) {
+    const { formatCurrency } = useLocalFormat();
+    return formatCurrency(value, currency);
+}
+
+// Wrapper that makes it compatible with DataDisplayComponent
+export function Money(props: { value: any; [key: string]: any }) {
+    const { value, ...rest } = props;
+    const currency = rest.currency || 'USD'; // Default currency if none provided
+    return MoneyInternal({ value, currency });
+}
+

+ 152 - 0
packages/dashboard/src/components/data-input/datetime-input.tsx

@@ -0,0 +1,152 @@
+"use client";
+ 
+import * as React from "react";
+import { format } from "date-fns";
+ 
+import { cn } from "@/lib/utils.js";
+import { Button } from "@/components/ui/button.js";
+import { Calendar } from "@/components/ui/calendar.js";
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from "@/components/ui/popover.js";
+import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area.js";
+import { CalendarClock } from "lucide-react";
+
+export interface DateTimeInputProps {
+  value: Date;
+  onChange: (value: Date) => void;
+}
+ 
+export function DateTimeInput(props: DateTimeInputProps) {
+  const [date, setDate] = React.useState<Date>(props.value);
+  const [isOpen, setIsOpen] = React.useState(false);
+ 
+  const hours = Array.from({ length: 12 }, (_, i) => i + 1);
+  const handleDateSelect = (selectedDate: Date | undefined) => {
+    if (selectedDate) {
+      setDate(selectedDate);
+      props.onChange(selectedDate);
+    }
+  };
+ 
+  const handleTimeChange = (
+    type: "hour" | "minute" | "ampm",
+    value: string
+  ) => {
+    if (date) {
+      const newDate = new Date(date);
+      if (type === "hour") {
+        newDate.setHours(
+          (parseInt(value) % 12) + (newDate.getHours() >= 12 ? 12 : 0)
+        );
+      } else if (type === "minute") {
+        newDate.setMinutes(parseInt(value));
+      } else if (type === "ampm") {
+        const currentHours = newDate.getHours();
+        newDate.setHours(
+          value === "PM" ? currentHours + 12 : currentHours - 12
+        );
+      }
+      setDate(newDate);
+      props.onChange(newDate);
+    }
+  };
+ 
+  return (
+    <Popover open={isOpen} onOpenChange={setIsOpen}>
+      <PopoverTrigger asChild>
+        <Button
+          variant="outline"
+          className={cn(
+            "w-full justify-start text-left font-normal",
+            !date && "text-muted-foreground"
+          )}
+        >
+          <CalendarClock className="mr-2 h-4 w-4" />
+          {date ? (
+            format(date, "MM/dd/yyyy hh:mm aa")
+          ) : (
+            <span>MM/DD/YYYY hh:mm aa</span>
+          )}
+        </Button>
+      </PopoverTrigger>
+      <PopoverContent className="w-auto p-0">
+        <div className="sm:flex">
+          <Calendar
+            mode="single"
+            selected={date}
+            onSelect={handleDateSelect}
+            initialFocus
+          />
+          <div className="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
+            <ScrollArea className="w-64 sm:w-auto">
+              <div className="flex sm:flex-col p-2">
+                {hours.reverse().map((hour) => (
+                  <Button
+                    key={hour}
+                    size="icon"
+                    variant={
+                      date && date.getHours() % 12 === hour % 12
+                        ? "default"
+                        : "ghost"
+                    }
+                    className="sm:w-full shrink-0 aspect-square"
+                    onClick={() => handleTimeChange("hour", hour.toString())}
+                  >
+                    {hour}
+                  </Button>
+                ))}
+              </div>
+              <ScrollBar orientation="horizontal" className="sm:hidden" />
+            </ScrollArea>
+            <ScrollArea className="w-64 sm:w-auto">
+              <div className="flex sm:flex-col p-2">
+                {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (
+                  <Button
+                    key={minute}
+                    size="icon"
+                    variant={
+                      date && date.getMinutes() === minute
+                        ? "default"
+                        : "ghost"
+                    }
+                    className="sm:w-full shrink-0 aspect-square"
+                    onClick={() =>
+                      handleTimeChange("minute", minute.toString())
+                    }
+                  >
+                    {minute}
+                  </Button>
+                ))}
+              </div>
+              <ScrollBar orientation="horizontal" className="sm:hidden" />
+            </ScrollArea>
+            <ScrollArea className="">
+              <div className="flex sm:flex-col p-2">
+                {["AM", "PM"].map((ampm) => (
+                  <Button
+                    key={ampm}
+                    size="icon"
+                    variant={
+                      date &&
+                      ((ampm === "AM" && date.getHours() < 12) ||
+                        (ampm === "PM" && date.getHours() >= 12))
+                        ? "default"
+                        : "ghost"
+                    }
+                    className="sm:w-full shrink-0 aspect-square"
+                    onClick={() => handleTimeChange("ampm", ampm)}
+                  >
+                    {ampm}
+                  </Button>
+                ))}
+              </div>
+            </ScrollArea>
+          </div>
+        </div>
+      </PopoverContent>
+    </Popover>
+  );
+}

+ 68 - 0
packages/dashboard/src/components/data-input/facet-value-input.tsx

@@ -0,0 +1,68 @@
+import { graphql } from "@/graphql/graphql.js";
+import { FacetValue, FacetValueSelector } from "../shared/facet-value-selector.js";
+import { useQuery } from "@tanstack/react-query";
+import { api } from "@/graphql/api.js";
+import { FacetValueChip } from "../shared/facet-value-chip.js";
+
+const facetValuesDocument = graphql(`
+    query FacetValues($options: FacetValueListOptions) {
+        facetValues(options: $options) {
+            items {
+                id
+                name
+                code
+                facet {
+                    id
+                    name
+                    code
+                }
+            }
+        }
+    }`
+);
+
+export interface FacetValueInputProps {
+    value: string;
+    onChange: (value: string) => void;
+    readOnly?: boolean;
+}
+
+export function FacetValueInput(props: FacetValueInputProps) {
+    const ids = decodeIds(props.value);
+    const { data } = useQuery({
+        queryKey: ['facetValues', ids],
+        queryFn: () => api.query(facetValuesDocument, {
+            options: {
+                filter: {
+                    id: { in: ids }
+                }
+            }
+        })
+    });
+
+    const onValueSelectHandler = (value: FacetValue) => {
+        const newIds = new Set([...ids, value.id]);
+        props.onChange(JSON.stringify(Array.from(newIds)));
+    }
+
+    const onValueRemoveHandler = (id: string) => {
+        const newIds = new Set(ids.filter(existingId => existingId !== id));
+        props.onChange(JSON.stringify(Array.from(newIds)));
+    }
+
+    return (<div>
+        
+        <div className="flex flex-wrap gap-2 mb-2">
+            {data?.facetValues.items.map(item => <FacetValueChip key={item.id} facetValue={item} onRemove={() => onValueRemoveHandler(item.id)} />)}
+        </div>
+        <FacetValueSelector onValueSelect={onValueSelectHandler} disabled={props.readOnly} />
+    </div>);
+}
+
+function decodeIds(idsString: string): string[] {
+    try {
+        return JSON.parse(idsString);
+    } catch (error) {
+        return [];
+    }
+}

+ 26 - 20
packages/dashboard/src/components/data-type-components/money.tsx → packages/dashboard/src/components/data-input/money-input.tsx

@@ -1,15 +1,21 @@
-import { useLocalFormat } from "@/hooks/use-local-format.js";
-import { Input } from "../ui/input.js";
-import { useUserSettings } from "@/hooks/use-user-settings.js";
-import { useMemo, useState, useEffect } from "react";
+import { Input } from '../ui/input.js';
+import { useUserSettings } from '@/hooks/use-user-settings.js';
+import { useMemo, useState, useEffect } from 'react';
+import { useLocalFormat } from '@/hooks/use-local-format.js';
 
-export function Money({ value, currency }: { value: number, currency: string }) {
-    const { formatCurrency } = useLocalFormat();
-    return formatCurrency(value, currency);
-}
-
-export function MoneyInput({ value, currency, onChange }: { value: number, currency: string, onChange: (value: number) => void }) {
-    const { settings: { displayLanguage, displayLocale } } = useUserSettings();
+// Original component
+function MoneyInputInternal({
+    value,
+    currency,
+    onChange,
+}: {
+    value: number;
+    currency: string;
+    onChange: (value: number) => void;
+}) {
+    const {
+        settings: { displayLanguage, displayLocale },
+    } = useUserSettings();
     const { toMajorUnits, toMinorUnits } = useLocalFormat();
     const [displayValue, setDisplayValue] = useState(toMajorUnits(value).toFixed(2));
 
@@ -50,11 +56,7 @@ export function MoneyInput({ value, currency, onChange }: { value: number, curre
 
     return (
         <div className="relative flex items-center">
-            {shouldPrefix && (
-                <span className="absolute left-3 text-muted-foreground">
-                    {currencySymbol}
-                </span>
-            )}
+            {shouldPrefix && <span className="absolute left-3 text-muted-foreground">{currencySymbol}</span>}
             <Input
                 type="text"
                 value={displayValue}
@@ -96,16 +98,20 @@ export function MoneyInput({ value, currency, onChange }: { value: number, curre
                         setDisplayValue(newValue.toFixed(2));
                     }
                 }}
-                className={shouldPrefix ? "pl-8" : "pr-8"}
+                className={shouldPrefix ? 'pl-8' : 'pr-8'}
                 step="0.01"
                 min="0"
             />
             {!shouldPrefix && (
-                <span className="absolute right-3 text-muted-foreground">
-                    {currencySymbol}
-                </span>
+                <span className="absolute right-3 text-muted-foreground">{currencySymbol}</span>
             )}
         </div>
     );
 }
 
+// Wrapper that makes it compatible with DataInputComponent
+export function MoneyInput(props: { value: any; onChange: (value: any) => void; [key: string]: any }) {
+    const { value, onChange, ...rest } = props;
+    const currency = rest.currency || 'USD'; // Default currency if none provided
+    return <MoneyInputInternal value={value} currency={currency} onChange={onChange} />;
+}

+ 9 - 8
packages/dashboard/src/components/shared/configurable-operation-arg-input.tsx

@@ -1,24 +1,25 @@
+import { InputComponent } from "@/framework/component-registry/dynamic-component.js";
 import { ConfigurableOperationDefFragment } from "@/graphql/fragments.js";
-import { Input } from "../ui/input.js";
 import { ConfigArgType } from "@vendure/core";
-import { Checkbox } from "../ui/checkbox.js";
-import { FacetValueSelector } from "./facet-value-selector.js";
+import { FacetValueInput } from "../data-input/facet-value-input.js";
+
 export interface ConfigurableOperationArgInputProps {
     definition: ConfigurableOperationDefFragment['args'][number];
+    readOnly?: boolean;
     value: string;
     onChange: (value: any) => void;
 }
 
-export function ConfigurableOperationArgInput({ definition, value, onChange }: ConfigurableOperationArgInputProps) {
+export function ConfigurableOperationArgInput({ definition, value, onChange, readOnly }: ConfigurableOperationArgInputProps) {
     if ((definition.ui as any)?.component === 'facet-value-form-input') {
-        return <FacetValueSelector onValueSelect={value => onChange(value)} />
+        return <FacetValueInput value={value} onChange={onChange} readOnly={readOnly} />
     }
     switch (definition.type as ConfigArgType) {
         case 'boolean':
-            return <Checkbox value={value} onCheckedChange={state => onChange(state)} />;
+            return <InputComponent id="vendure:checkboxInput" value={value} onChange={value => onChange(value)} readOnly={readOnly} />;
         case 'string':
-            return <Input value={value} onChange={e => onChange(e.target.value)} />;
+            return <InputComponent id="vendure:textInput" value={value} onChange={value => onChange(value)} readOnly={readOnly} />;
         default:
-            return <Input value={value} onChange={e => onChange(e.target.value)} />;
+            return <InputComponent id="vendure:textInput" value={value} onChange={value => onChange(value)} readOnly={readOnly} />;
     }
 }

+ 19 - 7
packages/dashboard/src/components/shared/configurable-operation-input.tsx

@@ -5,6 +5,8 @@ import { Form, FormControl, FormField, FormItem, FormLabel } from '../ui/form.js
 import { Input } from '../ui/input.js';
 import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
 import { ConfigurableOperationArgInput } from './configurable-operation-arg-input.js';
+import { Button } from '../ui/button.js';
+import { Trash } from 'lucide-react';
 
 export interface ConfigurableOperationInputProps {
     operationDefinition: ConfigurableOperationDefFragment;
@@ -14,6 +16,7 @@ export interface ConfigurableOperationInputProps {
     hideDescription?: boolean;
     value: ConfigurableOperationInputType;
     onChange: (val: ConfigurableOperationInputType) => void;
+    onRemove?: () => void;
 }
 
 export function ConfigurableOperationInput({
@@ -24,6 +27,7 @@ export function ConfigurableOperationInput({
     hideDescription,
     value,
     onChange,
+    onRemove,
 }: ConfigurableOperationInputProps) {
     const form = useForm({
         defaultValues: {
@@ -44,15 +48,23 @@ export function ConfigurableOperationInput({
         onChange(newVal);
     };
 
+
     return (
         <Form {...form}>
             <div className="space-y-4">
-                {!hideDescription && (
-                    <div className="font-medium">
-                        {' '}
-                        {interpolateDescription(operationDefinition, value.arguments)}
-                    </div>
-                )}
+                <div className="flex flex-row justify-between">
+                    {!hideDescription && (
+                        <div className="font-medium">
+                            {' '}
+                            {interpolateDescription(operationDefinition, value.arguments)}
+                        </div>
+                    )}
+                    {removable !== false && (
+                        <Button variant="outline" size="icon" onClick={onRemove}>
+                            <Trash />
+                        </Button>
+                    )}
+                </div>
                 <div className="grid grid-cols-2 gap-4">
                     {operationDefinition.args.map(arg => {
                         const argValue = value.arguments.find(a => a.name === arg.name)?.value || '';
@@ -62,7 +74,7 @@ export function ConfigurableOperationInput({
                                 name={`args.${arg.name}`}
                                 render={() => (
                                 <FormItem>
-                                    <FormLabel>{arg.name}</FormLabel>
+                                    <FormLabel>{arg.label || arg.name}</FormLabel>
                                     <FormControl>
                                         <ConfigurableOperationArgInput
                                             definition={arg}

+ 10 - 15
packages/dashboard/src/components/shared/facet-value-selector.tsx

@@ -1,27 +1,22 @@
-import React, { useState } from 'react';
-import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
-import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command.js';
 import { Button } from '@/components/ui/button.js';
-import { Trans } from '@lingui/react/macro';
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command.js';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover.js';
+import { api } from '@/graphql/api.js';
 import { graphql } from '@/graphql/graphql.js';
 import { useDebounce } from '@/hooks/use-debounce.js';
-import { api } from '@/graphql/api.js';
-import { cn } from '@/lib/utils.js';
-import { Check, ChevronRight, Plus, Loader2 } from 'lucide-react';
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover.js';
+import { Trans } from '@lingui/react/macro';
+import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
+import { ChevronRight, Loader2, Plus } from 'lucide-react';
+import React, { useState } from 'react';
 
-interface FacetValue {
+export interface FacetValue {
     id: string;
     name: string;
     code: string;
-    facet: {
-        id: string;
-        name: string;
-        code: string;
-    };
+    facet: Facet;
 }
 
-interface Facet {
+export interface Facet {
     id: string;
     name: string;
     code: string;

+ 11 - 12
packages/dashboard/src/components/shared/paginated-list-data-table.tsx

@@ -1,17 +1,17 @@
 import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header.js';
 import { DataTable } from '@/components/data-table/data-table.js';
-import { useComponentRegistry } from '@/framework/component-registry/component-registry.js';
 import {
     FieldInfo,
-    getQueryName,
-    getTypeFieldInfo,
     getObjectPathToPaginatedList,
+    getTypeFieldInfo
 } from '@/framework/document-introspection/get-document-structure.js';
 import { useListQueryFields } from '@/framework/document-introspection/hooks.js';
 import { api } from '@/graphql/api.js';
-import { useDebounce } from 'use-debounce';
 import { useQueryClient } from '@tanstack/react-query';
+import { useDebounce } from 'use-debounce';
 
+import { DisplayComponent } from '@/framework/component-registry/dynamic-component.js';
+import { ResultOf } from '@/graphql/graphql.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { useQuery } from '@tanstack/react-query';
 import {
@@ -22,9 +22,7 @@ import {
     Table,
 } from '@tanstack/react-table';
 import { AccessorKeyColumnDef, ColumnDef } from '@tanstack/table-core';
-import { graphql, ResultOf } from '@/graphql/graphql.js';
 import React, { useMemo } from 'react';
-import { Delegate } from '@/framework/component-registry/delegate.js';
 
 // Type that identifies a paginated list structure (has items array and totalItems)
 type IsPaginatedList<T> = T extends { items: any[]; totalItems: number } ? true : false;
@@ -167,7 +165,7 @@ export interface PaginatedListDataTableProps<
     V extends ListQueryOptionsShape,
 > {
     listQuery: T;
-    pathToListQuery?: PaginatedListPaths<T>;
+    transformQueryKey?: (queryKey: any[]) => any[];
     transformVariables?: (variables: V) => V;
     customizeColumns?: CustomizeColumnConfig<T>;
     additionalColumns?: ColumnDef<any>[];
@@ -188,6 +186,7 @@ export function PaginatedListDataTable<
     V extends ListQueryOptionsShape = {},
 >({
     listQuery,
+    transformQueryKey,
     transformVariables,
     customizeColumns,
     additionalColumns,
@@ -201,7 +200,6 @@ export function PaginatedListDataTable<
     onSortChange,
     onFilterChange,
 }: PaginatedListDataTableProps<T, U, V>) {
-    const { getComponent } = useComponentRegistry();
     const [searchTerm, setSearchTerm] = React.useState<string>('');
     const [debouncedSearchTerm] = useDebounce(searchTerm, 500);
     const queryClient = useQueryClient();
@@ -220,7 +218,8 @@ export function PaginatedListDataTable<
         ? { _and: columnFilters.map(f => ({ [f.id]: f.value })) }
         : undefined;
 
-    const queryKey = ['PaginatedListDataTable', listQuery, page, itemsPerPage, sorting, filter];
+    const defaultQueryKey = ['PaginatedListDataTable', listQuery, page, itemsPerPage, sorting, filter];
+    const queryKey = transformQueryKey ? transformQueryKey(defaultQueryKey) : defaultQueryKey;
 
     function refetchPaginatedList() {
         queryClient.invalidateQueries({ queryKey });
@@ -290,13 +289,13 @@ export function PaginatedListDataTable<
                         (fieldInfo.type === 'DateTime' && typeof value === 'string') ||
                         value instanceof Date
                     ) {
-                        return <Delegate component="dateTime.display" value={value} />;
+                        return <DisplayComponent id="vendure:dateTime" value={value} />;
                     }
                     if (fieldInfo.type === 'Boolean') {
-                        return <Delegate component="boolean.display" value={value} />;
+                        return <DisplayComponent id="vendure:boolean" value={value} />;
                     }
                     if (fieldInfo.type === 'Asset') {
-                        return <Delegate component="asset.display" value={value} />;
+                        return <DisplayComponent id="vendure:asset" value={value} />;
                     }
                     if (value !== null && typeof value === 'object') {
                         return JSON.stringify(value);

+ 73 - 0
packages/dashboard/src/components/ui/calendar.tsx

@@ -0,0 +1,73 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function Calendar({
+  className,
+  classNames,
+  showOutsideDays = true,
+  ...props
+}: React.ComponentProps<typeof DayPicker>) {
+  return (
+    <DayPicker
+      showOutsideDays={showOutsideDays}
+      className={cn("p-3", className)}
+      classNames={{
+        months: "flex flex-col sm:flex-row gap-2",
+        month: "flex flex-col gap-4",
+        caption: "flex justify-center pt-1 relative items-center w-full",
+        caption_label: "text-sm font-medium",
+        nav: "flex items-center gap-1",
+        nav_button: cn(
+          buttonVariants({ variant: "outline" }),
+          "size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
+        ),
+        nav_button_previous: "absolute left-1",
+        nav_button_next: "absolute right-1",
+        table: "w-full border-collapse space-x-1",
+        head_row: "flex",
+        head_cell:
+          "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
+        row: "flex w-full mt-2",
+        cell: cn(
+          "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
+          props.mode === "range"
+            ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+            : "[&:has([aria-selected])]:rounded-md"
+        ),
+        day: cn(
+          buttonVariants({ variant: "ghost" }),
+          "size-8 p-0 font-normal aria-selected:opacity-100"
+        ),
+        day_range_start:
+          "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
+        day_range_end:
+          "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
+        day_selected:
+          "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+        day_today: "bg-accent text-accent-foreground",
+        day_outside:
+          "day-outside text-muted-foreground aria-selected:text-muted-foreground",
+        day_disabled: "text-muted-foreground opacity-50",
+        day_range_middle:
+          "aria-selected:bg-accent aria-selected:text-accent-foreground",
+        day_hidden: "invisible",
+        ...classNames,
+      }}
+      components={{
+        IconLeft: ({ className, ...props }) => (
+          <ChevronLeft className={cn("size-4", className)} {...props} />
+        ),
+        IconRight: ({ className, ...props }) => (
+          <ChevronRight className={cn("size-4", className)} {...props} />
+        ),
+      }}
+      {...props}
+    />
+  )
+}
+
+export { Calendar }

+ 57 - 83
packages/dashboard/src/framework/component-registry/component-registry.tsx

@@ -1,95 +1,69 @@
-import { AssetThumbnail } from '@/components/data-type-components/asset.js';
-import { BooleanDisplayCheckbox } from '@/components/data-type-components/boolean.js';
-import { DateTime } from '@/components/data-type-components/date-time.js';
-import { Money } from '@/components/data-type-components/money.js';
+import { AssetThumbnail } from '@/components/data-display/asset.js';
+import { BooleanDisplayCheckbox } from '@/components/data-display/boolean.js';
+import { DateTime } from '@/components/data-display/date-time.js';
+import { Money } from '@/components/data-display/money.js';
+import { DateTimeInput } from '@/components/data-input/datetime-input.js';
+import { FacetValueInput } from '@/components/data-input/facet-value-input.js';
+import { MoneyInput } from '@/components/data-input/money-input.js';
+import { Checkbox } from '@/components/ui/checkbox.js';
+import { Input } from '@/components/ui/input.js';
+import * as React from 'react';
 
-export interface ComponentRegistryEntry {
-    component: React.ComponentType<any>;
+export interface ComponentRegistryEntry<Props extends Record<string, any>> {
+    component: React.ComponentType<Props>;
 }
 
+// Basic component types
+export type DataDisplayComponent = React.ComponentType<{ value: any; [key: string]: any }>;
+export type DataInputComponent = React.ComponentType<{ value: any; onChange: (value: any) => void; [key: string]: any }>;
+
+// Simple component registry
 interface ComponentRegistry {
-    type: {
-        [dataType: string]: {
-            display: {
-                [id: string]: ComponentRegistryEntry;
-            };
-        };
-    };
+    dataDisplay: Record<string, DataDisplayComponent>;
+    dataInput: Record<string, DataInputComponent>;
 }
 
-export const COMPONENT_REGISTRY = {
-    type: {
-        boolean: {
-            display: {
-                default: {
-                    component: BooleanDisplayCheckbox,
-                },
-            },
-        },
-        dateTime: {
-            display: {
-                default: {
-                    component: DateTime,
-                },
-            },
-        },
-        asset: {
-            display: {
-                default: {
-                    component: AssetThumbnail,
-                },
-            },
-        },
-        money: {
-            display: {
-                default: {
-                    component: Money,
-                },
-            },
-        },
+export const COMPONENT_REGISTRY: ComponentRegistry = {
+    dataDisplay: {
+        'vendure:boolean': BooleanDisplayCheckbox,
+        'vendure:dateTime': DateTime,
+        'vendure:asset': AssetThumbnail,
+        'vendure:money': Money,
     },
-} satisfies ComponentRegistry;
-
-export type TypeRegistry = (typeof COMPONENT_REGISTRY)['type'];
-export type TypeRegistryTypes = keyof TypeRegistry;
-export type TypeRegistryCategories<T extends TypeRegistryTypes> = {
-    [K in keyof TypeRegistry[T]]: K;
-}[keyof TypeRegistry[T]];
-export type TypeRegistryComponents<
-    T extends TypeRegistryTypes,
-    U extends TypeRegistryCategories<T> = TypeRegistryCategories<T>,
-> = {
-    [K in keyof TypeRegistry[T][U]]: K;
-}[keyof TypeRegistry[T][U]];
-export type NonDefaultComponents<
-    T extends TypeRegistryTypes,
-    U extends TypeRegistryCategories<T> = TypeRegistryCategories<T>,
-> = {
-    [K in TypeRegistryComponents<T, U>]: K extends 'default' ? never : `${T}.${U & string}.${K & string}`;
-}[keyof TypeRegistry[T][U]];
-
-export type ComponentTypePath<
-    T extends TypeRegistryTypes,
-    U extends TypeRegistryCategories<T> = TypeRegistryCategories<T>,
-> = `${T}.${U & string}` | `${NonDefaultComponents<T, U>}`;
-
-export function useComponentRegistry() {
-    function getComponent<
-        T extends TypeRegistryTypes,
-        U extends TypeRegistryCategories<T> = TypeRegistryCategories<T>,
-    >(path: ComponentTypePath<T>): React.ComponentType<{ value: any }> {
-        const [type, category, componentKey] = path.split('.') as [T, U, string];
-        const availableComponents = COMPONENT_REGISTRY.type[type][category] as any;
-        const componentEntry = availableComponents[componentKey ?? 'default'] as
-            | ComponentRegistryEntry
-            | undefined;
-        if (!componentEntry) {
-            throw new Error(`Component not found for path: ${path}`);
-        }
-        return componentEntry.component;
+    dataInput: {
+        'vendure:moneyInput': MoneyInput,
+        'vendure:textInput': (props) => <Input {...props} onChange={e => props.onChange(e.target.value)} />,
+        'vendure:numberInput': (props) => <Input {...props} onChange={e => props.onChange(e.target.value)} type="number" />,
+        'vendure:dateTimeInput': DateTimeInput,
+        'vendure:checkboxInput': (props) => <Checkbox {...props} checked={props.value === 'true' || props.value === true}  onCheckedChange={value => props.onChange(value)} />,
+        'vendure:facetValueInput': FacetValueInput,
     }
+};
 
+// Simplified implementation - replace with actual implementation
+export function useComponentRegistry() {
     return {
-        getComponent,
+        getDisplayComponent: (id: string): DataDisplayComponent | undefined => {
+            // This is a placeholder implementation
+            return COMPONENT_REGISTRY.dataDisplay[id];
+        },
+        getInputComponent: (id: string): DataInputComponent | undefined => {
+            // This is a placeholder implementation
+            return COMPONENT_REGISTRY.dataInput[id];
+        },
     };
 }
+
+export function registerInputComponent(id: string,  component: DataInputComponent) {
+    if (COMPONENT_REGISTRY.dataInput[id]) {
+        throw new Error(`Input component with id ${id} already registered`);
+    }
+    COMPONENT_REGISTRY.dataInput[id] = component;
+}
+
+export function registerDisplayComponent(id: string, component: DataDisplayComponent) {
+    if (COMPONENT_REGISTRY.dataDisplay[id]) {
+        throw new Error(`Display component with id ${id} already registered`);
+    }
+    COMPONENT_REGISTRY.dataDisplay[id] = component;
+}

+ 0 - 35
packages/dashboard/src/framework/component-registry/delegate.tsx

@@ -1,35 +0,0 @@
-import React from "react";
-import { ComponentTypePath, TypeRegistryCategories, TypeRegistryTypes, useComponentRegistry } from "./component-registry.js";
-
-export type DelegateProps<
-    T extends TypeRegistryTypes,
-    U extends TypeRegistryCategories<T> = TypeRegistryCategories<T>,
-> = {
-    component: ComponentTypePath<T, TypeRegistryCategories<T>>;
-    value: any;
-    // rest of the props are passed to the component
-    [key: string]: any;
-}
-
-/**
- * @description
- * This component is used to delegate the rendering of a component to the component registry.
- * 
- * @example
- * ```ts
- * <Delegate component="money.display.default" value={100} />
- * ```
- * 
- * @returns 
- */
-export function Delegate<
-    T extends TypeRegistryTypes,
-    U extends TypeRegistryCategories<T> = TypeRegistryCategories<T>,
->(props: DelegateProps<T, U>): React.ReactNode {
-    const { getComponent } = useComponentRegistry();
-    const Component = getComponent(props.component);
-    const { value, ...rest } = props;
-    return <Component value={value} {...rest} />;
-}
-
-

+ 58 - 0
packages/dashboard/src/framework/component-registry/dynamic-component.tsx

@@ -0,0 +1,58 @@
+import React from "react";
+import { COMPONENT_REGISTRY, useComponentRegistry } from "./component-registry.js";
+
+export type DisplayComponentProps<
+    T extends keyof (typeof COMPONENT_REGISTRY)['dataDisplay'] | string,
+> = {
+    id: T;
+    value: any;
+    // rest of the props are passed to the component
+    [key: string]: any;
+}
+
+
+export type InputComponentProps<
+    T extends keyof (typeof COMPONENT_REGISTRY)['dataInput'] | string,
+> = {
+    id: T;
+    value: any;
+    // rest of the props are passed to the component
+    [key: string]: any;
+}
+
+/**
+ * @description
+ * This component is used to delegate the rendering of a component to the component registry.
+ * 
+ * @example
+ * ```ts
+ * <Delegate component="money.display.default" value={100} />
+ * ```
+ * 
+ * @returns 
+ */
+export function DisplayComponent<
+    T extends keyof (typeof COMPONENT_REGISTRY)['dataDisplay'] | string,
+>(props: DisplayComponentProps<T>): React.ReactNode {   
+    const { getDisplayComponent } = useComponentRegistry();
+    const Component = getDisplayComponent(props.id);
+    if (!Component) {
+        throw new Error(`Component with id ${props.id} not found`);
+    }
+    const { value, ...rest } = props;
+    return <Component value={value} {...rest} />;
+}
+
+export function InputComponent<
+    T extends keyof (typeof COMPONENT_REGISTRY)['dataInput'] | string,
+>(props: InputComponentProps<T>): React.ReactNode {
+    const { getInputComponent } = useComponentRegistry();
+    const Component = getInputComponent(props.id);
+    if (!Component) {
+        throw new Error(`Component with id ${props.id} not found`);
+    }
+    const { value, onChange, ...rest } = props;
+    return <Component value={value} onChange={onChange} {...rest} />;
+}       
+
+

+ 7 - 0
packages/dashboard/src/routes/_authenticated/_collections/collections.graphql.ts

@@ -1,9 +1,11 @@
+import { api } from '@/graphql/api.js';
 import {
     assetFragment,
     configurableOperationDefFragment,
     configurableOperationFragment,
 } from '@/graphql/fragments.js';
 import { graphql } from '@/graphql/graphql.js';
+import { queryOptions } from '@tanstack/react-query';
 
 export const collectionListDocument = graphql(
     `
@@ -111,3 +113,8 @@ export const getCollectionFiltersDocument = graphql(
     `,
     [configurableOperationDefFragment],
 );
+
+export const getCollectionFiltersQueryOptions = queryOptions({
+    queryKey: ['getCollectionFilters'],
+    queryFn: () => api.query(getCollectionFiltersDocument),
+});

+ 27 - 19
packages/dashboard/src/routes/_authenticated/_collections/collections_.$id.tsx

@@ -1,4 +1,5 @@
 import { ContentLanguageSelector } from '@/components/layout/content-language-selector.js';
+import { EntityAssets } from '@/components/shared/entity-assets.js';
 import { ErrorPage } from '@/components/shared/error-page.js';
 import { PermissionGuard } from '@/components/shared/permission-guard.js';
 import { TranslatableFormField } from '@/components/shared/translatable-form-field.js';
@@ -14,6 +15,7 @@ import {
 } from '@/components/ui/form.js';
 import { Input } from '@/components/ui/input.js';
 import { Switch } from '@/components/ui/switch.js';
+import { Textarea } from '@/components/ui/textarea.js';
 import { NEW_ENTITY_PATH } from '@/constants.js';
 import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
 import {
@@ -31,15 +33,11 @@ import { toast } from 'sonner';
 import {
     collectionDetailDocument,
     createCollectionDocument,
-    getCollectionFiltersDocument,
-    updateCollectionDocument,
+    updateCollectionDocument
 } from './collections.graphql.js';
 import { CollectionContentsTable } from './components/collection-contents-table.js';
-import { Textarea } from '@/components/ui/textarea.js';
-import { EntityAssets } from '@/components/shared/entity-assets.js';
-import { api } from '@/graphql/api.js';
-import { useQuery } from '@tanstack/react-query';
 import { CollectionFiltersSelect } from './components/collection-filters-select.js';
+import { CollectionContentsPreviewTable } from './components/collection-contents-preview-table.js';
 
 export const Route = createFileRoute('/_authenticated/_collections/collections_/$id')({
     component: CollectionDetailPage,
@@ -74,6 +72,12 @@ export function CollectionDetailPage() {
         queryDocument: addCustomFields(collectionDetailDocument),
         entityField: 'collection',
         createDocument: createCollectionDocument,
+        transformCreateInput: values => {
+            return {
+                ...values,
+                filters: values.filters.filter(f => f.code !== ''),
+            };
+        },
         updateDocument: updateCollectionDocument,
         setValuesForUpdate: entity => {
             return {
@@ -97,12 +101,6 @@ export function CollectionDetailPage() {
                 customFields: entity.customFields,
             };
         },
-        transformCreateInput: values => {
-            return {
-                ...values,
-                values: [],
-            };
-        },
         params: { id: params.id },
         onSuccess: async data => {
             toast(i18n.t('Successfully updated collection'), {
@@ -121,6 +119,13 @@ export function CollectionDetailPage() {
         },
     });
 
+
+    const shouldPreviewContents =
+        form.getFieldState('inheritFilters').isDirty || form.getFieldState('filters').isDirty;
+
+    const currentFiltersValue = form.watch('filters');
+    const currentInheritFiltersValue = form.watch('inheritFilters');
+
     return (
         <Page>
             <PageTitle>{creatingNewEntity ? <Trans>New collection</Trans> : (entity?.name ?? '')}</PageTitle>
@@ -238,10 +243,7 @@ export function CollectionDetailPage() {
                                 control={form.control}
                                 name="filters"
                                 render={({ field }) => (
-                                    <CollectionFiltersSelect
-                                        value={field.value}
-                                        onChange={field.onChange}
-                                    />
+                                    <CollectionFiltersSelect value={field.value} onChange={field.onChange} />
                                 )}
                             />
                         </PageBlock>
@@ -272,11 +274,17 @@ export function CollectionDetailPage() {
                                 <FormMessage />
                             </FormItem>
                         </PageBlock>
-                        {!creatingNewEntity && (
                             <PageBlock column="main" title={<Trans>Facet values</Trans>}>
-                                <CollectionContentsTable collectionId={entity?.id} />
+                                {shouldPreviewContents || creatingNewEntity ? (
+                                    <CollectionContentsPreviewTable
+                                        parentId={entity?.parent?.id}
+                                        filters={currentFiltersValue}
+                                        inheritFilters={currentInheritFiltersValue}
+                                    />
+                                ) : (
+                                    <CollectionContentsTable collectionId={entity?.id} />
+                                )}
                             </PageBlock>
-                        )}
                     </PageLayout>
                 </form>
             </Form>

+ 127 - 0
packages/dashboard/src/routes/_authenticated/_collections/components/collection-contents-preview-table.tsx

@@ -0,0 +1,127 @@
+import { PaginatedListDataTable } from '@/components/shared/paginated-list-data-table.js';
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert.js';
+import { Button } from '@/components/ui/button.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { graphql } from '@/graphql/graphql.js';
+import { useQuery } from '@tanstack/react-query';
+import { Link } from '@tanstack/react-router';
+import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
+import { PreviewCollectionVariantsInput } from '@vendure/common/lib/generated-types';
+import { Eye } from 'lucide-react';
+import { useState } from 'react';
+import { getCollectionFiltersQueryOptions } from '../collections.graphql.js';
+
+export const previewCollectionContentsDocument = graphql(`
+    query PreviewCollectionContents(
+        $input: PreviewCollectionVariantsInput!
+        $options: ProductVariantListOptions
+    ) {
+        previewCollectionVariants(input: $input, options: $options) {
+            items {
+                id
+                createdAt
+                updatedAt
+                productId
+                name
+                sku
+            }
+            totalItems
+        }
+    }
+`);
+
+export type CollectionContentsPreviewTableProps = PreviewCollectionVariantsInput;
+
+export function CollectionContentsPreviewTable({
+    parentId,
+    filters: collectionFilters,
+    inheritFilters,
+}: CollectionContentsPreviewTableProps) {
+    const [sorting, setSorting] = useState<SortingState>([]);
+    const [page, setPage] = useState(1);
+    const [pageSize, setPageSize] = useState(10);
+    const [filters, setFilters] = useState<ColumnFiltersState>([]);
+    const { data: filterDefs } = useQuery(getCollectionFiltersQueryOptions);
+
+    const effectiveFilters = collectionFilters.filter(f => {
+        // ensure that every filter has all required arguments
+        const filterDef = filterDefs?.collectionFilters.find(fd => fd.code === f.code);
+        if (!filterDef) {
+            return false;
+        }
+        for (const arg of filterDef.args) {
+            const argPair = f.arguments.find(a => a.name === arg.name);
+            const argValue = argPair?.value ?? arg.defaultValue;
+            if (arg.required && argValue == null) {
+                return false;
+            }
+        }
+        return true;
+    });
+
+    return (
+        <div>
+            <Alert>
+                <Eye className="h-4 w-4" />
+                <AlertTitle>Preview</AlertTitle>
+                <AlertDescription>
+                    This is a preview of the collection contents based on the current
+                    filter settings. Once you save the collection, the contents will be
+                    updated to reflect the new filter settings.
+                </AlertDescription>
+            </Alert>
+
+            <PaginatedListDataTable
+                listQuery={addCustomFields(previewCollectionContentsDocument)}
+                transformQueryKey={queryKey => {
+                    return [...queryKey, JSON.stringify(effectiveFilters), inheritFilters];
+                }}
+                transformVariables={variables => {
+                    return {
+                        options: variables.options,
+                        input: {
+                            parentId,
+                            filters: effectiveFilters,
+                            inheritFilters,
+                        },
+                    };
+                }}
+                customizeColumns={{
+                    name: {
+                        header: 'Variant name',
+                        cell: ({ row }) => {
+                            return (
+                                <Button asChild variant="ghost">
+                                    <Link to={`../../product-variants/${row.original.id}`}>
+                                        {row.original.name}{' '}
+                                    </Link>
+                                </Button>
+                            );
+                        },
+                    },
+                }}
+                page={page}
+                itemsPerPage={pageSize}
+                sorting={sorting}
+                columnFilters={filters}
+                onPageChange={(_, page, perPage) => {
+                    setPage(page);
+                    setPageSize(perPage);
+                }}
+                onSortChange={(_, sorting) => {
+                    setSorting(sorting);
+                }}
+                onFilterChange={(_, filters) => {
+                    setFilters(filters);
+                }}
+                onSearchTermChange={searchTerm => {
+                    return {
+                        name: {
+                            contains: searchTerm,
+                        },
+                    };
+                }}
+            />
+        </div>
+    );
+}

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_collections/components/collection-contents-sheet.tsx

@@ -20,7 +20,7 @@ export interface CollectionContentsSheetProps {
 export function CollectionContentsSheet({ collectionId, collectionName, children }: CollectionContentsSheetProps) {
     return (
         <Sheet>
-            <SheetTrigger>
+            <SheetTrigger asChild>
                 <Button variant="outline" size="sm" className="flex items-center gap-2">
                     {children}
                     <PanelLeftOpen className="w-4 h-4" />

+ 26 - 15
packages/dashboard/src/routes/_authenticated/_collections/components/collection-filters-select.tsx

@@ -1,3 +1,4 @@
+import { ConfigurableOperationInput } from '@/components/shared/configurable-operation-input.js';
 import { Button } from '@/components/ui/button.js';
 import {
     DropdownMenu,
@@ -5,15 +6,13 @@ import {
     DropdownMenuItem,
     DropdownMenuTrigger,
 } from '@/components/ui/dropdown-menu.js';
-import { api } from '@/graphql/api.js';
-import { ConfigurableOperationDefFragment, ConfigurableOperationFragment } from '@/graphql/fragments.js';
-import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
+import { Separator } from '@/components/ui/separator.js';
+import { ConfigurableOperationDefFragment } from '@/graphql/fragments.js';
 import { Trans } from '@lingui/react/macro';
 import { useQuery } from '@tanstack/react-query';
+import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
 import { Plus } from 'lucide-react';
-import { getCollectionFiltersDocument } from '../collections.graphql.js';
-import { useServerConfig } from '@/hooks/use-server-config.js';
-import { ConfigurableOperationInput } from '@/components/shared/configurable-operation-input.js';
+import { getCollectionFiltersQueryOptions } from '../collections.graphql.js';
 
 export interface CollectionFiltersSelectProps {
     value: ConfigurableOperationInputType[];
@@ -21,19 +20,25 @@ export interface CollectionFiltersSelectProps {
 }
 
 export function CollectionFiltersSelect({ value, onChange }: CollectionFiltersSelectProps) {
-    const serverConfig = useServerConfig();
-    const { data: filtersData } = useQuery({
-        queryKey: ['collectionFilters'],
-        queryFn: () => api.query(getCollectionFiltersDocument),
-    });
+    const { data: filtersData } = useQuery(getCollectionFiltersQueryOptions);
 
     const filters = filtersData?.collectionFilters;
 
     const onFilterSelected = (filter: ConfigurableOperationDefFragment) => {
-        if (value.find(f => f.code === filter.code)) {
+        const filterDef = filters?.find(f => f.code === filter.code);
+        if (!filterDef) {
             return;
         }
-        onChange([...value, { code: filter.code, arguments: [] }]);
+        onChange([
+            ...value,
+            {
+                code: filter.code,
+                arguments: filterDef.args.map(arg => ({
+                    name: arg.name,
+                    value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
+                })),
+            },
+        ]);
     };
 
     const onOperationValueChange = (
@@ -43,8 +48,12 @@ export function CollectionFiltersSelect({ value, onChange }: CollectionFiltersSe
         onChange(value.map(f => (f.code === filter.code ? newVal : f)));
     };
 
+    const onOperationRemove = (index: number) => {
+        onChange(value.filter((_, i) => i !== index));
+    };
+
     return (
-        <div className="flex flex-col gap-2">
+        <div className="flex flex-col gap-2 mt-4">
             {(value ?? []).map((filter, index) => {
                 const filterDef = filters?.find(f => f.code === filter.code);
                 if (!filterDef) {
@@ -56,7 +65,9 @@ export function CollectionFiltersSelect({ value, onChange }: CollectionFiltersSe
                             operationDefinition={filterDef}
                             value={filter}
                             onChange={value => onOperationValueChange(filter, value)}
+                            onRemove={() => onOperationRemove(index)}
                         />
+                        <Separator className="my-2" />
                     </div>
                 );
             })}
@@ -69,7 +80,7 @@ export function CollectionFiltersSelect({ value, onChange }: CollectionFiltersSe
                 </DropdownMenuTrigger>
                 <DropdownMenuContent className="w-96">
                     {filters?.map(filter => (
-                        <DropdownMenuItem key={filter.code} onClick={() => onFilterSelected?.(filter)}>
+                        <DropdownMenuItem key={filter.code} onClick={() => onFilterSelected(filter)}>
                             {filter.description}
                         </DropdownMenuItem>
                     ))}

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_facets/components/facet-values-sheet.tsx

@@ -20,7 +20,7 @@ export interface FacetValuesSheetProps {
 export function FacetValuesSheet({ facetName, facetId, children }: FacetValuesSheetProps) {
     return (
         <Sheet>
-            <SheetTrigger>
+            <SheetTrigger asChild>
                 <Button variant="outline" size="sm" className="flex items-center gap-2">
                     {children}
                     <PanelLeftOpen className="w-4 h-4" />

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_product-variants/components/variant-price-detail.tsx

@@ -5,7 +5,7 @@ import { Trans } from '@lingui/react/macro';
 import { graphql } from '@/graphql/graphql.js';
 import { api } from '@/graphql/api.js';
 import { useChannel } from '@/hooks/use-channel.js';
-import { Money } from '@/components/data-type-components/money.js';
+import { Money } from '@/components/data-display/money.js';
 
 const taxRatesDocument = graphql(`
     query TaxRates($options: TaxRateListOptions) {

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_product-variants/product-variants.tsx

@@ -4,7 +4,7 @@ import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { productVariantListDocument } from './product-variants.graphql.js';
 import { Button } from '@/components/ui/button.js';
-import { Money } from '@/components/data-type-components/money.js';
+import { Money } from '@/components/data-display/money.js';
 import { useLocalFormat } from '@/hooks/use-local-format.js';
 
 export const Route = createFileRoute('/_authenticated/_product-variants/product-variants')({

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_product-variants/product-variants_.$id.tsx

@@ -1,4 +1,4 @@
-import { MoneyInput } from '@/components/data-type-components/money.js';
+import { MoneyInput } from '@/components/data-display/money.js';
 import { ContentLanguageSelector } from '@/components/layout/content-language-selector.js';
 import { EntityAssets } from '@/components/shared/entity-assets.js';
 import { ErrorPage } from '@/components/shared/error-page.js';

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_products/components/product-variants-table.tsx

@@ -2,7 +2,7 @@ import { PaginatedListDataTable } from "@/components/shared/paginated-list-data-
 import { productVariantListDocument } from "../products.graphql.js";
 import { useState } from "react";
 import { ColumnFiltersState, SortingState } from "@tanstack/react-table";
-import { Money } from "@/components/data-type-components/money.js";
+import { Money } from "@/components/data-display/money.js";
 import { useLocalFormat } from "@/hooks/use-local-format.js";
 import { Link } from "@tanstack/react-router";
 import { Button } from "@/components/ui/button.js";