Explorar o código

feat(dashboard): Improve display of variant stock levels in lists

Michael Bromley hai 7 meses
pai
achega
1ce87edeeb

+ 6 - 35
packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.tsx

@@ -1,6 +1,6 @@
 import { Money } from '@/components/data-display/money.js';
 import { DetailPageButton } from '@/components/shared/detail-page-button.js';
-import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { StockLevelLabel } from '@/components/shared/stock-level-label.js';
 import { ListPage } from '@/framework/page/list-page.js';
 import { useLocalFormat } from '@/hooks/use-local-format.js';
 import { Trans } from '@/lib/trans.js';
@@ -23,48 +23,19 @@ function ProductListPage() {
             customizeColumns={{
                 name: {
                     header: 'Product Name',
-                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                    cell: ({ row: { original } }) => <DetailPageButton id={original.id} label={original.name} />,
                 },
                 currencyCode: {
-                    cell: ({ cell, row }) => {
-                        const value = cell.getValue();
-                        return formatCurrencyName(value as string, 'full');
-                    },
+                    cell: ({ row: { original } }) => formatCurrencyName(original.currencyCode, 'full'),
                 },
                 price: {
-                    cell: ({ cell, row }) => {
-                        const value = cell.getValue();
-                        const currencyCode = row.original.currencyCode;
-                        if (typeof value === 'number') {
-                            return <Money value={value} currency={currencyCode} />;
-                        }
-                        return value;
-                    },
+                    cell: ({ row: { original } }) => <Money value={original.price} currency={original.currencyCode} />,
                 },
                 priceWithTax: {
-                    cell: ({ cell, row }) => {
-                        const value = cell.getValue();
-                        const currencyCode = row.original.currencyCode;
-                        if (typeof value === 'number') {
-                            return <Money value={value} currency={currencyCode} />;
-                        }
-                        return value;
-                    },
+                    cell: ({ row: { original } }) => <Money value={original.priceWithTax} currency={original.currencyCode} />,
                 },
                 stockLevels: {
-                    cell: ({ cell, row }) => {
-                        const value = cell.getValue();
-                        if (Array.isArray(value)) {
-                            const totalOnHand = value.reduce((acc, curr) => acc + curr.stockOnHand, 0);
-                            const totalAllocated = value.reduce((acc, curr) => acc + curr.stockAllocated, 0);
-                            return (
-                                <span>
-                                    {totalOnHand} / {totalAllocated}
-                                </span>
-                            );
-                        }
-                        return value;
-                    },
+                    cell: ({ row: { original } }) => <StockLevelLabel stockLevels={original.stockLevels} />,
                 },
             }}
             onSearchTermChange={searchTerm => {

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

@@ -1,11 +1,11 @@
-import { PaginatedListDataTable, PaginatedListRefresherRegisterFn } from "@/components/shared/paginated-list-data-table.js";
-import { productVariantListDocument } from "../products.graphql.js";
-import { useState } from "react";
-import { ColumnFiltersState, SortingState } from "@tanstack/react-table";
 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 { Link } from "@tanstack/react-router";
-import { Button } from "@/components/ui/button.js";
+import { DetailPageButton } from "@/index.js";
+import { ColumnFiltersState, SortingState } from "@tanstack/react-table";
+import { useState } from "react";
+import { productVariantListDocument } from "../products.graphql.js";
 
 interface ProductVariantsTableProps {
     productId: string;
@@ -33,42 +33,19 @@ export function ProductVariantsTable({ productId, registerRefresher }: ProductVa
         customizeColumns={{
             name: {
                 header: 'Variant name',
-                cell: ({ row }) => {
-                    const variant = row.original as any;
-                    return (
-                        <Button asChild variant="ghost">
-                            <Link to={`../../product-variants/${variant.id}`}>{variant.name} </Link>
-                        </Button>
-                    );
-                },
+                cell: ({ row: { original } }) => <DetailPageButton href={`../../product-variants/${original.id}`} label={original.name} />,
             },
             currencyCode: {
-                cell: ({ cell, row }) => {
-                    const value = cell.getValue();
-                    return formatCurrencyName(value as string, 'full');
-                },
+                cell: ({ row: { original } }) => formatCurrencyName(original.currencyCode, 'full'),
             },
             price: {
-                cell: ({ cell, row }) => {
-                    const variant = row.original as any;
-                    const value = cell.getValue();
-                    const currencyCode = variant.currencyCode;
-                    if (typeof value === 'number') {
-                        return <Money value={value} currency={currencyCode} />;
-                    }
-                    return value;
-                },
+                cell: ({ row: { original } }) => <Money value={original.price} currency={original.currencyCode} />,
             },
             priceWithTax: {
-                cell: ({ cell, row }) => {
-                    const variant = row.original as any;
-                    const value = cell.getValue();
-                    const currencyCode = variant.currencyCode;
-                    if (typeof value === 'number') {
-                        return <Money value={value} currency={currencyCode} />;
-                    }
-                    return value;
-                },
+                cell: ({ row: { original } }) => <Money value={original.priceWithTax} currency={original.currencyCode} />,
+            },
+            stockLevels: {
+                cell: ({ row: { original } }) => <StockLevelLabel stockLevels={original.stockLevels} />,
             },
         }}
         page={page}

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

@@ -71,7 +71,12 @@ export const productVariantListDocument = graphql(`
                 currencyCode
                 price
                 priceWithTax
+                stockLevels {
+                    stockOnHand
+                    stockAllocated
+                }
             }
+            totalItems
         }
     }
 `);

+ 7 - 2
packages/dashboard/src/lib/components/shared/detail-page-button.tsx

@@ -4,16 +4,21 @@ import { Button } from '../ui/button.js';
 
 export function DetailPageButton({
     id,
+    href,
     label,
     disabled,
 }: {
-    id: string;
     label: string | React.ReactNode;
+    id?: string;
+    href?: string;
     disabled?: boolean;
 }) {
+    if (!id && !href) {
+        return <span>{label}</span>;
+    }
     return (
         <Button asChild variant="ghost" disabled={disabled}>
-            <Link to={`./${id}`}>
+            <Link to={href ?? `./${id}`}>
                 {label}
                 {!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
             </Link>

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

@@ -114,6 +114,14 @@ export type FacetedFilterConfig<T extends TypedDocumentNode<any, any>> = {
     [Key in AllItemFieldKeys<T>]?: FacetedFilter;
 };
 
+export type ListQueryFields<T extends TypedDocumentNode<any, any>> = {
+    [Key in keyof ResultOf<T>]: ResultOf<T>[Key] extends { items: infer U }
+        ? U extends any[]
+            ? U[number]
+            : never
+        : never;
+}[keyof ResultOf<T>];
+
 export type ListQueryShape =
     | {
           [key: string]: {
@@ -185,7 +193,7 @@ export type PaginatedListRefresherRegisterFn = (refreshFn: () => void) => void;
 
 export interface PaginatedListDataTableProps<
     T extends TypedDocumentNode<U, V>,
-    U extends any,
+    U extends ListQueryShape,
     V extends ListQueryOptionsShape,
     AC extends AdditionalColumns<T>,
 > {
@@ -195,7 +203,7 @@ export interface PaginatedListDataTableProps<
     transformVariables?: (variables: V) => V;
     customizeColumns?: CustomizeColumnConfig<T>;
     additionalColumns?: AC;
-    defaultColumnOrder?: (AllItemFieldKeys<T> | AC[number]['id'])[];
+    defaultColumnOrder?: (keyof ListQueryFields<T> | keyof AC | CustomFieldKeysOfItem<ListQueryFields<T>>)[];
     defaultVisibility?: Partial<Record<AllItemFieldKeys<T>, boolean>>;
     onSearchTermChange?: (searchTerm: string) => NonNullable<V['options']>['filter'];
     page: number;
@@ -407,10 +415,10 @@ export function PaginatedListDataTable<
             // appear as the first columns in sequence, and leave the remainder in the
             // existing order
             const orderedColumns = finalColumns
-                .filter(column => column.id && defaultColumnOrder.includes(column.id))
-                .sort((a, b) => defaultColumnOrder.indexOf(a.id) - defaultColumnOrder.indexOf(b.id));
+                .filter(column => column.id && defaultColumnOrder.includes(column.id as any))
+                .sort((a, b) => defaultColumnOrder.indexOf(a.id as any) - defaultColumnOrder.indexOf(b.id as any));
             const remainingColumns = finalColumns.filter(
-                column => !column.id || !defaultColumnOrder.includes(column.id),
+                column => !column.id || !defaultColumnOrder.includes(column.id as any),
             );
             finalColumns = [...orderedColumns, ...remainingColumns];
         }

+ 24 - 0
packages/dashboard/src/lib/components/shared/stock-level-label.tsx

@@ -0,0 +1,24 @@
+import { useLingui } from '../../lib/trans.js';
+
+export type StockLevel = {
+    stockOnHand: number;
+    stockAllocated: number;
+};
+
+export function StockLevelLabel({ stockLevels }: { stockLevels: StockLevel[] }) {
+    const { i18n } = useLingui();
+    
+    if (!Array.isArray(stockLevels)) {
+        return null;
+    }
+    const totalOnHand = stockLevels.reduce((acc, curr) => acc + curr.stockOnHand, 0);
+    const totalAllocated = stockLevels.reduce((acc, curr) => acc + curr.stockAllocated, 0);
+    
+    return (
+        <span 
+            title={`${i18n.t('Stock on hand')}: ${totalOnHand}, ${i18n.t('Stock allocated')}: ${totalAllocated}`}
+        >
+            {totalOnHand} <span className="text-muted-foreground">/ {totalAllocated}</span>
+        </span>
+    );
+}

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

@@ -5,6 +5,7 @@ import {
     FacetedFilterConfig,
     ListQueryOptionsShape,
     ListQueryShape,
+    ListQueryFields,
     PaginatedListDataTable,
     RowAction,
 } from '@/components/shared/paginated-list-data-table.js';
@@ -13,7 +14,6 @@ import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { AnyRoute, AnyRouter, useNavigate } from '@tanstack/react-router';
 import { ColumnFiltersState, SortingState, Table } from '@tanstack/react-table';
 import { TableOptions } from '@tanstack/table-core';
-import { ResultOf } from 'gql.tada';
 
 import { addCustomFields } from '../document-introspection/add-custom-fields.js';
 import {
@@ -24,14 +24,6 @@ import {
     PageTitle,
 } from '../layout-engine/page-layout.js';
 
-type ListQueryFields<T extends TypedDocumentNode<any, any>> = {
-    [Key in keyof ResultOf<T>]: ResultOf<T>[Key] extends { items: infer U }
-        ? U extends any[]
-            ? U[number]
-            : never
-        : never;
-}[keyof ResultOf<T>];
-
 /**
  * @description
  * **Status: Developer Preview**

+ 1 - 0
packages/dashboard/src/lib/index.ts

@@ -67,6 +67,7 @@ export * from './components/shared/rich-text-editor.js';
 export * from './components/shared/role-code-label.js';
 export * from './components/shared/role-selector.js';
 export * from './components/shared/seller-selector.js';
+export * from './components/shared/stock-level-label.js';
 export * from './components/shared/tax-category-selector.js';
 export * from './components/shared/translatable-form-field.js';
 export * from './components/shared/vendure-image.js';