Răsfoiți Sursa

feat(dashboard): Channel detail view

Michael Bromley 10 luni în urmă
părinte
comite
2f53546b46

+ 11 - 0
packages/dashboard/src/components/layout/generated-breadcrumbs.tsx

@@ -36,6 +36,11 @@ export function GeneratedBreadcrumbs() {
                             label: breadcrumb,
                             path: pathname,
                         };
+                    } else if (React.isValidElement(breadcrumb)) {
+                        return {
+                            label: breadcrumb,
+                            path: pathname,
+                        };
                     } else {
                         return {
                             label: breadcrumb.label,
@@ -50,6 +55,12 @@ export function GeneratedBreadcrumbs() {
                     path: pathname,
                 };
             }
+            if (React.isValidElement(loaderData.breadcrumb)) {
+                return {
+                    label: loaderData.breadcrumb,
+                    path: pathname,
+                };
+            }
         })
         .flat();
     return (

+ 10 - 334
packages/dashboard/src/components/layout/language-dialog.tsx

@@ -1,343 +1,19 @@
+import { CurrencyCode } from '@/constants.js';
+import { useLocalFormat } from '@/hooks/use-local-format.js';
+import { useUserSettings } from '@/hooks/use-user-settings.js';
 import { Trans } from '@lingui/react/macro';
+import { useState } from 'react';
+import { uiConfig } from 'virtual:vendure-ui-config';
+import { Button } from '../ui/button.js';
 import {
-    Dialog,
-    DialogTitle,
+    DialogClose,
     DialogContent,
-    DialogHeader,
-    DialogTrigger,
     DialogFooter,
-    DialogClose,
+    DialogHeader,
+    DialogTitle
 } from '../ui/dialog.js';
-import { Button } from '../ui/button.js';
-import { FormItem, FormLabel } from '../ui/form.js';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
 import { Label } from '../ui/label.js';
-import { uiConfig } from 'virtual:vendure-ui-config';
-import { useUserSettings } from '@/hooks/use-user-settings.js';
-import { useLocalFormat } from '@/hooks/use-local-format.js';
-import { useState } from 'react';
-
-/**
- * This is copied from the generated types from @vendure/common/lib/generated-types.d.ts
- * It is used to provide a list of available currency codes for the user to select from.
- * esbuild currently does not support import enums.
- */
-enum CurrencyCode {
-    /** United Arab Emirates dirham */
-    AED = 'AED',
-    /** Afghan afghani */
-    AFN = 'AFN',
-    /** Albanian lek */
-    ALL = 'ALL',
-    /** Armenian dram */
-    AMD = 'AMD',
-    /** Netherlands Antillean guilder */
-    ANG = 'ANG',
-    /** Angolan kwanza */
-    AOA = 'AOA',
-    /** Argentine peso */
-    ARS = 'ARS',
-    /** Australian dollar */
-    AUD = 'AUD',
-    /** Aruban florin */
-    AWG = 'AWG',
-    /** Azerbaijani manat */
-    AZN = 'AZN',
-    /** Bosnia and Herzegovina convertible mark */
-    BAM = 'BAM',
-    /** Barbados dollar */
-    BBD = 'BBD',
-    /** Bangladeshi taka */
-    BDT = 'BDT',
-    /** Bulgarian lev */
-    BGN = 'BGN',
-    /** Bahraini dinar */
-    BHD = 'BHD',
-    /** Burundian franc */
-    BIF = 'BIF',
-    /** Bermudian dollar */
-    BMD = 'BMD',
-    /** Brunei dollar */
-    BND = 'BND',
-    /** Boliviano */
-    BOB = 'BOB',
-    /** Brazilian real */
-    BRL = 'BRL',
-    /** Bahamian dollar */
-    BSD = 'BSD',
-    /** Bhutanese ngultrum */
-    BTN = 'BTN',
-    /** Botswana pula */
-    BWP = 'BWP',
-    /** Belarusian ruble */
-    BYN = 'BYN',
-    /** Belize dollar */
-    BZD = 'BZD',
-    /** Canadian dollar */
-    CAD = 'CAD',
-    /** Congolese franc */
-    CDF = 'CDF',
-    /** Swiss franc */
-    CHF = 'CHF',
-    /** Chilean peso */
-    CLP = 'CLP',
-    /** Renminbi (Chinese) yuan */
-    CNY = 'CNY',
-    /** Colombian peso */
-    COP = 'COP',
-    /** Costa Rican colon */
-    CRC = 'CRC',
-    /** Cuban convertible peso */
-    CUC = 'CUC',
-    /** Cuban peso */
-    CUP = 'CUP',
-    /** Cape Verde escudo */
-    CVE = 'CVE',
-    /** Czech koruna */
-    CZK = 'CZK',
-    /** Djiboutian franc */
-    DJF = 'DJF',
-    /** Danish krone */
-    DKK = 'DKK',
-    /** Dominican peso */
-    DOP = 'DOP',
-    /** Algerian dinar */
-    DZD = 'DZD',
-    /** Egyptian pound */
-    EGP = 'EGP',
-    /** Eritrean nakfa */
-    ERN = 'ERN',
-    /** Ethiopian birr */
-    ETB = 'ETB',
-    /** Euro */
-    EUR = 'EUR',
-    /** Fiji dollar */
-    FJD = 'FJD',
-    /** Falkland Islands pound */
-    FKP = 'FKP',
-    /** Pound sterling */
-    GBP = 'GBP',
-    /** Georgian lari */
-    GEL = 'GEL',
-    /** Ghanaian cedi */
-    GHS = 'GHS',
-    /** Gibraltar pound */
-    GIP = 'GIP',
-    /** Gambian dalasi */
-    GMD = 'GMD',
-    /** Guinean franc */
-    GNF = 'GNF',
-    /** Guatemalan quetzal */
-    GTQ = 'GTQ',
-    /** Guyanese dollar */
-    GYD = 'GYD',
-    /** Hong Kong dollar */
-    HKD = 'HKD',
-    /** Honduran lempira */
-    HNL = 'HNL',
-    /** Croatian kuna */
-    HRK = 'HRK',
-    /** Haitian gourde */
-    HTG = 'HTG',
-    /** Hungarian forint */
-    HUF = 'HUF',
-    /** Indonesian rupiah */
-    IDR = 'IDR',
-    /** Israeli new shekel */
-    ILS = 'ILS',
-    /** Indian rupee */
-    INR = 'INR',
-    /** Iraqi dinar */
-    IQD = 'IQD',
-    /** Iranian rial */
-    IRR = 'IRR',
-    /** Icelandic króna */
-    ISK = 'ISK',
-    /** Jamaican dollar */
-    JMD = 'JMD',
-    /** Jordanian dinar */
-    JOD = 'JOD',
-    /** Japanese yen */
-    JPY = 'JPY',
-    /** Kenyan shilling */
-    KES = 'KES',
-    /** Kyrgyzstani som */
-    KGS = 'KGS',
-    /** Cambodian riel */
-    KHR = 'KHR',
-    /** Comoro franc */
-    KMF = 'KMF',
-    /** North Korean won */
-    KPW = 'KPW',
-    /** South Korean won */
-    KRW = 'KRW',
-    /** Kuwaiti dinar */
-    KWD = 'KWD',
-    /** Cayman Islands dollar */
-    KYD = 'KYD',
-    /** Kazakhstani tenge */
-    KZT = 'KZT',
-    /** Lao kip */
-    LAK = 'LAK',
-    /** Lebanese pound */
-    LBP = 'LBP',
-    /** Sri Lankan rupee */
-    LKR = 'LKR',
-    /** Liberian dollar */
-    LRD = 'LRD',
-    /** Lesotho loti */
-    LSL = 'LSL',
-    /** Libyan dinar */
-    LYD = 'LYD',
-    /** Moroccan dirham */
-    MAD = 'MAD',
-    /** Moldovan leu */
-    MDL = 'MDL',
-    /** Malagasy ariary */
-    MGA = 'MGA',
-    /** Macedonian denar */
-    MKD = 'MKD',
-    /** Myanmar kyat */
-    MMK = 'MMK',
-    /** Mongolian tögrög */
-    MNT = 'MNT',
-    /** Macanese pataca */
-    MOP = 'MOP',
-    /** Mauritanian ouguiya */
-    MRU = 'MRU',
-    /** Mauritian rupee */
-    MUR = 'MUR',
-    /** Maldivian rufiyaa */
-    MVR = 'MVR',
-    /** Malawian kwacha */
-    MWK = 'MWK',
-    /** Mexican peso */
-    MXN = 'MXN',
-    /** Malaysian ringgit */
-    MYR = 'MYR',
-    /** Mozambican metical */
-    MZN = 'MZN',
-    /** Namibian dollar */
-    NAD = 'NAD',
-    /** Nigerian naira */
-    NGN = 'NGN',
-    /** Nicaraguan córdoba */
-    NIO = 'NIO',
-    /** Norwegian krone */
-    NOK = 'NOK',
-    /** Nepalese rupee */
-    NPR = 'NPR',
-    /** New Zealand dollar */
-    NZD = 'NZD',
-    /** Omani rial */
-    OMR = 'OMR',
-    /** Panamanian balboa */
-    PAB = 'PAB',
-    /** Peruvian sol */
-    PEN = 'PEN',
-    /** Papua New Guinean kina */
-    PGK = 'PGK',
-    /** Philippine peso */
-    PHP = 'PHP',
-    /** Pakistani rupee */
-    PKR = 'PKR',
-    /** Polish złoty */
-    PLN = 'PLN',
-    /** Paraguayan guaraní */
-    PYG = 'PYG',
-    /** Qatari riyal */
-    QAR = 'QAR',
-    /** Romanian leu */
-    RON = 'RON',
-    /** Serbian dinar */
-    RSD = 'RSD',
-    /** Russian ruble */
-    RUB = 'RUB',
-    /** Rwandan franc */
-    RWF = 'RWF',
-    /** Saudi riyal */
-    SAR = 'SAR',
-    /** Solomon Islands dollar */
-    SBD = 'SBD',
-    /** Seychelles rupee */
-    SCR = 'SCR',
-    /** Sudanese pound */
-    SDG = 'SDG',
-    /** Swedish krona/kronor */
-    SEK = 'SEK',
-    /** Singapore dollar */
-    SGD = 'SGD',
-    /** Saint Helena pound */
-    SHP = 'SHP',
-    /** Sierra Leonean leone */
-    SLL = 'SLL',
-    /** Somali shilling */
-    SOS = 'SOS',
-    /** Surinamese dollar */
-    SRD = 'SRD',
-    /** South Sudanese pound */
-    SSP = 'SSP',
-    /** São Tomé and Príncipe dobra */
-    STN = 'STN',
-    /** Salvadoran colón */
-    SVC = 'SVC',
-    /** Syrian pound */
-    SYP = 'SYP',
-    /** Swazi lilangeni */
-    SZL = 'SZL',
-    /** Thai baht */
-    THB = 'THB',
-    /** Tajikistani somoni */
-    TJS = 'TJS',
-    /** Turkmenistan manat */
-    TMT = 'TMT',
-    /** Tunisian dinar */
-    TND = 'TND',
-    /** Tongan paʻanga */
-    TOP = 'TOP',
-    /** Turkish lira */
-    TRY = 'TRY',
-    /** Trinidad and Tobago dollar */
-    TTD = 'TTD',
-    /** New Taiwan dollar */
-    TWD = 'TWD',
-    /** Tanzanian shilling */
-    TZS = 'TZS',
-    /** Ukrainian hryvnia */
-    UAH = 'UAH',
-    /** Ugandan shilling */
-    UGX = 'UGX',
-    /** United States dollar */
-    USD = 'USD',
-    /** Uruguayan peso */
-    UYU = 'UYU',
-    /** Uzbekistan som */
-    UZS = 'UZS',
-    /** Venezuelan bolívar soberano */
-    VES = 'VES',
-    /** Vietnamese đồng */
-    VND = 'VND',
-    /** Vanuatu vatu */
-    VUV = 'VUV',
-    /** Samoan tala */
-    WST = 'WST',
-    /** CFA franc BEAC */
-    XAF = 'XAF',
-    /** East Caribbean dollar */
-    XCD = 'XCD',
-    /** CFA franc BCEAO */
-    XOF = 'XOF',
-    /** CFP franc (franc Pacifique) */
-    XPF = 'XPF',
-    /** Yemeni rial */
-    YER = 'YER',
-    /** South African rand */
-    ZAR = 'ZAR',
-    /** Zambian kwacha */
-    ZMW = 'ZMW',
-    /** Zimbabwean dollar */
-    ZWL = 'ZWL',
-}
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select.js';
 
 export function LanguageDialog() {
     const { availableLocales, availableLanguages } = uiConfig;

+ 137 - 0
packages/dashboard/src/components/shared/currency-selector.tsx

@@ -0,0 +1,137 @@
+import { api } from '@/graphql/api.js';
+import { graphql } from '@/graphql/graphql.js';
+import { useQuery } from '@tanstack/react-query';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover.js';
+import { useLocalFormat } from '@/hooks/use-local-format.js';
+import { Badge } from '@/components/ui/badge.js';
+import { X, ChevronDown } from 'lucide-react';
+import { cn } from '@/lib/utils.js';
+import { Button } from '@/components/ui/button.js';
+import { CurrencyCode } from '@/constants.js';
+import { useState } from 'react';
+
+export interface CurrencySelectorProps<T extends boolean> {    
+    value: T extends true ? string[] : string;
+    onChange: (value: T extends true ? string[] : string) => void;
+    multiple?: T;
+    availableCurrencyCodes?: string[];
+}
+
+export function CurrencySelector<T extends boolean>(props: CurrencySelectorProps<T>) {
+    const { formatCurrencyName } = useLocalFormat();
+    const { value, onChange, multiple, availableCurrencyCodes } = props;
+    const [search, setSearch] = useState("");
+
+    const filteredCurrencies = availableCurrencyCodes ?? Object.values(CurrencyCode).filter(currencyCode =>
+        formatCurrencyName(currencyCode).toLowerCase().includes(search.toLowerCase())
+    );
+
+    const handleSelect = (selectedValue: string) => {
+        if (multiple) {
+            const currentValue = value as string[];
+            const newValue = currentValue.includes(selectedValue)
+                ? currentValue.filter(v => v !== selectedValue)
+                : [...currentValue, selectedValue];
+            onChange(newValue as T extends true ? string[] : string);
+        } else {
+            onChange(selectedValue as T extends true ? string[] : string);
+        }
+    };
+
+    const handleRemove = (languageToRemove: string) => {
+        if (multiple) {
+            const currentValue = value as string[];
+            onChange(currentValue.filter(v => v !== languageToRemove) as T extends true ? string[] : string);
+        }
+    };
+
+    const renderTrigger = () => {
+        if (multiple) {
+            const selectedCurrencies = value as string[];
+            return (
+                <Button
+                    variant="outline"
+                    role="combobox"
+                    className={cn(
+                        "w-full justify-between",
+                        "min-h-[2.5rem] h-auto",
+                        "flex flex-wrap gap-1 p-1"
+                    )}
+                >
+                    <div className="flex flex-wrap gap-1">
+                        {selectedCurrencies.length > 0 ? (
+                            selectedCurrencies.map(currencyCode => (
+                                <Badge
+                                    key={currencyCode}
+                                    variant="secondary"
+                                    className="flex items-center gap-1"
+                                >
+                                    {formatCurrencyName(currencyCode)}
+                                    <button
+                                        onClick={(e) => {
+                                            e.stopPropagation();
+                                            handleRemove(currencyCode);
+                                        }}
+                                        className="ml-1 hover:text-destructive"
+                                    >
+                                        <X className="h-3 w-3" />
+                                    </button>
+                                </Badge>
+                            ))
+                        ) : (
+                            <span className="text-muted-foreground">Select currencies</span>
+                        )}
+                    </div>
+                    <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
+                </Button>
+            );
+        }
+        return (
+            <Button
+                variant="outline"
+                role="combobox"
+                className="w-full justify-between"
+            >
+                {value ? formatCurrencyName(value as string) : "Select a currency"}
+                <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
+            </Button>
+        );
+    };
+
+    return (
+        <Popover>
+            <PopoverTrigger asChild>
+                {renderTrigger()}
+            </PopoverTrigger>
+            <PopoverContent 
+                className="w-[200px] p-0" 
+                side="bottom" 
+                align="start"
+            >
+                <div className="p-2">
+                    <input
+                        type="text"
+                        placeholder="Search currencies..."
+                        value={search}
+                        onChange={(e) => setSearch(e.target.value)}
+                        className="w-full px-2 py-1 text-sm border rounded"
+                    />
+                </div>
+                <div className="max-h-[300px] overflow-auto">
+                    {filteredCurrencies.map(currencyCode => (
+                        <button
+                            key={currencyCode}
+                            onClick={() => handleSelect(currencyCode)}
+                            className={cn(
+                                "w-full px-2 py-1.5 text-sm text-left hover:bg-accent",
+                                multiple && (value as string[]).includes(currencyCode) && "bg-accent"
+                            )}
+                        >
+                            {formatCurrencyName(currencyCode)}
+                        </button>
+                    ))}
+                </div>
+            </PopoverContent>
+        </Popover>
+    );
+}

+ 1 - 1
packages/dashboard/src/components/shared/detail-page-button.tsx

@@ -2,7 +2,7 @@ import { Link } from '@tanstack/react-router';
 import { Button } from '../ui/button.js';
 import { SquareArrowOutUpRightIcon } from 'lucide-react';
 
-export function DetailPageButton({ id, label }: { id: string; label: string }) {
+export function DetailPageButton({ id, label }: { id: string; label: string | React.ReactNode }) {
     return (
         <Button asChild variant="ghost">
             <Link to={`./${id}`}>

+ 120 - 0
packages/dashboard/src/components/shared/language-selector.tsx

@@ -0,0 +1,120 @@
+import { api } from '@/graphql/api.js';
+import { graphql } from '@/graphql/graphql.js';
+import { useQuery } from '@tanstack/react-query';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover.js';
+import { useLocalFormat } from '@/hooks/use-local-format.js';
+import { Badge } from '@/components/ui/badge.js';
+import { X, ChevronDown } from 'lucide-react';
+import { cn } from '@/lib/utils.js';
+import { Button } from '@/components/ui/button.js';
+
+const availableGlobalLanguages = graphql(`
+    query AvailableGlobalLanguages {
+        globalSettings {
+            availableLanguages
+        }
+    }
+`);
+
+export interface LanguageSelectorProps<T extends boolean> {
+    value: T extends true ? string[] : string;
+    onChange: (value: T extends true ? string[] : string) => void;
+    multiple?: T;
+    availableLanguageCodes?: string[];
+}
+
+export function LanguageSelector<T extends boolean>(props: LanguageSelectorProps<T>) {
+    const { data } = useQuery({
+        queryKey: ['availableGlobalLanguages'],
+        queryFn: () => api.query(availableGlobalLanguages),
+        staleTime: 1000 * 60 * 5, // 5 minutes
+    });
+    const { formatLanguageName } = useLocalFormat();
+    const { value, onChange, multiple, availableLanguageCodes } = props;
+
+    const handleSelect = (selectedValue: string) => {
+        if (multiple) {
+            const currentValue = value as string[];
+            const newValue = currentValue.includes(selectedValue)
+                ? currentValue.filter(v => v !== selectedValue)
+                : [...currentValue, selectedValue];
+            onChange(newValue as T extends true ? string[] : string);
+        } else {
+            onChange(selectedValue as T extends true ? string[] : string);
+        }
+    };
+
+    const handleRemove = (languageToRemove: string) => {
+        if (multiple) {
+            const currentValue = value as string[];
+            onChange(currentValue.filter(v => v !== languageToRemove) as T extends true ? string[] : string);
+        }
+    };
+
+    const renderTrigger = () => {
+        if (multiple) {
+            const selectedLanguages = value as string[];
+            return (
+                <Button
+                    variant="outline"
+                    role="combobox"
+                    className={cn(
+                        'w-full justify-between',
+                        'min-h-[2.5rem] h-auto',
+                        'flex flex-wrap gap-1 p-1',
+                    )}
+                >
+                    <div className="flex flex-wrap gap-1">
+                        {selectedLanguages.length > 0 ? (
+                            selectedLanguages.map(language => (
+                                <Badge key={language} variant="secondary" className="flex items-center gap-1">
+                                    {formatLanguageName(language)}
+                                    <button
+                                        onClick={e => {
+                                            e.stopPropagation();
+                                            handleRemove(language);
+                                        }}
+                                        className="ml-1 hover:text-destructive"
+                                    >
+                                        <X className="h-3 w-3" />
+                                    </button>
+                                </Badge>
+                            ))
+                        ) : (
+                            <span className="text-muted-foreground">Select languages</span>
+                        )}
+                    </div>
+                    <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
+                </Button>
+            );
+        }
+        return (
+            <Button variant="outline" role="combobox" className="w-full justify-between">
+                {value ? formatLanguageName(value as string) : 'Select a language'}
+                <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
+            </Button>
+        );
+    };
+
+    return (
+        <Popover>
+            <PopoverTrigger asChild>{renderTrigger()}</PopoverTrigger>
+            <PopoverContent className="w-[200px] p-0">
+                <div className="max-h-[300px] overflow-auto">
+                    {(availableLanguageCodes ?? data?.globalSettings.availableLanguages)?.map(language => (
+                        <button
+                            key={language}
+                            onClick={() => handleSelect(language)}
+                            className={cn(
+                                'w-full px-2 py-1.5 text-sm text-left hover:bg-accent',
+                                multiple && (value as string[]).includes(language) && 'bg-accent',
+                            )}
+                        >
+                            {formatLanguageName(language)}
+                        </button>
+                    ))}
+                </div>
+            </PopoverContent>
+        </Popover>
+    );
+}

+ 107 - 0
packages/dashboard/src/components/shared/seller-selector.tsx

@@ -0,0 +1,107 @@
+import { Button } from '@/components/ui/button.js';
+import { Command, CommandEmpty, 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 { Trans } from '@lingui/react/macro';
+import { useQuery } from '@tanstack/react-query';
+import { Plus, Search } from 'lucide-react';
+import { useState } from 'react';
+
+const sellerListDocument = graphql(`
+    query SellerList($options: SellerListOptions) {
+        sellers(options: $options) {
+            items {
+                id
+                name
+            }
+            totalItems
+        }
+    }
+`);
+
+export interface Seller {
+    id: string;
+    name: string;
+}
+
+export interface SellerSelectorProps {
+    value?: string | null;
+    onChange: (value: string) => void;
+    label?: string | React.ReactNode;
+    readOnly?: boolean;
+}
+
+export function SellerSelector(props: SellerSelectorProps) {
+    const [open, setOpen] = useState(false);
+    const [searchTerm, setSearchTerm] = useState('');
+
+    const { data, isLoading } = useQuery({
+        queryKey: ['sellerList', searchTerm],
+        queryFn: () =>
+            api.query(sellerListDocument, {
+                options: {
+                    sort: { name: 'ASC' },
+                    filter: searchTerm ? {
+                        name: { contains: searchTerm },
+                    } : undefined,  
+                },
+            }),
+        staleTime: 1000 * 60, // 1 minute
+    });
+
+    const handleSearch = (value: string) => {
+        setSearchTerm(value);
+    };
+
+    const selectedSeller = data?.sellers.items.find(seller => seller.id === props.value);
+
+    return (
+        <Popover open={open} onOpenChange={setOpen}>
+            <PopoverTrigger asChild>
+                <Button variant="outline" size="sm" type="button" disabled={props.readOnly} className="gap-2">
+                    {selectedSeller ? (
+                        <span className="flex-1 text-left">{selectedSeller.name}</span>
+                    ) : (
+                        <>
+                            <Plus className="h-4 w-4" />
+                            {props.label ?? <Trans>Select seller</Trans>}
+                        </>
+                    )}
+                </Button>
+            </PopoverTrigger>
+            <PopoverContent className="p-0 w-[350px]" align="start">
+                <Command shouldFilter={false}>
+                    <div className="flex items-center border-b px-3">
+                        <CommandInput 
+                            placeholder="Search sellers..." 
+                            onValueChange={handleSearch}
+                            className="h-10 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
+                        />
+                    </div>
+                    <CommandList>
+                        <CommandEmpty>
+                            {isLoading ? (
+                                <Trans>Loading...</Trans>
+                            ) : (
+                                <Trans>No sellers found</Trans>
+                            )}
+                        </CommandEmpty>
+                        {data?.sellers.items.map(seller => (
+                            <CommandItem
+                                key={seller.id}
+                                onSelect={() => {
+                                    props.onChange(seller.id);
+                                    setOpen(false);
+                                }}
+                                className="flex flex-col items-start"
+                            >
+                                <div className="font-medium">{seller.name}</div>
+                            </CommandItem>
+                        ))}
+                    </CommandList>
+                </Command>
+            </PopoverContent>
+        </Popover>
+    );
+}

+ 322 - 0
packages/dashboard/src/constants.ts

@@ -1 +1,323 @@
 export const NEW_ENTITY_PATH = 'new';
+export const DEFAULT_CHANNEL_CODE = '__default_channel__';
+/**
+ * This is copied from the generated types from @vendure/common/lib/generated-types.d.ts
+ * It is used to provide a list of available currency codes for the user to select from.
+ * esbuild currently does not support import enums.
+ */
+export const CurrencyCode = {
+    /** United Arab Emirates dirham */
+    AED: 'AED',
+    /** Afghan afghani */
+    AFN: 'AFN',
+    /** Albanian lek */
+    ALL: 'ALL',
+    /** Armenian dram */
+    AMD: 'AMD',
+    /** Netherlands Antillean guilder */
+    ANG: 'ANG',
+    /** Angolan kwanza */
+    AOA: 'AOA',
+    /** Argentine peso */
+    ARS: 'ARS',
+    /** Australian dollar */
+    AUD: 'AUD',
+    /** Aruban florin */
+    AWG: 'AWG',
+    /** Azerbaijani manat */
+    AZN: 'AZN',
+    /** Bosnia and Herzegovina convertible mark */
+    BAM: 'BAM',
+    /** Barbados dollar */
+    BBD: 'BBD',
+    /** Bangladeshi taka */
+    BDT: 'BDT',
+    /** Bulgarian lev */
+    BGN: 'BGN',
+    /** Bahraini dinar */
+    BHD: 'BHD',
+    /** Burundian franc */
+    BIF: 'BIF',
+    /** Bermudian dollar */
+    BMD: 'BMD',
+    /** Brunei dollar */
+    BND: 'BND',
+    /** Boliviano */
+    BOB: 'BOB',
+    /** Brazilian real */
+    BRL: 'BRL',
+    /** Bahamian dollar */
+    BSD: 'BSD',
+    /** Bhutanese ngultrum */
+    BTN: 'BTN',
+    /** Botswana pula */
+    BWP: 'BWP',
+    /** Belarusian ruble */
+    BYN: 'BYN',
+    /** Belize dollar */
+    BZD: 'BZD',
+    /** Canadian dollar */
+    CAD: 'CAD',
+    /** Congolese franc */
+    CDF: 'CDF',
+    /** Swiss franc */
+    CHF: 'CHF',
+    /** Chilean peso */
+    CLP: 'CLP',
+    /** Renminbi (Chinese) yuan */
+    CNY: 'CNY',
+    /** Colombian peso */
+    COP: 'COP',
+    /** Costa Rican colon */
+    CRC: 'CRC',
+    /** Cuban convertible peso */
+    CUC: 'CUC',
+    /** Cuban peso */
+    CUP: 'CUP',
+    /** Cape Verde escudo */
+    CVE: 'CVE',
+    /** Czech koruna */
+    CZK: 'CZK',
+    /** Djiboutian franc */
+    DJF: 'DJF',
+    /** Danish krone */
+    DKK: 'DKK',
+    /** Dominican peso */
+    DOP: 'DOP',
+    /** Algerian dinar */
+    DZD: 'DZD',
+    /** Egyptian pound */
+    EGP: 'EGP',
+    /** Eritrean nakfa */
+    ERN: 'ERN',
+    /** Ethiopian birr */
+    ETB: 'ETB',
+    /** Euro */
+    EUR: 'EUR',
+    /** Fiji dollar */
+    FJD: 'FJD',
+    /** Falkland Islands pound */
+    FKP: 'FKP',
+    /** Pound sterling */
+    GBP: 'GBP',
+    /** Georgian lari */
+    GEL: 'GEL',
+    /** Ghanaian cedi */
+    GHS: 'GHS',
+    /** Gibraltar pound */
+    GIP: 'GIP',
+    /** Gambian dalasi */
+    GMD: 'GMD',
+    /** Guinean franc */
+    GNF: 'GNF',
+    /** Guatemalan quetzal */
+    GTQ: 'GTQ',
+    /** Guyanese dollar */
+    GYD: 'GYD',
+    /** Hong Kong dollar */
+    HKD: 'HKD',
+    /** Honduran lempira */
+    HNL: 'HNL',
+    /** Croatian kuna */
+    HRK: 'HRK',
+    /** Haitian gourde */
+    HTG: 'HTG',
+    /** Hungarian forint */
+    HUF: 'HUF',
+    /** Indonesian rupiah */
+    IDR: 'IDR',
+    /** Israeli new shekel */
+    ILS: 'ILS',
+    /** Indian rupee */
+    INR: 'INR',
+    /** Iraqi dinar */
+    IQD: 'IQD',
+    /** Iranian rial */
+    IRR: 'IRR',
+    /** Icelandic króna */
+    ISK: 'ISK',
+    /** Jamaican dollar */
+    JMD: 'JMD',
+    /** Jordanian dinar */
+    JOD: 'JOD',
+    /** Japanese yen */
+    JPY: 'JPY',
+    /** Kenyan shilling */
+    KES: 'KES',
+    /** Kyrgyzstani som */
+    KGS: 'KGS',
+    /** Cambodian riel */
+    KHR: 'KHR',
+    /** Comoro franc */
+    KMF: 'KMF',
+    /** North Korean won */
+    KPW: 'KPW',
+    /** South Korean won */
+    KRW: 'KRW',
+    /** Kuwaiti dinar */
+    KWD: 'KWD',
+    /** Cayman Islands dollar */
+    KYD: 'KYD',
+    /** Kazakhstani tenge */
+    KZT: 'KZT',
+    /** Lao kip */
+    LAK: 'LAK',
+    /** Lebanese pound */
+    LBP: 'LBP',
+    /** Sri Lankan rupee */
+    LKR: 'LKR',
+    /** Liberian dollar */
+    LRD: 'LRD',
+    /** Lesotho loti */
+    LSL: 'LSL',
+    /** Libyan dinar */
+    LYD: 'LYD',
+    /** Moroccan dirham */
+    MAD: 'MAD',
+    /** Moldovan leu */
+    MDL: 'MDL',
+    /** Malagasy ariary */
+    MGA: 'MGA',
+    /** Macedonian denar */
+    MKD: 'MKD',
+    /** Myanmar kyat */
+    MMK: 'MMK',
+    /** Mongolian tögrög */
+    MNT: 'MNT',
+    /** Macanese pataca */
+    MOP: 'MOP',
+    /** Mauritanian ouguiya */
+    MRU: 'MRU',
+    /** Mauritian rupee */
+    MUR: 'MUR',
+    /** Maldivian rufiyaa */
+    MVR: 'MVR',
+    /** Malawian kwacha */
+    MWK: 'MWK',
+    /** Mexican peso */
+    MXN: 'MXN',
+    /** Malaysian ringgit */
+    MYR: 'MYR',
+    /** Mozambican metical */
+    MZN: 'MZN',
+    /** Namibian dollar */
+    NAD: 'NAD',
+    /** Nigerian naira */
+    NGN: 'NGN',
+    /** Nicaraguan córdoba */
+    NIO: 'NIO',
+    /** Norwegian krone */
+    NOK: 'NOK',
+    /** Nepalese rupee */
+    NPR: 'NPR',
+    /** New Zealand dollar */
+    NZD: 'NZD',
+    /** Omani rial */
+    OMR: 'OMR',
+    /** Panamanian balboa */
+    PAB: 'PAB',
+    /** Peruvian sol */
+    PEN: 'PEN',
+    /** Papua New Guinean kina */
+    PGK: 'PGK',
+    /** Philippine peso */
+    PHP: 'PHP',
+    /** Pakistani rupee */
+    PKR: 'PKR',
+    /** Polish złoty */
+    PLN: 'PLN',
+    /** Paraguayan guaraní */
+    PYG: 'PYG',
+    /** Qatari riyal */
+    QAR: 'QAR',
+    /** Romanian leu */
+    RON: 'RON',
+    /** Serbian dinar */
+    RSD: 'RSD',
+    /** Russian ruble */
+    RUB: 'RUB',
+    /** Rwandan franc */
+    RWF: 'RWF',
+    /** Saudi riyal */
+    SAR: 'SAR',
+    /** Solomon Islands dollar */
+    SBD: 'SBD',
+    /** Seychelles rupee */
+    SCR: 'SCR',
+    /** Sudanese pound */
+    SDG: 'SDG',
+    /** Swedish krona/kronor */
+    SEK: 'SEK',
+    /** Singapore dollar */
+    SGD: 'SGD',
+    /** Saint Helena pound */
+    SHP: 'SHP',
+    /** Sierra Leonean leone */
+    SLL: 'SLL',
+    /** Somali shilling */
+    SOS: 'SOS',
+    /** Surinamese dollar */
+    SRD: 'SRD',
+    /** South Sudanese pound */
+    SSP: 'SSP',
+    /** São Tomé and Príncipe dobra */
+    STN: 'STN',
+    /** Salvadoran colón */
+    SVC: 'SVC',
+    /** Syrian pound */
+    SYP: 'SYP',
+    /** Swazi lilangeni */
+    SZL: 'SZL',
+    /** Thai baht */
+    THB: 'THB',
+    /** Tajikistani somoni */
+    TJS: 'TJS',
+    /** Turkmenistan manat */
+    TMT: 'TMT',
+    /** Tunisian dinar */
+    TND: 'TND',
+    /** Tongan paʻanga */
+    TOP: 'TOP',
+    /** Turkish lira */
+    TRY: 'TRY',
+    /** Trinidad and Tobago dollar */
+    TTD: 'TTD',
+    /** New Taiwan dollar */
+    TWD: 'TWD',
+    /** Tanzanian shilling */
+    TZS: 'TZS',
+    /** Ukrainian hryvnia */
+    UAH: 'UAH',
+    /** Ugandan shilling */
+    UGX: 'UGX',
+    /** United States dollar */
+    USD: 'USD',
+    /** Uruguayan peso */
+    UYU: 'UYU',
+    /** Uzbekistan som */
+    UZS: 'UZS',
+    /** Venezuelan bolívar soberano */
+    VES: 'VES',
+    /** Vietnamese đồng */
+    VND: 'VND',
+    /** Vanuatu vatu */
+    VUV: 'VUV',
+    /** Samoan tala */
+    WST: 'WST',
+    /** CFA franc BEAC */
+    XAF: 'XAF',
+    /** East Caribbean dollar */
+    XCD: 'XCD',
+    /** CFA franc BCEAO */
+    XOF: 'XOF',
+    /** CFP franc (franc Pacifique) */
+    XPF: 'XPF',
+    /** Yemeni rial */
+    YER: 'YER',
+    /** South African rand */
+    ZAR: 'ZAR',
+    /** Zambian kwacha */
+    ZMW: 'ZMW',
+    /** Zimbabwean dollar */
+    ZWL: 'ZWL',
+};

+ 44 - 2
packages/dashboard/src/routes/_authenticated/_channels/channels.graphql.ts

@@ -29,8 +29,8 @@ export const channelItemFragment = graphql(`
 
 export const channelListQuery = graphql(
     `
-        query ChannelList {
-            channels {
+        query ChannelList($options: ChannelListOptions) {
+            channels(options: $options) {
                 items {
                     ...ChannelItem
                 }
@@ -40,3 +40,45 @@ export const channelListQuery = graphql(
     `,
     [channelItemFragment],
 );
+
+export const channelDetailDocument = graphql(
+    `
+        query ChannelDetail($id: ID!) {
+            channel(id: $id) {
+                ...ChannelItem
+                customFields
+            }
+        }
+    `,
+    [channelItemFragment],
+);
+
+export const createChannelDocument = graphql(`
+    mutation CreateChannel($input: CreateChannelInput!) {
+        createChannel(input: $input) {
+            __typename
+            ... on Channel {
+                id
+            }
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+        }
+    }
+`);
+
+export const updateChannelDocument = graphql(`
+    mutation UpdateChannel($input: UpdateChannelInput!) {
+        updateChannel(input: $input) {
+            __typename
+            ... on Channel {
+                id
+            }
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_channels/channels.tsx

@@ -8,6 +8,7 @@ import { PermissionGuard } from '@/components/shared/permission-guard.js';
 import { PlusIcon } from 'lucide-react';
 import { DetailPageButton } from '@/components/shared/detail-page-button.js';
 import { Trans } from '@lingui/react/macro';
+import { ChannelCodeLabel } from './components/channel-code-label.js';
 
 export const Route = createFileRoute('/_authenticated/_channels/channels')({
     component: ChannelListPage,
@@ -33,7 +34,7 @@ function ChannelListPage() {
                 code: {
                     header: 'Code',
                     cell: ({ row }) => {
-                        return <DetailPageButton id={row.original.id} label={row.original.code} />;
+                        return <DetailPageButton id={row.original.id} label={<ChannelCodeLabel code={row.original.code} />} />;
                     },
                 },
             }}

+ 345 - 0
packages/dashboard/src/routes/_authenticated/_channels/channels_.$id.tsx

@@ -0,0 +1,345 @@
+import { ErrorPage } from '@/components/shared/error-page.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { Button } from '@/components/ui/button.js';
+import {
+    Form,
+    FormControl,
+    FormDescription,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from '@/components/ui/form.js';
+import { Input } from '@/components/ui/input.js';
+import { Switch } from '@/components/ui/switch.js';
+import { NEW_ENTITY_PATH } from '@/constants.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import {
+    CustomFieldsPageBlock,
+    Page,
+    PageActionBar,
+    PageBlock,
+    PageLayout,
+    PageTitle,
+} from '@/framework/layout-engine/page-layout.js';
+import { getDetailQueryOptions, useDetailPage } from '@/framework/page/use-detail-page.js';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { createFileRoute, useNavigate } from '@tanstack/react-router';
+import { toast } from 'sonner';
+import { createChannelDocument, channelDetailDocument, updateChannelDocument } from './channels.graphql.js';
+import { SellerSelector } from '@/components/shared/seller-selector.js';
+import { LanguageSelector } from '@/components/shared/language-selector.js';
+import { CurrencySelector } from '@/components/shared/currency-selector.js';
+import { ZoneSelector } from '@/components/shared/zone-selector.js';
+import { Badge } from '@/components/ui/badge.js';
+import { DEFAULT_CHANNEL_CODE } from '@/constants.js';
+import { ChannelCodeLabel } from './components/channel-code-label.js';
+
+export const Route = createFileRoute('/_authenticated/_channels/channels_/$id')({
+    component: ChannelDetailPage,
+    loader: async ({ context, params }) => {
+        const isNew = params.id === NEW_ENTITY_PATH;
+        const result = isNew
+            ? null
+            : await context.queryClient.ensureQueryData(
+                  getDetailQueryOptions(addCustomFields(channelDetailDocument), { id: params.id }),
+                  { id: params.id },
+              );
+        if (!isNew && !result.channel) {
+            throw new Error(`Channel with the ID ${params.id} was not found`);
+        }
+        return {
+            breadcrumb: [
+                { path: '/channels', label: 'Channels' },
+                isNew ? <Trans>New channel</Trans> : <ChannelCodeLabel code={result.channel.code ?? ''} />,
+            ],
+        };
+    },
+    errorComponent: ({ error }) => <ErrorPage message={error.message} />,
+});
+
+export function ChannelDetailPage() {
+    const params = Route.useParams();
+    const navigate = useNavigate();
+    const creatingNewEntity = params.id === NEW_ENTITY_PATH;
+    const { i18n } = useLingui();
+
+    const { form, submitHandler, entity, isPending } = useDetailPage({
+        queryDocument: addCustomFields(channelDetailDocument),
+        entityField: 'channel',
+        createDocument: createChannelDocument,
+        updateDocument: updateChannelDocument,
+        setValuesForUpdate: entity => {
+            return {
+                id: entity.id,
+                code: entity.code,
+                token: entity.token,
+                pricesIncludeTax: entity.pricesIncludeTax,
+                availableCurrencyCodes: entity.availableCurrencyCodes,
+                availableLanguageCodes: entity.availableLanguageCodes,
+                defaultCurrencyCode: entity.defaultCurrencyCode,
+                defaultLanguageCode: entity.defaultLanguageCode,
+                defaultShippingZoneId: entity.defaultShippingZone?.id,
+                defaultTaxZoneId: entity.defaultTaxZone?.id,
+                sellerId: entity.seller?.id,
+                customFields: entity.customFields,
+            };
+        },
+        transformCreateInput: input => {
+            return {
+                ...input,
+                currencyCode: undefined,
+            };
+        },
+        params: { id: params.id },
+        onSuccess: async data => {
+            if (data.__typename === 'Channel') {
+                toast(i18n.t('Successfully updated channel'), {
+                    position: 'top-right',
+                });
+                form.reset(form.getValues());
+                if (creatingNewEntity) {
+                    await navigate({ to: `../${data?.id}`, from: Route.id });
+                }
+            } else {
+                toast(i18n.t('Failed to update channel'), {
+                    position: 'top-right',
+                    description: data.message,
+                });
+            }
+        },
+        onError: err => {
+            toast(i18n.t('Failed to update channel'), {
+                position: 'top-right',
+                description: err instanceof Error ? err.message : 'Unknown error',
+            });
+        },
+    });
+
+    const availableCurrencyCodes = form.watch('availableCurrencyCodes');
+    const availableLanguageCodes = form.watch('availableLanguageCodes');
+
+    const codeIsDefault = entity?.code === DEFAULT_CHANNEL_CODE;
+
+    return (
+        <Page>
+            <PageTitle>
+                {creatingNewEntity ? (
+                    <Trans>New channel</Trans>
+                ) : (
+                    <ChannelCodeLabel code={entity?.code ?? ''} />
+                )}
+            </PageTitle>
+            <Form {...form}>
+                <form onSubmit={submitHandler} className="space-y-8">
+                    <PageActionBar>
+                        <div></div>
+                        <PermissionGuard requires={['UpdateChannel']}>
+                            <Button
+                                type="submit"
+                                disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                            >
+                                <Trans>Update</Trans>
+                            </Button>
+                        </PermissionGuard>
+                    </PageActionBar>
+                    <PageLayout>
+                        <PageBlock column="main">
+                            <div className="md:grid md:grid-cols-2 gap-4 items-start">
+                                <FormField
+                                    control={form.control}
+                                    name="code"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Code</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Input placeholder="" {...field} disabled={codeIsDefault} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <div></div>
+                                <FormField
+                                    control={form.control}
+                                    name="token"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Token</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Input placeholder="" {...field} />
+                                            </FormControl>
+                                            <FormDescription>
+                                                The token is used to specify the channel when making API
+                                                requests.
+                                            </FormDescription>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="sellerId"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Seller</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <SellerSelector
+                                                    value={field.value ?? ''}
+                                                    onChange={field.onChange}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="availableLanguageCodes"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Available languages</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <LanguageSelector
+                                                    value={field.value ?? []}
+                                                    onChange={field.onChange}
+                                                    multiple={true}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="availableCurrencyCodes"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Available currencies</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <CurrencySelector
+                                                    value={field.value ?? []}
+                                                    onChange={field.onChange}
+                                                    multiple={true}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                            </div>
+                        </PageBlock>
+                        <PageBlock column="main" title={<Trans>Channel defaults</Trans>}>
+                            <div className="md:grid md:grid-cols-2 gap-4 items-start">
+                                <FormField
+                                    control={form.control}
+                                    name="defaultLanguageCode"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Default language</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <LanguageSelector
+                                                    value={field.value ?? ''}
+                                                    onChange={field.onChange}
+                                                    multiple={false}
+                                                    availableLanguageCodes={availableLanguageCodes ?? []}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="defaultCurrencyCode"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Default currency</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <CurrencySelector
+                                                    value={field.value ?? ''}
+                                                    onChange={field.onChange}
+                                                    multiple={false}
+                                                    availableCurrencyCodes={availableCurrencyCodes ?? []}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="defaultTaxZoneId"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Default tax zone</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <ZoneSelector
+                                                    value={field.value ?? ''}
+                                                    onChange={field.onChange}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="defaultShippingZoneId"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Default shipping zone</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <ZoneSelector
+                                                    value={field.value ?? ''}
+                                                    onChange={field.onChange}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={form.control}
+                                    name="pricesIncludeTax"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>
+                                                <Trans>Prices include tax for default tax zone</Trans>
+                                            </FormLabel>
+                                            <FormControl>
+                                                <Switch
+                                                    checked={field.value ?? false}
+                                                    onCheckedChange={field.onChange}
+                                                />
+                                            </FormControl>
+                                            <FormDescription>
+                                                <Trans>
+                                                    When this is enabled, the prices entered in the product
+                                                    catalog will be included in the tax for the default tax
+                                                    zone.
+                                                </Trans>
+                                            </FormDescription>
+                                        </FormItem>
+                                    )}
+                                />
+                            </div>
+                        </PageBlock>
+                        <CustomFieldsPageBlock column="main" entityType="Channel" control={form.control} />
+                    </PageLayout>
+                </form>
+            </Form>
+        </Page>
+    );
+}

+ 8 - 0
packages/dashboard/src/routes/_authenticated/_channels/components/channel-code-label.tsx

@@ -0,0 +1,8 @@
+import { DEFAULT_CHANNEL_CODE } from '@/constants.js';
+import { Trans } from '@lingui/react/macro';
+
+export function ChannelCodeLabel({ code }: { code: string } | { code: undefined }) {
+    console.log('code', code);
+    return code === DEFAULT_CHANNEL_CODE ? <Trans>Default channel</Trans> : code;
+}
+