Browse Source

fix(dashboard): Make variant detail breadcrumbs context-aware

Michael Bromley 7 months ago
parent
commit
07c174c391

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

@@ -40,8 +40,15 @@ export const Route = createFileRoute('/_authenticated/_product-variants/product-
     component: ProductVariantDetailPage,
     loader: detailPageRouteLoader({
         queryDocument: productVariantDetailDocument,
-        breadcrumb(_isNew, entity) {
-            return [{ path: '/product-variants', label: 'Product variants' }, entity?.name];
+        breadcrumb(_isNew, entity, location) {
+            if ((location.search as any).from === 'product') {
+                return [
+                    { path: '/product', label: 'Products' },
+                    { path: `/products/${entity?.product.id}`, label: entity?.product.name ?? '' },
+                    entity?.name,
+                ];
+            }
+            return [{ path: '/product-variants', label: 'Product Variants' }, entity?.name];
         },
     }),
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,

+ 74 - 55
packages/dashboard/src/app/routes/_authenticated/_products/components/product-variants-table.tsx

@@ -1,12 +1,15 @@
-import { Money } from "@/components/data-display/money.js";
-import { PaginatedListDataTable, PaginatedListRefresherRegisterFn } from "@/components/shared/paginated-list-data-table.js";
-import { StockLevelLabel } from "@/components/shared/stock-level-label.js";
-import { useLocalFormat } from "@/hooks/use-local-format.js";
-import { DetailPageButton } from "@/index.js";
-import { ColumnFiltersState, SortingState } from "@tanstack/react-table";
-import { useState } from "react";
-import { productVariantListDocument } from "../products.graphql.js";
+import { Money } from '@/components/data-display/money.js';
+import {
+    PaginatedListDataTable,
+    PaginatedListRefresherRegisterFn,
+} from '@/components/shared/paginated-list-data-table.js';
+import { StockLevelLabel } from '@/components/shared/stock-level-label.js';
 import { graphql } from '@/graphql/graphql.js';
+import { useLocalFormat } from '@/hooks/use-local-format.js';
+import { DetailPageButton } from '@/index.js';
+import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
+import { useState } from 'react';
+import { productVariantListDocument } from '../products.graphql.js';
 
 export const deleteProductVariantDocument = graphql(`
     mutation DeleteProductVariant($id: ID!) {
@@ -17,62 +20,78 @@ export const deleteProductVariantDocument = graphql(`
     }
 `);
 
-
 interface ProductVariantsTableProps {
     productId: string;
     registerRefresher?: PaginatedListRefresherRegisterFn;
+    fromProductDetailPage?: boolean;
 }
 
-export function ProductVariantsTable({ productId, registerRefresher }: ProductVariantsTableProps) {
+export function ProductVariantsTable({
+    productId,
+    registerRefresher,
+    fromProductDetailPage,
+}: ProductVariantsTableProps) {
     const { formatCurrencyName } = useLocalFormat();
     const [page, setPage] = useState(1);
     const [pageSize, setPageSize] = useState(10);
     const [sorting, setSorting] = useState<SortingState>([]);
     const [filters, setFilters] = useState<ColumnFiltersState>([]);
 
-    return <PaginatedListDataTable
-        registerRefresher={registerRefresher}
-        listQuery={productVariantListDocument}
-        deleteMutation={deleteProductVariantDocument}
-        transformVariables={variables => ({
-            ...variables,
-            productId,
-        })}
-        defaultVisibility={{
-            id: false,
-            currencyCode: false,
-        }}
-        customizeColumns={{
-            name: {
-                header: 'Variant name',
-                cell: ({ row: { original } }) => <DetailPageButton href={`../../product-variants/${original.id}`} label={original.name} />,
-            },
-            currencyCode: {
-                cell: ({ row: { original } }) => formatCurrencyName(original.currencyCode, 'full'),
-            },
-            price: {
-                cell: ({ row: { original } }) => <Money value={original.price} currency={original.currencyCode} />,
-            },
-            priceWithTax: {
-                cell: ({ row: { original } }) => <Money value={original.priceWithTax} currency={original.currencyCode} />,
-            },
-            stockLevels: {
-                cell: ({ row: { original } }) => <StockLevelLabel stockLevels={original.stockLevels} />,
-            },
-        }}
-        page={page}
-        itemsPerPage={pageSize}
-        sorting={sorting}
-        columnFilters={filters}
-        onPageChange={(_, page, perPage) => {
-            setPage(page);
-            setPageSize(perPage);
-        }}
-        onSortChange={(_, sorting) => {
-            setSorting(sorting);
-        }}
-        onFilterChange={(_, filters) => {
-            setFilters(filters);
-        }}
-    />;
+    return (
+        <PaginatedListDataTable
+            registerRefresher={registerRefresher}
+            listQuery={productVariantListDocument}
+            deleteMutation={deleteProductVariantDocument}
+            transformVariables={variables => ({
+                ...variables,
+                productId,
+            })}
+            defaultVisibility={{
+                id: false,
+                currencyCode: false,
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Variant name',
+                    cell: ({ row: { original } }) => (
+                        <DetailPageButton
+                            href={`../../product-variants/${original.id}`}
+                            label={original.name}
+                            search={fromProductDetailPage ? { from: 'product' } : undefined}
+                        />
+                    ),
+                },
+                currencyCode: {
+                    cell: ({ row: { original } }) => formatCurrencyName(original.currencyCode, 'full'),
+                },
+                price: {
+                    cell: ({ row: { original } }) => (
+                        <Money value={original.price} currency={original.currencyCode} />
+                    ),
+                },
+                priceWithTax: {
+                    cell: ({ row: { original } }) => (
+                        <Money value={original.priceWithTax} currency={original.currencyCode} />
+                    ),
+                },
+                stockLevels: {
+                    cell: ({ row: { original } }) => <StockLevelLabel stockLevels={original.stockLevels} />,
+                },
+            }}
+            page={page}
+            itemsPerPage={pageSize}
+            sorting={sorting}
+            columnFilters={filters}
+            onPageChange={(_, page, perPage) => {
+                setPage(page);
+                setPageSize(perPage);
+            }}
+            onSortChange={(_, sorting) => {
+                setSorting(sorting);
+            }}
+            onFilterChange={(_, filters) => {
+                setFilters(filters);
+            }}
+        />
+    );
 }

+ 1 - 0
packages/dashboard/src/app/routes/_authenticated/_products/products_.$id.tsx

@@ -148,6 +148,7 @@ function ProductDetailPage() {
                             registerRefresher={refresher => {
                                 refreshRef.current = refresher;
                             }}
+                            fromProductDetailPage={true}
                         />
                         <div className="mt-4">
                             <AddProductVariantDialog

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

@@ -7,18 +7,20 @@ export function DetailPageButton({
     href,
     label,
     disabled,
+    search,
 }: {
     label: string | React.ReactNode;
     id?: string;
     href?: string;
     disabled?: boolean;
+    search?: Record<string, string>;
 }) {
     if (!id && !href) {
         return <span>{label}</span>;
     }
     return (
         <Button asChild variant="ghost" disabled={disabled}>
-            <Link to={href ?? `./${id}`}>
+            <Link to={href ?? `./${id}`} search={search ?? {}}>
                 {label}
                 {!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
             </Link>

+ 9 - 3
packages/dashboard/src/lib/framework/page/detail-page-route-loader.tsx

@@ -2,7 +2,7 @@ import { NEW_ENTITY_PATH } from '@/constants.js';
 
 import { PageBreadcrumb } from '@/components/layout/generated-breadcrumbs.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { FileBaseRouteOptions } from '@tanstack/react-router';
+import { FileBaseRouteOptions, ParsedLocation } from '@tanstack/react-router';
 import { addCustomFields } from '../document-introspection/add-custom-fields.js';
 import { getQueryName, getQueryTypeFieldInfo } from '../document-introspection/get-document-structure.js';
 import { DetailEntity } from './page-types.js';
@@ -10,7 +10,11 @@ import { getDetailQueryOptions } from './use-detail-page.js';
 
 export interface DetailPageRouteLoaderConfig<T extends TypedDocumentNode<any, any>> {
     queryDocument: T;
-    breadcrumb: (isNew: boolean, entity: DetailEntity<T>) => Array<PageBreadcrumb | undefined>;
+    breadcrumb: (
+        isNew: boolean,
+        entity: DetailEntity<T>,
+        location: ParsedLocation,
+    ) => Array<PageBreadcrumb | undefined>;
 }
 
 export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
@@ -20,9 +24,11 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
     const loader: FileBaseRouteOptions<any, any>['loader'] = async ({
         context,
         params,
+        location,
     }: {
         context: any;
         params: any;
+        location: ParsedLocation;
     }) => {
         if (!params.id) {
             throw new Error('ID param is required');
@@ -42,7 +48,7 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
             throw new Error(`${entityName} with the ID ${params.id} was not found`);
         }
         return {
-            breadcrumb: breadcrumb(isNew, result?.[entityField]),
+            breadcrumb: breadcrumb(isNew, result?.[entityField], location),
         };
     };
     return loader;