Browse Source

fix(dashboard): Fix several issues on the list pages (#3851)

Michael Bromley 3 months ago
parent
commit
8a5c2bb6db

+ 2 - 0
packages/core/e2e/graphql/fragments.ts

@@ -23,6 +23,8 @@ export const ADMINISTRATOR_FRAGMENT = gql`
 export const ASSET_FRAGMENT = gql`
 export const ASSET_FRAGMENT = gql`
     fragment Asset on Asset {
     fragment Asset on Asset {
         id
         id
+        createdAt
+        updatedAt
         name
         name
         fileSize
         fileSize
         mimeType
         mimeType

+ 4 - 4
packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx

@@ -32,7 +32,7 @@ function AdministratorListPage() {
             }}
             }}
             additionalColumns={{
             additionalColumns={{
                 name: {
                 name: {
-                    id: 'name',
+                    meta: { dependencies: ['id', 'firstName', 'lastName'] },
                     header: () => <Trans>Name</Trans>,
                     header: () => <Trans>Name</Trans>,
                     cell: ({ row }) => (
                     cell: ({ row }) => (
                         <DetailPageButton
                         <DetailPageButton
@@ -42,12 +42,12 @@ function AdministratorListPage() {
                     ),
                     ),
                 },
                 },
                 roles: {
                 roles: {
-                    id: 'roles',
+                    meta: { dependencies: ['user'] },
                     header: () => <Trans>Roles</Trans>,
                     header: () => <Trans>Roles</Trans>,
                     cell: ({ row }) => {
                     cell: ({ row }) => {
                         return (
                         return (
                             <div className="flex flex-wrap gap-2">
                             <div className="flex flex-wrap gap-2">
-                                {row.original.user.roles.map(role => {
+                                {row.original.user?.roles.map(role => {
                                     return (
                                     return (
                                         <Badge variant="secondary" key={role.id}>
                                         <Badge variant="secondary" key={role.id}>
                                             <RoleCodeLabel code={role.code} />
                                             <RoleCodeLabel code={role.code} />
@@ -61,7 +61,6 @@ function AdministratorListPage() {
             }}
             }}
             customizeColumns={{
             customizeColumns={{
                 emailAddress: {
                 emailAddress: {
-                    id: 'Identifier',
                     header: () => <Trans>Identifier</Trans>,
                     header: () => <Trans>Identifier</Trans>,
                     cell: ({ row }) => {
                     cell: ({ row }) => {
                         return <div>{row.original.emailAddress}</div>;
                         return <div>{row.original.emailAddress}</div>;
@@ -70,6 +69,7 @@ function AdministratorListPage() {
             }}
             }}
             defaultVisibility={{
             defaultVisibility={{
                 emailAddress: true,
                 emailAddress: true,
+                name: true,
             }}
             }}
             defaultColumnOrder={['name', 'emailAddress', 'roles']}
             defaultColumnOrder={['name', 'emailAddress', 'roles']}
             bulkActions={[
             bulkActions={[

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

@@ -3,6 +3,8 @@ import { graphql } from '@/vdb/graphql/graphql.js';
 export const countryItemFragment = graphql(`
 export const countryItemFragment = graphql(`
     fragment CountryItem on Country {
     fragment CountryItem on Country {
         id
         id
+        createdAt
+        updatedAt
         name
         name
         code
         code
         enabled
         enabled

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

@@ -10,6 +10,10 @@ export const customerListDocument = graphql(`
                 firstName
                 firstName
                 lastName
                 lastName
                 emailAddress
                 emailAddress
+                groups {
+                    id
+                    name
+                }
                 user {
                 user {
                     id
                     id
                     verified
                     verified

+ 18 - 7
packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx

@@ -1,5 +1,6 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
 import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
+import { Badge } from '@/vdb/components/ui/badge.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
 import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
@@ -43,15 +44,27 @@ function CustomerListPage() {
             customizeColumns={{
             customizeColumns={{
                 user: {
                 user: {
                     header: () => <Trans>Status</Trans>,
                     header: () => <Trans>Status</Trans>,
-                    cell: ({ cell }) => {
-                        const value = cell.getValue();
+                    cell: ({ row }) => {
+                        const value = row.original.user;
                         return <CustomerStatusBadge user={value} />;
                         return <CustomerStatusBadge user={value} />;
                     },
                     },
                 },
                 },
+                groups: {
+                    cell: ({ row }) => {
+                        return row.original.groups?.map(g => (
+                            <Badge variant="secondary" key={g.id}>
+                                {g.name}
+                            </Badge>
+                        ));
+                    },
+                },
             }}
             }}
             additionalColumns={{
             additionalColumns={{
                 name: {
                 name: {
                     id: 'name',
                     id: 'name',
+                    meta: {
+                        dependencies: ['id', 'firstName', 'lastName'],
+                    },
                     header: () => <Trans>Name</Trans>,
                     header: () => <Trans>Name</Trans>,
                     cell: ({ row }) => {
                     cell: ({ row }) => {
                         const value = `${row.original.firstName} ${row.original.lastName}`;
                         const value = `${row.original.firstName} ${row.original.lastName}`;
@@ -61,11 +74,9 @@ function CustomerListPage() {
             }}
             }}
             defaultColumnOrder={['name', 'emailAddress', 'user', 'createdAt']}
             defaultColumnOrder={['name', 'emailAddress', 'user', 'createdAt']}
             defaultVisibility={{
             defaultVisibility={{
-                id: false,
-                createdAt: false,
-                updatedAt: false,
-                firstName: false,
-                lastName: false,
+                name: true,
+                emailAddress: true,
+                user: true,
             }}
             }}
             bulkActions={[
             bulkActions={[
                 {
                 {

+ 1 - 1
packages/dashboard/src/app/routes/_authenticated/_orders/components/order-table.tsx

@@ -160,7 +160,7 @@ export function OrderTable({ order, pageId }: Readonly<OrderTableProps>) {
         enableSorting: false,
         enableSorting: false,
     });
     });
 
 
-    const columnVisibility = getColumnVisibility(fields, defaultColumnVisibility, customFieldColumnNames);
+    const columnVisibility = getColumnVisibility(columns, defaultColumnVisibility, customFieldColumnNames);
     const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
     const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
     const data = order.lines;
     const data = order.lines;
 
 

+ 8 - 0
packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.tsx

@@ -25,6 +25,14 @@ function ProductListPage() {
             pageId="product-variant-list"
             pageId="product-variant-list"
             title={<Trans>Product Variants</Trans>}
             title={<Trans>Product Variants</Trans>}
             listQuery={productVariantListDocument}
             listQuery={productVariantListDocument}
+            defaultVisibility={{
+                featuredAsset: true,
+                name: true,
+                sku: true,
+                priceWithTax: true,
+                enabled: true,
+                stockLevels: true,
+            }}
             bulkActions={[
             bulkActions={[
                 {
                 {
                     component: AssignProductVariantsToChannelBulkAction,
                     component: AssignProductVariantsToChannelBulkAction,

+ 6 - 0
packages/dashboard/src/app/routes/_authenticated/_products/products.tsx

@@ -54,6 +54,12 @@ function ProductListPage() {
                     name: { contains: searchTerm },
                     name: { contains: searchTerm },
                 };
                 };
             }}
             }}
+            defaultVisibility={{
+                name: true,
+                featuredAsset: true,
+                slug: true,
+                enabled: true,
+            }}
             route={Route}
             route={Route}
             bulkActions={[
             bulkActions={[
                 {
                 {

+ 21 - 4
packages/dashboard/src/lib/components/data-table/data-table-utils.ts

@@ -1,4 +1,5 @@
-import { FieldInfo } from '@/vdb/framework/document-introspection/get-document-structure.js';
+import { AccessorFnColumnDef } from '@tanstack/react-table';
+import { AccessorKeyColumnDef } from '@tanstack/table-core';
 
 
 /**
 /**
  * Returns the default column visibility configuration.
  * Returns the default column visibility configuration.
@@ -13,7 +14,7 @@ import { FieldInfo } from '@/vdb/framework/document-introspection/get-document-s
  * ```
  * ```
  */
  */
 export function getColumnVisibility(
 export function getColumnVisibility(
-    fields: FieldInfo[],
+    columns: Array<AccessorKeyColumnDef<any> | AccessorFnColumnDef<any>>,
     defaultVisibility?: Record<string, boolean | undefined>,
     defaultVisibility?: Record<string, boolean | undefined>,
     customFieldColumnNames?: string[],
     customFieldColumnNames?: string[],
 ): Record<string, boolean> {
 ): Record<string, boolean> {
@@ -23,12 +24,28 @@ export function getColumnVisibility(
         id: false,
         id: false,
         createdAt: false,
         createdAt: false,
         updatedAt: false,
         updatedAt: false,
-        ...(allDefaultsTrue ? { ...Object.fromEntries(fields.map(f => [f.name, false])) } : {}),
-        ...(allDefaultsFalse ? { ...Object.fromEntries(fields.map(f => [f.name, true])) } : {}),
+        ...(allDefaultsTrue ? { ...Object.fromEntries(columns.map(f => [f.id, false])) } : {}),
+        ...(allDefaultsFalse ? { ...Object.fromEntries(columns.map(f => [f.id, true])) } : {}),
         // Make custom fields hidden by default unless overridden
         // Make custom fields hidden by default unless overridden
         ...(customFieldColumnNames
         ...(customFieldColumnNames
             ? { ...Object.fromEntries(customFieldColumnNames.map(f => [f, false])) }
             ? { ...Object.fromEntries(customFieldColumnNames.map(f => [f, false])) }
             : {}),
             : {}),
         ...defaultVisibility,
         ...defaultVisibility,
+        selection: true,
+        actions: true,
     };
     };
 }
 }
+
+/**
+ * Ensures that the default column order always starts with `id`, `createdAt`, `deletedAt`
+ */
+export function getStandardizedDefaultColumnOrder<T extends string | number | symbol>(
+    defaultColumnOrder?: T[],
+): T[] {
+    const standardFirstColumns = new Set(['id', 'createdAt', 'updatedAt']);
+    if (!defaultColumnOrder) {
+        return [...standardFirstColumns] as T[];
+    }
+    const rest = defaultColumnOrder.filter(c => !standardFirstColumns.has(c as string));
+    return [...standardFirstColumns, ...rest] as T[];
+}

+ 6 - 8
packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx

@@ -10,7 +10,7 @@ import { api } from '@/vdb/graphql/api.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { Trans, useLingui } from '@lingui/react/macro';
 import { Trans, useLingui } from '@lingui/react/macro';
 import { useMutation } from '@tanstack/react-query';
 import { useMutation } from '@tanstack/react-query';
-import { AccessorKeyColumnDef, createColumnHelper, Row } from '@tanstack/react-table';
+import { AccessorFnColumnDef, AccessorKeyColumnDef, createColumnHelper, Row } from '@tanstack/react-table';
 import { EllipsisIcon, TrashIcon } from 'lucide-react';
 import { EllipsisIcon, TrashIcon } from 'lucide-react';
 import { useMemo } from 'react';
 import { useMemo } from 'react';
 import { toast } from 'sonner';
 import { toast } from 'sonner';
@@ -36,12 +36,7 @@ import {
 } from '../ui/alert-dialog.js';
 } from '../ui/alert-dialog.js';
 import { Button } from '../ui/button.js';
 import { Button } from '../ui/button.js';
 import { Checkbox } from '../ui/checkbox.js';
 import { Checkbox } from '../ui/checkbox.js';
-import {
-    DropdownMenu,
-    DropdownMenuContent,
-    DropdownMenuItem,
-    DropdownMenuTrigger,
-} from '../ui/dropdown-menu.js';
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu.js';
 import { DataTableColumnHeader } from './data-table-column-header.js';
 import { DataTableColumnHeader } from './data-table-column-header.js';
 
 
 /**
 /**
@@ -78,7 +73,10 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
     includeSelectionColumn?: boolean;
     includeSelectionColumn?: boolean;
     includeActionsColumn?: boolean;
     includeActionsColumn?: boolean;
     enableSorting?: boolean;
     enableSorting?: boolean;
-}>) {
+}>): {
+    columns: Array<AccessorKeyColumnDef<any> | AccessorFnColumnDef<any>>;
+    customFieldColumnNames: string[];
+} {
     const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
     const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
     const allBulkActions = useAllBulkActions(bulkActions ?? []);
     const allBulkActions = useAllBulkActions(bulkActions ?? []);
 
 

+ 21 - 18
packages/dashboard/src/lib/components/shared/paginated-list-data-table.tsx

@@ -14,7 +14,7 @@ import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { ColumnFiltersState, ColumnSort, SortingState, Table } from '@tanstack/react-table';
 import { ColumnFiltersState, ColumnSort, SortingState, Table } from '@tanstack/react-table';
 import { ColumnDef, Row, TableOptions, VisibilityState } from '@tanstack/table-core';
 import { ColumnDef, Row, TableOptions, VisibilityState } from '@tanstack/table-core';
 import React from 'react';
 import React from 'react';
-import { getColumnVisibility } from '../data-table/data-table-utils.js';
+import { getColumnVisibility, getStandardizedDefaultColumnOrder } from '../data-table/data-table-utils.js';
 import { useGeneratedColumns } from '../data-table/use-generated-columns.js';
 import { useGeneratedColumns } from '../data-table/use-generated-columns.js';
 
 
 // Type that identifies a paginated list structure (has items array and totalItems)
 // Type that identifies a paginated list structure (has items array and totalItems)
@@ -89,21 +89,25 @@ export type AllItemFieldKeys<T extends TypedDocumentNode<any, any>> =
     | keyof PaginatedListItemFields<T>
     | keyof PaginatedListItemFields<T>
     | CustomFieldKeysOfItem<PaginatedListItemFields<T>>;
     | CustomFieldKeysOfItem<PaginatedListItemFields<T>>;
 
 
-export type CustomizeColumnConfig<T extends TypedDocumentNode<any, any>> = {
-    [Key in AllItemFieldKeys<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>, any>> & {
-        meta?: {
-            /**
-             * @description
-             * Columns that rely on _other_ columns in order to correctly render,
-             * can declare those other columns as dependencies in order to ensure that
-             * those columns are always fetched, even when those columns are not explicitly
-             * included in the visible table columns.
-             */
-            dependencies?: Array<AllItemFieldKeys<T>>;
-        };
+export type ColumnDefWithMetaDependencies<T extends TypedDocumentNode<any, any>> = Partial<
+    ColumnDef<T, any>
+> & {
+    meta?: {
+        /**
+         * @description
+         * Columns that rely on _other_ columns in order to correctly render,
+         * can declare those other columns as dependencies in order to ensure that
+         * those columns are always fetched, even when those columns are not explicitly
+         * included in the visible table columns.
+         */
+        dependencies?: Array<AllItemFieldKeys<T>>;
     };
     };
 };
 };
 
 
+export type CustomizeColumnConfig<T extends TypedDocumentNode<any, any>> = {
+    [Key in AllItemFieldKeys<T>]?: ColumnDefWithMetaDependencies<PaginatedListItemFields<T>>;
+};
+
 export type FacetedFilterConfig<T extends TypedDocumentNode<any, any>> = {
 export type FacetedFilterConfig<T extends TypedDocumentNode<any, any>> = {
     [Key in AllItemFieldKeys<T>]?: FacetedFilter;
     [Key in AllItemFieldKeys<T>]?: FacetedFilter;
 };
 };
@@ -145,7 +149,7 @@ export type ListQueryOptionsShape = {
 };
 };
 
 
 export type AdditionalColumns<T extends TypedDocumentNode<any, any>> = {
 export type AdditionalColumns<T extends TypedDocumentNode<any, any>> = {
-    [key: string]: ColumnDef<PaginatedListItemFields<T>>;
+    [key: string]: ColumnDefWithMetaDependencies<PaginatedListItemFields<T>>;
 };
 };
 
 
 export interface PaginatedListContext {
 export interface PaginatedListContext {
@@ -419,15 +423,14 @@ export function PaginatedListDataTable<
         bulkActions,
         bulkActions,
         deleteMutation,
         deleteMutation,
         additionalColumns,
         additionalColumns,
-        defaultColumnOrder,
+        defaultColumnOrder: getStandardizedDefaultColumnOrder(defaultColumnOrder),
     });
     });
-
-    const columnVisibility = getColumnVisibility(fields, defaultVisibility, customFieldColumnNames);
+    const columnVisibility = getColumnVisibility(columns, defaultVisibility, customFieldColumnNames);
     // Get the actual visible columns and only fetch those
     // Get the actual visible columns and only fetch those
     const visibleColumns = columns
     const visibleColumns = columns
         // Filter out invisible columns, but _always_ select "id"
         // Filter out invisible columns, but _always_ select "id"
         // because it is usually needed.
         // because it is usually needed.
-        .filter(c => columnVisibility[c.id as string] || c.id === 'id')
+        .filter(c => columnVisibility[c.id as string] !== false || c.id === 'id')
         .map(c => ({
         .map(c => ({
             name: c.id as string,
             name: c.id as string,
             isCustomField: (c.meta as any)?.isCustomField ?? false,
             isCustomField: (c.meta as any)?.isCustomField ?? false,