Browse Source

feat(dashboard): Implement entity deletion for all list views

Michael Bromley 10 months ago
parent
commit
a3a6ce2c63
41 changed files with 475 additions and 73 deletions
  1. 1 3
      packages/dashboard/src/components/data-input/datetime-input.tsx
  2. 5 7
      packages/dashboard/src/components/data-table/data-table.tsx
  3. 126 13
      packages/dashboard/src/components/shared/paginated-list-data-table.tsx
  4. 8 1
      packages/dashboard/src/framework/page/list-page.tsx
  5. 9 0
      packages/dashboard/src/routes/_authenticated/_administrators/administrators.graphql.ts
  6. 2 1
      packages/dashboard/src/routes/_authenticated/_administrators/administrators.tsx
  7. 9 0
      packages/dashboard/src/routes/_authenticated/_channels/channels.graphql.ts
  8. 2 1
      packages/dashboard/src/routes/_authenticated/_channels/channels.tsx
  9. 9 0
      packages/dashboard/src/routes/_authenticated/_collections/collections.graphql.ts
  10. 3 2
      packages/dashboard/src/routes/_authenticated/_collections/collections.tsx
  11. 9 0
      packages/dashboard/src/routes/_authenticated/_countries/countries.graphql.ts
  12. 2 1
      packages/dashboard/src/routes/_authenticated/_countries/countries.tsx
  13. 9 0
      packages/dashboard/src/routes/_authenticated/_customer-groups/customer-groups.graphql.ts
  14. 4 1
      packages/dashboard/src/routes/_authenticated/_customer-groups/customer-groups.tsx
  15. 113 12
      packages/dashboard/src/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx
  16. 9 0
      packages/dashboard/src/routes/_authenticated/_customers/customers.graphql.ts
  17. 3 2
      packages/dashboard/src/routes/_authenticated/_customers/customers.tsx
  18. 9 0
      packages/dashboard/src/routes/_authenticated/_facets/facets.graphql.ts
  19. 3 2
      packages/dashboard/src/routes/_authenticated/_facets/facets.tsx
  20. 9 0
      packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.graphql.ts
  21. 2 1
      packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.tsx
  22. 9 0
      packages/dashboard/src/routes/_authenticated/_product-variants/product-variants.graphql.ts
  23. 3 2
      packages/dashboard/src/routes/_authenticated/_product-variants/product-variants.tsx
  24. 9 0
      packages/dashboard/src/routes/_authenticated/_products/products.graphql.ts
  25. 4 3
      packages/dashboard/src/routes/_authenticated/_products/products.tsx
  26. 11 0
      packages/dashboard/src/routes/_authenticated/_promotions/promotions.graphql.ts
  27. 2 1
      packages/dashboard/src/routes/_authenticated/_promotions/promotions.tsx
  28. 23 11
      packages/dashboard/src/routes/_authenticated/_promotions/promotions_.$id.tsx
  29. 2 1
      packages/dashboard/src/routes/_authenticated/_roles/roles.tsx
  30. 9 0
      packages/dashboard/src/routes/_authenticated/_sellers/sellers.graphql.ts
  31. 2 1
      packages/dashboard/src/routes/_authenticated/_sellers/sellers.tsx
  32. 9 0
      packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts
  33. 2 1
      packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.tsx
  34. 9 0
      packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.graphql.ts
  35. 2 1
      packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.tsx
  36. 9 0
      packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.graphql.ts
  37. 2 1
      packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.tsx
  38. 9 0
      packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.graphql.ts
  39. 2 3
      packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.tsx
  40. 9 0
      packages/dashboard/src/routes/_authenticated/_zones/zones.graphql.ts
  41. 2 1
      packages/dashboard/src/routes/_authenticated/_zones/zones.tsx

+ 1 - 3
packages/dashboard/src/components/data-input/datetime-input.tsx

@@ -20,13 +20,12 @@ export interface DateTimeInputProps {
 }
  
 export function DateTimeInput(props: DateTimeInputProps) {
-  const [date, setDate] = React.useState<string>(props.value && props.value instanceof Date ? props.value.toISOString() : props.value ?? '');
+  const date = props.value && props.value instanceof Date ? props.value.toISOString() : 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.toISOString());
       props.onChange(selectedDate);
     }
   };
@@ -49,7 +48,6 @@ export function DateTimeInput(props: DateTimeInputProps) {
           value === "PM" ? currentHours + 12 : currentHours - 12
         );
       }
-      setDate(newDate.toISOString());
       props.onChange(newDate);
     }
   };

+ 5 - 7
packages/dashboard/src/components/data-table/data-table.tsx

@@ -1,24 +1,22 @@
 'use client';
 
+import { DataTablePagination } from '@/components/data-table/data-table-pagination.js';
+import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
 import { Badge } from '@/components/ui/badge.js';
 import { Input } from '@/components/ui/input.js';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
-import { DataTablePagination } from '@/components/data-table/data-table-pagination.js';
-import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
-
 import {
     ColumnDef,
+    ColumnFilter,
+    ColumnFiltersState,
     flexRender,
     getCoreRowModel,
     getPaginationRowModel,
     PaginationState,
-    VisibilityState,
     SortingState,
     Table as TableType,
     useReactTable,
-    ColumnFilter,
-    ColumnFiltersState,
-    Column,
+    VisibilityState
 } from '@tanstack/react-table';
 import { CircleX, Filter } from 'lucide-react';
 import React, { Suspense, useEffect } from 'react';

+ 126 - 13
packages/dashboard/src/components/shared/paginated-list-data-table.tsx

@@ -7,7 +7,7 @@ import {
 } from '@/framework/document-introspection/get-document-structure.js';
 import { useListQueryFields } from '@/framework/document-introspection/hooks.js';
 import { api } from '@/graphql/api.js';
-import { useQueryClient } from '@tanstack/react-query';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
 import { useDebounce } from 'use-debounce';
 
 import { DisplayComponent } from '@/framework/component-registry/dynamic-component.js';
@@ -21,8 +21,26 @@ import {
     SortingState,
     Table,
 } from '@tanstack/react-table';
-import { ColumnDef } from '@tanstack/table-core';
+import { AccessorKeyColumnDef, ColumnDef, Row } from '@tanstack/table-core';
 import React, { useMemo } from 'react';
+import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuGroup,
+    DropdownMenuItem,
+    DropdownMenuLabel,
+    DropdownMenuPortal,
+    DropdownMenuSeparator,
+    DropdownMenuShortcut,
+    DropdownMenuSub,
+    DropdownMenuSubContent,
+    DropdownMenuSubTrigger,
+    DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu.js';
+import { Button } from '../ui/button.js';
+import { EllipsisIcon, TrashIcon } from 'lucide-react';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { toast } from 'sonner';
 
 // Type that identifies a paginated list structure (has items array and totalItems)
 type IsPaginatedList<T> = T extends { items: any[]; totalItems: number } ? true : false;
@@ -88,7 +106,9 @@ export type PaginatedListKeys<
 }[keyof PaginatedListItemFields<T, Path>];
 
 export type CustomizeColumnConfig<T extends TypedDocumentNode<any, any>> = {
-    [Key in keyof PaginatedListItemFields<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>, PaginatedListItemFields<T>[Key]>>;
+    [Key in keyof PaginatedListItemFields<T>]?: Partial<
+        ColumnDef<PaginatedListItemFields<T>, PaginatedListItemFields<T>[Key]>
+    >;
 };
 
 export type FacetedFilterConfig<T extends TypedDocumentNode<any, any>> = {
@@ -124,8 +144,8 @@ export type ListQueryOptionsShape = {
 };
 
 export type AdditionalColumns<T extends TypedDocumentNode<any, any>> = {
-    [key: string]: ColumnDef<PaginatedListItemFields<T>>
-}
+    [key: string]: ColumnDef<PaginatedListItemFields<T>>;
+};
 
 export interface PaginatedListContext {
     refetchPaginatedList: () => void;
@@ -157,6 +177,11 @@ export function usePaginatedList() {
     return context;
 }
 
+export interface RowAction<T> {
+    label: React.ReactNode;
+    onClick?: (row: Row<T>) => void;
+}
+
 export interface PaginatedListDataTableProps<
     T extends TypedDocumentNode<U, V>,
     U extends any,
@@ -164,6 +189,7 @@ export interface PaginatedListDataTableProps<
     AC extends AdditionalColumns<T>,
 > {
     listQuery: T;
+    deleteMutation?: TypedDocumentNode<any, any>;
     transformQueryKey?: (queryKey: any[]) => any[];
     transformVariables?: (variables: V) => V;
     customizeColumns?: CustomizeColumnConfig<T>;
@@ -179,6 +205,7 @@ export interface PaginatedListDataTableProps<
     onSortChange: (table: Table<any>, sorting: SortingState) => void;
     onFilterChange: (table: Table<any>, filters: ColumnFiltersState) => void;
     facetedFilters?: FacetedFilterConfig<T>;
+    rowActions?: RowAction<PaginatedListItemFields<T>>[];
 }
 
 export const PaginatedListDataTableKey = 'PaginatedListDataTable';
@@ -190,6 +217,7 @@ export function PaginatedListDataTable<
     AC extends AdditionalColumns<T> = AdditionalColumns<T>,
 >({
     listQuery,
+    deleteMutation,
     transformQueryKey,
     transformVariables,
     customizeColumns,
@@ -205,13 +233,12 @@ export function PaginatedListDataTable<
     onSortChange,
     onFilterChange,
     facetedFilters,
+    rowActions,
 }: PaginatedListDataTableProps<T, U, V, AC>) {
     const [searchTerm, setSearchTerm] = React.useState<string>('');
     const [debouncedSearchTerm] = useDebounce(searchTerm, 500);
     const queryClient = useQueryClient();
 
-
-
     const sort = sorting?.reduce((acc: any, sort: ColumnSort) => {
         const direction = sort.desc ? 'DESC' : 'ASC';
         const field = sort.id;
@@ -222,7 +249,6 @@ export function PaginatedListDataTable<
         return { ...acc, [field]: direction };
     }, {});
 
-
     const filter = columnFilters?.length
         ? {
               _and: columnFilters.map(f => {
@@ -234,7 +260,15 @@ export function PaginatedListDataTable<
           }
         : undefined;
 
-    const defaultQueryKey = [PaginatedListDataTableKey, listQuery, page, itemsPerPage, sorting, filter, debouncedSearchTerm];
+    const defaultQueryKey = [
+        PaginatedListDataTableKey,
+        listQuery,
+        page,
+        itemsPerPage,
+        sorting,
+        filter,
+        debouncedSearchTerm,
+    ];
     const queryKey = transformQueryKey ? transformQueryKey(defaultQueryKey) : defaultQueryKey;
 
     function refetchPaginatedList() {
@@ -269,6 +303,7 @@ export function PaginatedListDataTable<
     }
 
     const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
+    const customFieldColumnNames: string[] = [];
 
     const columns = useMemo(() => {
         const columnConfigs: Array<{ fieldInfo: FieldInfo; isCustomField: boolean }> = [];
@@ -285,6 +320,7 @@ export function PaginatedListDataTable<
             columnConfigs.push(
                 ...customFieldFields.map(field => ({ fieldInfo: field, isCustomField: true })),
             );
+            customFieldColumnNames.push(...customFieldFields.map(field => field.name));
         }
 
         const queryBasedColumns = columnConfigs.map(({ fieldInfo, isCustomField }) => {
@@ -352,14 +388,20 @@ export function PaginatedListDataTable<
             const remainingColumns = finalColumns.filter(
                 column => !column.id || !defaultColumnOrder.includes(column.id),
             );
-            finalColumns = [...orderedColumns, ...remainingColumns];
+            finalColumns = [...orderedColumns];
         }
 
-        return finalColumns;
-    }, [fields, customizeColumns]);
+        if (rowActions || deleteMutation) {
+            const rowActionColumn = getRowActions(rowActions, deleteMutation);
+            if (rowActionColumn) {
+                finalColumns.push(rowActionColumn);
+            }
+        }
 
-    const columnVisibility = getColumnVisibility(fields, defaultVisibility);
+        return finalColumns;
+    }, [fields, customizeColumns, rowActions]);
 
+    const columnVisibility = getColumnVisibility(fields, defaultVisibility, customFieldColumnNames);
     return (
         <PaginatedListContext.Provider value={{ refetchPaginatedList }}>
             <DataTable
@@ -381,12 +423,80 @@ export function PaginatedListDataTable<
     );
 }
 
+function getRowActions(
+    rowActions?: RowAction<any>[],
+    deleteMutation?: TypedDocumentNode<any, any>,
+): AccessorKeyColumnDef<any> | undefined {
+    return {
+        id: 'actions',
+        accessorKey: 'actions',
+        header: 'Actions',
+        cell: ({ row }) => {
+            return (
+                <DropdownMenu>
+                    <DropdownMenuTrigger asChild>
+                        <Button variant="ghost" size="icon">
+                            <EllipsisIcon />
+                        </Button>
+                    </DropdownMenuTrigger>
+                    <DropdownMenuContent>
+                        {rowActions?.map((action, index) => (
+                            <DropdownMenuItem onClick={() => action.onClick?.(row)} key={index}>
+                                {action.label}
+                            </DropdownMenuItem>
+                        ))}
+                        {deleteMutation && <DeleteMutationRowAction deleteMutation={deleteMutation} row={row} />}
+                    </DropdownMenuContent>
+                </DropdownMenu>
+            );
+        },
+    };
+}
+
+function DeleteMutationRowAction({
+    deleteMutation,
+    row,
+}: {
+    deleteMutation: TypedDocumentNode<any, any>;
+    row: Row<{ id: string }>;
+}) {
+    const { refetchPaginatedList } = usePaginatedList();
+    const { i18n } = useLingui();
+    const { mutate: deleteMutationFn } = useMutation({
+        mutationFn: api.mutate(deleteMutation),
+        onSuccess: (result: { [key: string]: { result: 'DELETED' | 'NOT_DELETED'; message: string } }) => {
+            const unwrappedResult = Object.values(result)[0];
+            if (unwrappedResult.result === 'DELETED') {
+                refetchPaginatedList();
+                toast.success(i18n.t('Deleted successfully'));
+            } else {
+                toast.error(i18n.t('Failed to delete'), {
+                    description: unwrappedResult.message,
+                });
+            }
+        },
+        onError: (err: Error) => {
+            toast.error(i18n.t('Failed to delete'), {
+                description: err.message,
+            });
+        },
+    });
+    return (
+        <DropdownMenuItem onClick={() => deleteMutationFn({ id: row.original.id })}>
+            <div className="flex items-center gap-2 text-destructive">
+                <TrashIcon className="w-4 h-4 text-destructive" />
+                <Trans>Delete</Trans>
+            </div>
+        </DropdownMenuItem>
+    );
+}
 /**
  * Returns the default column visibility configuration.
  */
 function getColumnVisibility(
     fields: FieldInfo[],
     defaultVisibility?: Record<string, boolean | undefined>,
+    customFieldColumnNames?: string[],
 ): Record<string, boolean> {
     const allDefaultsTrue = defaultVisibility && Object.values(defaultVisibility).every(v => v === true);
     const allDefaultsFalse = defaultVisibility && Object.values(defaultVisibility).every(v => v === false);
@@ -397,5 +507,8 @@ function getColumnVisibility(
         ...(allDefaultsTrue ? { ...Object.fromEntries(fields.map(f => [f.name, false])) } : {}),
         ...(allDefaultsFalse ? { ...Object.fromEntries(fields.map(f => [f.name, true])) } : {}),
         ...defaultVisibility,
+        ...(customFieldColumnNames
+            ? { ...Object.fromEntries(customFieldColumnNames.map(f => [f, false])) }
+            : {}),
     };
 }

+ 8 - 1
packages/dashboard/src/framework/page/list-page.tsx

@@ -6,7 +6,8 @@ import {
     FacetedFilterConfig,
     ListQueryOptionsShape,
     ListQueryShape,
-    PaginatedListDataTable
+    PaginatedListDataTable,
+    RowAction
 } from '@/components/shared/paginated-list-data-table.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { AnyRouter, useNavigate } from '@tanstack/react-router';
@@ -31,6 +32,7 @@ export interface ListPageProps<
     AC extends AdditionalColumns<T>,
 > extends PageProps {
     listQuery: T;
+    deleteMutation?: TypedDocumentNode<any, { id: string }>;
     transformVariables?: (variables: V) => V;
     onSearchTermChange?: (searchTerm: string) => NonNullable<V['options']>['filter'];
     customizeColumns?: CustomizeColumnConfig<T>;
@@ -40,6 +42,7 @@ export interface ListPageProps<
     defaultVisibility?: Partial<Record<keyof ListQueryFields<T> | keyof AC, boolean>>;
     children?: React.ReactNode;
     facetedFilters?: FacetedFilterConfig<T>;
+    rowActions?: RowAction<ListQueryFields<T>>[];
 }
 
 export function ListPage<
@@ -50,6 +53,7 @@ export function ListPage<
 >({
     title,
     listQuery,
+    deleteMutation,
     transformVariables,
     customizeColumns,
     additionalColumns,
@@ -60,6 +64,7 @@ export function ListPage<
     onSearchTermChange,
     facetedFilters,
     children,
+    rowActions,
 }: ListPageProps<T, U, V, AC>) {
     const route = typeof routeOrFn === 'function' ? routeOrFn() : routeOrFn;
     const routeSearch = route.useSearch();
@@ -112,6 +117,7 @@ export function ListPage<
             <PageActionBar>{children}</PageActionBar>
             <PaginatedListDataTable
                 listQuery={listQueryWithCustomFields}
+                deleteMutation={deleteMutation}
                 transformVariables={transformVariables}
                 customizeColumns={customizeColumns}
                 additionalColumns={additionalColumns}
@@ -132,6 +138,7 @@ export function ListPage<
                     persistListStateToUrl(table, { filters });
                 }}
                 facetedFilters={facetedFilters}
+                rowActions={rowActions}
             />
         </Page>
     );

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_administrators/administrators.graphql.ts

@@ -68,3 +68,12 @@ export const updateAdministratorDocument = graphql(`
         }
     }
 `);
+
+export const deleteAdministratorDocument = graphql(`
+    mutation DeleteAdministrator($id: ID!) {
+        deleteAdministrator(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_administrators/administrators.tsx

@@ -8,7 +8,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { administratorListDocument } from './administrators.graphql.js';
+import { administratorListDocument, deleteAdministratorDocument } from './administrators.graphql.js';
 export const Route = createFileRoute('/_authenticated/_administrators/administrators')({
     component: AdministratorListPage,
     loader: () => ({ breadcrumb: () => <Trans>Administrators</Trans> }),
@@ -19,6 +19,7 @@ function AdministratorListPage() {
         <ListPage
             title="Administrators"
             listQuery={administratorListDocument}
+            deleteMutation={deleteAdministratorDocument}
             route={Route}
             onSearchTermChange={searchTerm => {
                 return {

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_channels/channels.graphql.ts

@@ -82,3 +82,12 @@ export const updateChannelDocument = graphql(`
         }
     }
 `);
+
+export const deleteChannelDocument = graphql(`
+    mutation DeleteChannel($id: ID!) {
+        deleteChannel(id: $id) {
+            result
+            message
+        }
+    }
+`);

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

@@ -7,7 +7,7 @@ import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { ChannelCodeLabel } from '../../../components/shared/channel-code-label.js';
-import { channelListQuery } from './channels.graphql.js';
+import { channelListQuery, deleteChannelDocument } from './channels.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_channels/channels')({
     component: ChannelListPage,
@@ -19,6 +19,7 @@ function ChannelListPage() {
         <ListPage
             title="Channels"
             listQuery={channelListQuery}
+            deleteMutation={deleteChannelDocument}
             route={Route}
             defaultVisibility={{
                 code: true,

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

@@ -104,6 +104,15 @@ export const updateCollectionDocument = graphql(`
     }
 `);
 
+export const deleteCollectionDocument = graphql(`
+    mutation DeleteCollection($id: ID!) {
+        deleteCollection(id: $id) {
+            result
+            message
+        }
+    }
+`);
+
 export const getCollectionFiltersDocument = graphql(
     `
         query GetCollectionFilters {

+ 3 - 2
packages/dashboard/src/routes/_authenticated/_collections/collections.tsx

@@ -6,7 +6,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { collectionListDocument } from './collections.graphql.js';
+import { collectionListDocument, deleteCollectionDocument } from './collections.graphql.js';
 import { CollectionContentsSheet } from './components/collection-contents-sheet.js';
 
 export const Route = createFileRoute('/_authenticated/_collections/collections')({
@@ -18,6 +18,8 @@ export function CollectionListPage() {
     return (
         <ListPage
             title="Collections"
+            listQuery={collectionListDocument}
+            deleteMutation={deleteCollectionDocument}
             customizeColumns={{
                 name: {
                     header: 'Collection Name',
@@ -63,7 +65,6 @@ export function CollectionListPage() {
                     name: { contains: searchTerm },
                 };
             }}
-            listQuery={collectionListDocument}
             route={Route}
         >
             <PageActionBar>

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_countries/countries.graphql.ts

@@ -58,3 +58,12 @@ export const updateCountryDocument = graphql(`
         }
     }
 `);
+
+export const deleteCountryDocument = graphql(`
+    mutation DeleteCountry($id: ID!) {
+        deleteCountry(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_countries/countries.tsx

@@ -6,7 +6,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { countriesListQuery } from './countries.graphql.js';
+import { countriesListQuery, deleteCountryDocument } from './countries.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_countries/countries')({
     component: CountryListPage,
@@ -17,6 +17,7 @@ function CountryListPage() {
     return (
         <ListPage
             listQuery={countriesListQuery}
+            deleteMutation={deleteCountryDocument}
             route={Route}
             title="Countries"
             defaultVisibility={{

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_customer-groups/customer-groups.graphql.ts

@@ -60,3 +60,12 @@ export const updateCustomerGroupDocument = graphql(`
         }
     }
 `);
+
+export const deleteCustomerGroupDocument = graphql(`
+    mutation DeleteCustomerGroup($id: ID!) {
+        deleteCustomerGroup(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 4 - 1
packages/dashboard/src/routes/_authenticated/_customer-groups/customer-groups.tsx

@@ -7,7 +7,9 @@ import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { CustomerGroupMembersSheet } from './components/customer-group-members-sheet.js';
-import { customerGroupListDocument } from './customer-groups.graphql.js';
+import { customerGroupListDocument, deleteCustomerGroupDocument } from './customer-groups.graphql.js';
+import { useMutation } from '@tanstack/react-query';
+import { api } from '@/graphql/api.js';
 
 export const Route = createFileRoute('/_authenticated/_customer-groups/customer-groups')({
     component: CustomerGroupListPage,
@@ -19,6 +21,7 @@ function CustomerGroupListPage() {
         <ListPage
             title="Customer Groups"
             listQuery={customerGroupListDocument}
+            deleteMutation={deleteCustomerGroupDocument}
             route={Route}
             customizeColumns={{
                 name: {

+ 113 - 12
packages/dashboard/src/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx

@@ -1,13 +1,114 @@
-import { createFileRoute } from '@tanstack/react-router'
-
-export const Route = createFileRoute(
-  '/_authenticated/_customer-groups/customer-groups_/$id',
-)({
-  component: RouteComponent,
-})
-
-function RouteComponent() {
-  return (
-    <div>Hello "/_authenticated/_customer-groups/customer-groups_/$id"!</div>
-  )
+import { ErrorPage } from '@/components/shared/error-page.js';
+import { FormFieldWrapper } from '@/components/shared/form-field-wrapper.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { Button } from '@/components/ui/button.js';
+import { Input } from '@/components/ui/input.js';
+import { NEW_ENTITY_PATH } from '@/constants.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import {
+    CustomFieldsPageBlock,
+    DetailFormGrid,
+    Page,
+    PageActionBar,
+    PageActionBarRight,
+    PageBlock,
+    PageDetailForm,
+    PageLayout,
+    PageTitle,
+} from '@/framework/layout-engine/page-layout.js';
+import { detailPageRouteLoader } from '@/framework/page/detail-page-route-loader.js';
+import { 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 { CustomerGroupMembersTable } from './components/customer-group-members-table.js';
+import {
+    createCustomerGroupDocument,
+    customerGroupDocument,
+    updateCustomerGroupDocument,
+} from './customer-groups.graphql.js';
+
+export const Route = createFileRoute('/_authenticated/_customer-groups/customer-groups_/$id')({
+    component: CustomerGroupDetailPage,
+    loader: detailPageRouteLoader({
+        queryDocument: customerGroupDocument,
+        breadcrumb: (isNew, entity) => [
+            { path: '/customer-groups', label: 'Customer groups' },
+            isNew ? <Trans>New customer group</Trans> : entity?.name,
+        ],
+    }),
+    errorComponent: ({ error }) => <ErrorPage message={error.message} />,
+});
+
+export function CustomerGroupDetailPage() {
+    const params = Route.useParams();
+    const navigate = useNavigate();
+    const creatingNewEntity = params.id === NEW_ENTITY_PATH;
+    const { i18n } = useLingui();
+
+    const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
+        queryDocument: addCustomFields(customerGroupDocument),
+        createDocument: createCustomerGroupDocument,
+        updateDocument: updateCustomerGroupDocument,
+        setValuesForUpdate: entity => {
+            return {
+                id: entity.id,
+                name: entity.name,
+                customFields: entity.customFields,
+            };
+        },
+        params: { id: params.id },
+        onSuccess: async data => {
+            toast.success(i18n.t('Successfully updated customer group'));
+            resetForm();
+            if (creatingNewEntity && data?.id) {
+                await navigate({ to: `../$id`, params: { id: data.id } });
+            }
+        },
+        onError: err => {
+            toast.error(i18n.t('Failed to update customer group'), {
+                description: err instanceof Error ? err.message : 'Unknown error',
+            });
+        },
+    });
+
+    return (
+        <Page>
+            <PageTitle>
+                {creatingNewEntity ? <Trans>New customer group</Trans> : (entity?.name ?? '')}
+            </PageTitle>
+            <PageDetailForm form={form} submitHandler={submitHandler}>
+                <PageActionBar>
+                    <PageActionBarRight>
+                        <PermissionGuard requires={['UpdateCustomerGroup']}>
+                            <Button
+                                type="submit"
+                                disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                            >
+                                <Trans>Update</Trans>
+                            </Button>
+                        </PermissionGuard>
+                    </PageActionBarRight>
+                </PageActionBar>
+                <PageLayout>
+                    <PageBlock column="main">
+                        <DetailFormGrid>
+                            <FormFieldWrapper
+                                control={form.control}
+                                name="name"
+                                label={<Trans>Name</Trans>}
+                                render={({ field }) => <Input placeholder="" {...field} />}
+                            />
+                        </DetailFormGrid>
+                    </PageBlock>
+                    <CustomFieldsPageBlock column="main" entityType="CustomerGroup" control={form.control} />
+                    {entity && (
+                        <PageBlock column="main" title={<Trans>Customers</Trans>}>
+                            <CustomerGroupMembersTable customerGroupId={entity?.id} />
+                        </PageBlock>
+                    )}
+                </PageLayout>
+            </PageDetailForm>
+        </Page>
+    );
 }

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_customers/customers.graphql.ts

@@ -129,6 +129,15 @@ export const updateCustomerDocument = graphql(`
     }
 `);
 
+export const deleteCustomerDocument = graphql(`
+    mutation DeleteCustomer($id: ID!) {
+        deleteCustomer(id: $id) {
+            result
+            message
+        }
+    }
+`);
+
 export const createCustomerAddressDocument = graphql(`
     mutation CreateCustomerAddress($customerId: ID!, $input: CreateAddressInput!) {
         createCustomerAddress(customerId: $customerId, input: $input) {

+ 3 - 2
packages/dashboard/src/routes/_authenticated/_customers/customers.tsx

@@ -3,7 +3,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { CustomerStatusBadge } from './components/customer-status-badge.js';
-import { customerListDocument } from './customers.graphql.js';
+import { customerListDocument, deleteCustomerDocument } from './customers.graphql.js';
 import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
 import { PlusIcon } from 'lucide-react';
 import { PermissionGuard } from '@/components/shared/permission-guard.js';
@@ -18,6 +18,8 @@ export function CustomerListPage() {
     return (
         <ListPage
             title="Customers"
+            listQuery={customerListDocument}
+            deleteMutation={deleteCustomerDocument}
             onSearchTermChange={searchTerm => {
                 return {
                     lastName: {
@@ -36,7 +38,6 @@ export function CustomerListPage() {
                     },
                 };
             }}
-            listQuery={customerListDocument}
             route={Route}
             customizeColumns={{
                 user: {

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_facets/facets.graphql.ts

@@ -93,3 +93,12 @@ export const updateFacetDocument = graphql(`
         }
     }
 `);
+
+export const deleteFacetDocument = graphql(`
+    mutation DeleteFacet($id: ID!) {
+        deleteFacet(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 3 - 2
packages/dashboard/src/routes/_authenticated/_facets/facets.tsx

@@ -6,7 +6,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { facetListDocument } from './facets.graphql.js';
+import { facetListDocument, deleteFacetDocument } from './facets.graphql.js';
 
 import { DetailPageButton } from '@/components/shared/detail-page-button.js';
 import { ResultOf } from 'gql.tada';
@@ -20,6 +20,8 @@ export function FacetListPage() {
     return (
         <ListPage
             title="Facets"
+            listQuery={facetListDocument}
+            deleteMutation={deleteFacetDocument}
             customizeColumns={{
                 name: {
                     header: 'Facet Name',
@@ -65,7 +67,6 @@ export function FacetListPage() {
                     name: { contains: searchTerm },
                 };
             }}
-            listQuery={facetListDocument}
             transformVariables={variables => {
                 return {
                     ...variables,

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.graphql.ts

@@ -72,3 +72,12 @@ export const updatePaymentMethodDocument = graphql(`
         }
     }
 `);
+
+export const deletePaymentMethodDocument = graphql(`
+    mutation DeletePaymentMethod($id: ID!) {
+        deletePaymentMethod(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.tsx

@@ -7,7 +7,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { paymentMethodListQuery } from './payment-methods.graphql.js';
+import { deletePaymentMethodDocument, paymentMethodListQuery } from './payment-methods.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_payment-methods/payment-methods')({
     component: PaymentMethodListPage,
@@ -18,6 +18,7 @@ function PaymentMethodListPage() {
     return (
         <ListPage
             listQuery={paymentMethodListQuery}
+            deleteMutation={deletePaymentMethodDocument}
             route={Route}
             title="Payment Methods"
             defaultVisibility={{

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_product-variants/product-variants.graphql.ts

@@ -112,3 +112,12 @@ export const updateProductVariantDocument = graphql(`
         }
     }
 `);
+
+export const deleteProductVariantDocument = graphql(`
+    mutation DeleteProductVariant($id: ID!) {
+        deleteProductVariant(id: $id) {
+            result
+            message
+        }
+    }
+`);

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

@@ -5,7 +5,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { useLocalFormat } from '@/hooks/use-local-format.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute } from '@tanstack/react-router';
-import { productVariantListDocument } from './product-variants.graphql.js';
+import { deleteProductVariantDocument, productVariantListDocument } from './product-variants.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_product-variants/product-variants')({
     component: ProductListPage,
@@ -17,6 +17,8 @@ export function ProductListPage() {
     return (
         <ListPage
             title={<Trans>Product Variants</Trans>}
+            listQuery={productVariantListDocument}
+            deleteMutation={deleteProductVariantDocument}
             customizeColumns={{
                 name: {
                     header: 'Product Name',
@@ -69,7 +71,6 @@ export function ProductListPage() {
                     name: { contains: searchTerm },
                 };
             }}
-            listQuery={productVariantListDocument}
             route={Route}
         >
             <PageActionBar>

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_products/products.graphql.ts

@@ -105,3 +105,12 @@ export const updateProductDocument = graphql(`
         }
     }
 `);
+
+export const deleteProductDocument = graphql(`
+    mutation DeleteProduct($id: ID!) {
+        deleteProduct(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 4 - 3
packages/dashboard/src/routes/_authenticated/_products/products.tsx

@@ -5,8 +5,8 @@ import { PageActionBar, PageActionBarRight } from '@/framework/layout-engine/pag
 import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
-import { PlusIcon } from 'lucide-react';
-import { productListDocument } from './products.graphql.js';
+import { PlusIcon, TrashIcon } from 'lucide-react';
+import { deleteProductDocument, productListDocument } from './products.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_products/products')({
     component: ProductListPage,
@@ -16,6 +16,8 @@ export const Route = createFileRoute('/_authenticated/_products/products')({
 export function ProductListPage() {
     return (
         <ListPage
+            listQuery={productListDocument}
+            deleteMutation={deleteProductDocument}
             title="Products"
             customizeColumns={{
                 name: {
@@ -28,7 +30,6 @@ export function ProductListPage() {
                     name: { contains: searchTerm },
                 };
             }}
-            listQuery={productListDocument}
             route={Route}
         >
             <PageActionBar>

+ 11 - 0
packages/dashboard/src/routes/_authenticated/_promotions/promotions.graphql.ts

@@ -59,6 +59,7 @@ export const promotionDetailDocument = graphql(
 export const createPromotionDocument = graphql(`
     mutation CreatePromotion($input: CreatePromotionInput!) {
         createPromotion(input: $input) {
+            __typename
             ... on Promotion {
                 id
             }
@@ -73,6 +74,7 @@ export const createPromotionDocument = graphql(`
 export const updatePromotionDocument = graphql(`
     mutation UpdatePromotion($input: UpdatePromotionInput!) {
         updatePromotion(input: $input) {
+            __typename
             ... on Promotion {
                 id
             }
@@ -83,3 +85,12 @@ export const updatePromotionDocument = graphql(`
         }
     }
 `);
+
+export const deletePromotionDocument = graphql(`
+    mutation DeletePromotion($id: ID!) {
+        deletePromotion(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_promotions/promotions.tsx

@@ -6,7 +6,7 @@ import { DetailPageButton } from '@/components/shared/detail-page-button.js';
 import { PlusIcon } from 'lucide-react';
 import { Button } from '@/components/ui/button.js';
 import { PermissionGuard } from '@/components/shared/permission-guard.js';
-import { promotionListDocument } from './promotions.graphql.js';
+import { deletePromotionDocument, promotionListDocument } from './promotions.graphql.js';
 import { BooleanDisplayBadge } from '@/components/data-display/boolean.js';
 
 export const Route = createFileRoute('/_authenticated/_promotions/promotions')({
@@ -18,6 +18,7 @@ function PromotionListPage() {
     return (
         <ListPage
             listQuery={promotionListDocument}
+            deleteMutation={deletePromotionDocument}
             route={Route}
             title="Promotions"
             defaultVisibility={{

+ 23 - 11
packages/dashboard/src/routes/_authenticated/_promotions/promotions_.$id.tsx

@@ -2,9 +2,7 @@ import { DateTimeInput } from '@/components/data-input/datetime-input.js';
 import { ErrorPage } from '@/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/components/shared/form-field-wrapper.js';
 import { PermissionGuard } from '@/components/shared/permission-guard.js';
-import {
-    TranslatableFormFieldWrapper
-} from '@/components/shared/translatable-form-field.js';
+import { TranslatableFormFieldWrapper } from '@/components/shared/translatable-form-field.js';
 import { Button } from '@/components/ui/button.js';
 import { Input } from '@/components/ui/input.js';
 import { Switch } from '@/components/ui/switch.js';
@@ -148,12 +146,14 @@ export function PromotionDetailPage() {
                             />
                             <div></div>
                         </DetailFormGrid>
-                        <TranslatableFormFieldWrapper
-                            control={form.control}
-                            name="description"
-                            label={<Trans>Description</Trans>}
-                            render={({ field }) => <Textarea {...field} />}
-                        />
+                        <div className="mb-4">
+                            <TranslatableFormFieldWrapper
+                                control={form.control}
+                                name="description"
+                                label={<Trans>Description</Trans>}
+                                render={({ field }) => <Textarea {...field} />}
+                            />
+                        </div>
                         <DetailFormGrid>
                             <FormFieldWrapper
                                 control={form.control}
@@ -187,13 +187,25 @@ export function PromotionDetailPage() {
                                 control={form.control}
                                 name="perCustomerUsageLimit"
                                 label={<Trans>Per customer usage limit</Trans>}
-                                render={({ field }) => <Input type="number" {...field} />}
+                                render={({ field }) => (
+                                    <Input
+                                        type="number"
+                                        value={field.value ?? ''}
+                                        onChange={e => field.onChange(e.target.valueAsNumber)}
+                                    />
+                                )}
                             />
                             <FormFieldWrapper
                                 control={form.control}
                                 name="usageLimit"
                                 label={<Trans>Usage limit</Trans>}
-                                render={({ field }) => <Input type="number" {...field} />}
+                                render={({ field }) => (
+                                    <Input
+                                        type="number"
+                                        value={field.value ?? ''}
+                                        onChange={e => field.onChange(e.target.valueAsNumber)}
+                                    />
+                                )}
                             />
                         </DetailFormGrid>
                     </PageBlock>

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_roles/roles.tsx

@@ -11,7 +11,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
 import { LayersIcon, PlusIcon } from 'lucide-react';
 import { ChannelCodeLabel } from '../../../components/shared/channel-code-label.js';
 import { ExpandablePermissions } from './components/expandable-permissions.js';
-import { roleListQuery } from './roles.graphql.js';
+import { deleteRoleDocument, roleListQuery } from './roles.graphql.js';
 export const Route = createFileRoute('/_authenticated/_roles/roles')({
     component: RoleListPage,
     loader: () => ({ breadcrumb: () => <Trans>Roles</Trans> }),
@@ -24,6 +24,7 @@ function RoleListPage() {
         <ListPage
             title="Roles"
             listQuery={roleListQuery}
+            deleteMutation={deleteRoleDocument}
             route={Route}
             defaultVisibility={{
                 description: true,

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_sellers/sellers.graphql.ts

@@ -50,3 +50,12 @@ export const createSellerDocument = graphql(`
         }
     }
 `);
+
+export const deleteSellerDocument = graphql(`
+    mutation DeleteSeller($id: ID!) {
+        deleteSeller(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_sellers/sellers.tsx

@@ -6,7 +6,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { sellerListQuery } from './sellers.graphql.js';
+import { deleteSellerDocument, sellerListQuery } from './sellers.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_sellers/sellers')({
     component: SellerListPage,
@@ -17,6 +17,7 @@ function SellerListPage() {
     return (
         <ListPage
             listQuery={sellerListQuery}
+            deleteMutation={deleteSellerDocument}
             route={Route}
             title="Sellers"
             defaultVisibility={{

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts

@@ -72,3 +72,12 @@ export const updateShippingMethodDocument = graphql(`
         }
     }
 `);
+
+export const deleteShippingMethodDocument = graphql(`
+    mutation DeleteShippingMethod($id: ID!) {
+        deleteShippingMethod(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.tsx

@@ -7,7 +7,7 @@ import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { TestShippingMethodDialog } from './components/test-shipping-method-dialog.js';
-import { shippingMethodListQuery } from './shipping-methods.graphql.js';
+import { deleteShippingMethodDocument, shippingMethodListQuery } from './shipping-methods.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_shipping-methods/shipping-methods')({
     component: ShippingMethodListPage,
@@ -18,6 +18,7 @@ function ShippingMethodListPage() {
     return (
         <ListPage
             listQuery={shippingMethodListQuery}
+            deleteMutation={deleteShippingMethodDocument}
             route={Route}
             title="Shipping Methods"
             defaultVisibility={{

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.graphql.ts

@@ -51,3 +51,12 @@ export const updateStockLocationDocument = graphql(`
         }
     }
 `);
+
+export const deleteStockLocationDocument = graphql(`
+    mutation DeleteStockLocation($id: ID!) {
+        deleteStockLocation(input: { id: $id }) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.tsx

@@ -6,7 +6,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { stockLocationListQuery } from './stock-locations.graphql.js';
+import { deleteStockLocationDocument, stockLocationListQuery } from './stock-locations.graphql.js';
 export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations')({
     component: StockLocationListPage,
     loader: () => ({ breadcrumb: () => <Trans>Stock Locations</Trans> }),
@@ -17,6 +17,7 @@ function StockLocationListPage() {
         <ListPage
             title="Stock Locations"
             listQuery={stockLocationListQuery}
+            deleteMutation={deleteStockLocationDocument}
             route={Route}
             customizeColumns={{
                 name: {

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.graphql.ts

@@ -52,3 +52,12 @@ export const updateTaxCategoryDocument = graphql(`
         }
     }
 `);
+
+export const deleteTaxCategoryDocument = graphql(`
+    mutation DeleteTaxCategory($id: ID!) {
+        deleteTaxCategory(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.tsx

@@ -7,7 +7,7 @@ import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { taxCategoryListQuery } from './tax-categories.graphql.js';
+import { deleteTaxCategoryDocument, taxCategoryListQuery } from './tax-categories.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories')({
     component: TaxCategoryListPage,
@@ -18,6 +18,7 @@ function TaxCategoryListPage() {
     return (
         <ListPage
             listQuery={taxCategoryListQuery}
+            deleteMutation={deleteTaxCategoryDocument}
             route={Route}
             title="Tax Categories"
             defaultVisibility={{

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.graphql.ts

@@ -64,3 +64,12 @@ export const updateTaxRateDocument = graphql(`
         }
     }
 `);
+
+export const deleteTaxRateDocument = graphql(`
+    mutation DeleteTaxRate($id: ID!) {
+        deleteTaxRate(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 3
packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.tsx

@@ -8,9 +8,7 @@ import { api } from '@/graphql/api.js';
 import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { taxCategoryListQuery } from '../_tax-categories/tax-categories.graphql.js';
-import { zoneListQuery } from '../_zones/zones.graphql.js';
-import { taxRateListQuery } from './tax-rates.graphql.js';
+import { deleteTaxRateDocument, taxRateListQuery } from './tax-rates.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates')({
     component: TaxRateListPage,
@@ -21,6 +19,7 @@ function TaxRateListPage() {
     return (
         <ListPage
             listQuery={taxRateListQuery}
+            deleteMutation={deleteTaxRateDocument}
             route={Route}
             title="Tax Rates"
             defaultVisibility={{

+ 9 - 0
packages/dashboard/src/routes/_authenticated/_zones/zones.graphql.ts

@@ -85,3 +85,12 @@ export const removeCountryFromZoneMutation = graphql(`
         }
     }
 `);
+
+export const deleteZoneDocument = graphql(`
+    mutation DeleteZone($id: ID!) {
+        deleteZone(id: $id) {
+            result
+            message
+        }
+    }
+`);

+ 2 - 1
packages/dashboard/src/routes/_authenticated/_zones/zones.tsx

@@ -7,7 +7,7 @@ import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { ZoneCountriesSheet } from './components/zone-countries-sheet.js';
-import { zoneListQuery } from './zones.graphql.js';
+import { deleteZoneDocument, zoneListQuery } from './zones.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_zones/zones')({
     component: ZoneListPage,
@@ -18,6 +18,7 @@ function ZoneListPage() {
     return (
         <ListPage
             listQuery={zoneListQuery}
+            deleteMutation={deleteZoneDocument}
             route={Route}
             title="Zones"
             defaultVisibility={{