Просмотр исходного кода

feat(dashboard): Expose bulk actions per row in data tables (#3820)

Michael Bromley 4 месяцев назад
Родитель
Сommit
de79737428
24 измененных файлов с 62 добавлено и 99 удалено
  1. 1 2
      packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx
  2. 1 2
      packages/dashboard/src/app/routes/_authenticated/_channels/channels.tsx
  3. 2 16
      packages/dashboard/src/app/routes/_authenticated/_collections/collections.tsx
  4. 0 33
      packages/dashboard/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx
  5. 1 2
      packages/dashboard/src/app/routes/_authenticated/_countries/countries.tsx
  6. 1 2
      packages/dashboard/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx
  7. 1 2
      packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx
  8. 0 1
      packages/dashboard/src/app/routes/_authenticated/_facets/facets.tsx
  9. 1 2
      packages/dashboard/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx
  10. 1 2
      packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.tsx
  11. 1 2
      packages/dashboard/src/app/routes/_authenticated/_products/products.tsx
  12. 1 2
      packages/dashboard/src/app/routes/_authenticated/_promotions/promotions.tsx
  13. 1 2
      packages/dashboard/src/app/routes/_authenticated/_roles/roles.tsx
  14. 1 2
      packages/dashboard/src/app/routes/_authenticated/_sellers/sellers.tsx
  15. 1 2
      packages/dashboard/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx
  16. 1 2
      packages/dashboard/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx
  17. 1 2
      packages/dashboard/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx
  18. 1 2
      packages/dashboard/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx
  19. 1 2
      packages/dashboard/src/app/routes/_authenticated/_zones/zones.tsx
  20. 5 14
      packages/dashboard/src/lib/components/data-table/data-table-bulk-actions.tsx
  21. 19 0
      packages/dashboard/src/lib/components/data-table/use-all-bulk-actions.ts
  22. 12 3
      packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx
  23. 1 0
      packages/dashboard/src/lib/components/shared/paginated-list-data-table.tsx
  24. 7 0
      packages/dashboard/src/lib/framework/page/list-page.tsx

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

@@ -8,7 +8,7 @@ import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { administratorListDocument, deleteAdministratorDocument } from './administrators.graphql.js';
+import { administratorListDocument } from './administrators.graphql.js';
 import { DeleteAdministratorsBulkAction } from './components/administrator-bulk-actions.js';
 
 export const Route = createFileRoute('/_authenticated/_administrators/administrators')({
@@ -22,7 +22,6 @@ function AdministratorListPage() {
             pageId="administrator-list"
             title="Administrators"
             listQuery={administratorListDocument}
-            deleteMutation={deleteAdministratorDocument}
             route={Route}
             onSearchTermChange={searchTerm => {
                 return {

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

@@ -8,7 +8,7 @@ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
-import { channelListQuery, deleteChannelDocument } from './channels.graphql.js';
+import { channelListQuery } from './channels.graphql.js';
 import { DeleteChannelsBulkAction } from './components/channel-bulk-actions.js';
 
 export const Route = createFileRoute('/_authenticated/_channels/channels')({
@@ -23,7 +23,6 @@ function ChannelListPage() {
             pageId="channel-list"
             title="Channels"
             listQuery={channelListQuery}
-            deleteMutation={deleteChannelDocument}
             route={Route}
             defaultVisibility={{
                 code: true,

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

@@ -10,11 +10,11 @@ import { createFileRoute, Link } from '@tanstack/react-router';
 import { ExpandedState, getExpandedRowModel } from '@tanstack/react-table';
 import { TableOptions } from '@tanstack/table-core';
 import { ResultOf } from 'gql.tada';
-import { Folder, FolderOpen, FolderTreeIcon, PlusIcon } from 'lucide-react';
+import { Folder, FolderOpen, PlusIcon } from 'lucide-react';
 import { useState } from 'react';
 
 import { Badge } from '@/vdb/components/ui/badge.js';
-import { collectionListDocument, deleteCollectionDocument } from './collections.graphql.js';
+import { collectionListDocument } from './collections.graphql.js';
 import {
     AssignCollectionsToChannelBulkAction,
     DeleteCollectionsBulkAction,
@@ -23,7 +23,6 @@ import {
     RemoveCollectionsFromChannelBulkAction,
 } from './components/collection-bulk-actions.js';
 import { CollectionContentsSheet } from './components/collection-contents-sheet.js';
-import { useMoveSingleCollection } from './components/move-single-collection.js';
 
 export const Route = createFileRoute('/_authenticated/_collections/collections')({
     component: CollectionListPage,
@@ -34,7 +33,6 @@ type Collection = ResultOf<typeof collectionListDocument>['collections']['items'
 
 function CollectionListPage() {
     const [expanded, setExpanded] = useState<ExpandedState>({});
-    const { handleMoveClick, MoveDialog } = useMoveSingleCollection();
     const childrenQueries = useQueries({
         queries: Object.entries(expanded).map(([collectionId, isExpanded]) => {
             return {
@@ -96,7 +94,6 @@ function CollectionListPage() {
                         },
                     };
                 }}
-                deleteMutation={deleteCollectionDocument}
                 customizeColumns={{
                     name: {
                         header: 'Collection Name',
@@ -210,16 +207,6 @@ function CollectionListPage() {
                     };
                 }}
                 route={Route}
-                rowActions={[
-                    {
-                        label: (
-                            <div className="flex items-center gap-2">
-                                <FolderTreeIcon className="w-4 h-4" /> <Trans>Move</Trans>
-                            </div>
-                        ),
-                        onClick: row => handleMoveClick(row.original),
-                    },
-                ]}
                 bulkActions={[
                     {
                         component: AssignCollectionsToChannelBulkAction,
@@ -254,7 +241,6 @@ function CollectionListPage() {
                     </PermissionGuard>
                 </PageActionBarRight>
             </ListPage>
-            <MoveDialog />
         </>
     );
 }

+ 0 - 33
packages/dashboard/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx

@@ -1,33 +0,0 @@
-import { ResultOf } from 'gql.tada';
-import { useState } from 'react';
-
-import { collectionListDocument } from '../collections.graphql.js';
-import { MoveCollectionsDialog } from './move-collections-dialog.js';
-
-type Collection = ResultOf<typeof collectionListDocument>['collections']['items'][number];
-
-export function useMoveSingleCollection() {
-    const [moveDialogOpen, setMoveDialogOpen] = useState(false);
-    const [collectionsToMove, setCollectionsToMove] = useState<Collection[]>([]);
-
-    const handleMoveClick = (collection: Collection) => {
-        setCollectionsToMove([collection]);
-        setMoveDialogOpen(true);
-    };
-
-    const MoveDialog = () => (
-        <MoveCollectionsDialog
-            open={moveDialogOpen}
-            onOpenChange={setMoveDialogOpen}
-            collectionsToMove={collectionsToMove}
-            onSuccess={() => {
-                // The dialog will handle invalidating queries internally
-            }}
-        />
-    );
-
-    return {
-        handleMoveClick,
-        MoveDialog,
-    };
-}

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

@@ -7,7 +7,7 @@ import { Trans } from '@/vdb/lib/trans.js';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { DeleteCountriesBulkAction } from './components/country-bulk-actions.js';
-import { countriesListQuery, deleteCountryDocument } from './countries.graphql.js';
+import { countriesListQuery } from './countries.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_countries/countries')({
     component: CountryListPage,
@@ -19,7 +19,6 @@ function CountryListPage() {
         <ListPage
             pageId="country-list"
             listQuery={countriesListQuery}
-            deleteMutation={deleteCountryDocument}
             route={Route}
             title="Countries"
             defaultVisibility={{

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

@@ -8,7 +8,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { DeleteCustomerGroupsBulkAction } from './components/customer-group-bulk-actions.js';
 import { CustomerGroupMembersSheet } from './components/customer-group-members-sheet.js';
-import { customerGroupListDocument, deleteCustomerGroupDocument } from './customer-groups.graphql.js';
+import { customerGroupListDocument } from './customer-groups.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_customer-groups/customer-groups')({
     component: CustomerGroupListPage,
@@ -21,7 +21,6 @@ function CustomerGroupListPage() {
             pageId="customer-group-list"
             title="Customer Groups"
             listQuery={customerGroupListDocument}
-            deleteMutation={deleteCustomerGroupDocument}
             route={Route}
             customizeColumns={{
                 name: {

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

@@ -8,7 +8,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { DeleteCustomersBulkAction } from './components/customer-bulk-actions.js';
 import { CustomerStatusBadge } from './components/customer-status-badge.js';
-import { customerListDocument, deleteCustomerDocument } from './customers.graphql.js';
+import { customerListDocument } from './customers.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_customers/customers')({
     component: CustomerListPage,
@@ -21,7 +21,6 @@ function CustomerListPage() {
             title="Customers"
             pageId="customer-list"
             listQuery={customerListDocument}
-            deleteMutation={deleteCustomerDocument}
             onSearchTermChange={searchTerm => {
                 return {
                     lastName: {

+ 0 - 1
packages/dashboard/src/app/routes/_authenticated/_facets/facets.tsx

@@ -63,7 +63,6 @@ function FacetListPage() {
             pageId="facet-list"
             title="Facets"
             listQuery={facetListDocument}
-            deleteMutation={deleteFacetDocument}
             defaultVisibility={{
                 name: true,
                 isPrivate: true,

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

@@ -12,7 +12,7 @@ import {
     DeletePaymentMethodsBulkAction,
     RemovePaymentMethodsFromChannelBulkAction,
 } from './components/payment-method-bulk-actions.js';
-import { deletePaymentMethodDocument, paymentMethodListQuery } from './payment-methods.graphql.js';
+import { paymentMethodListQuery } from './payment-methods.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_payment-methods/payment-methods')({
     component: PaymentMethodListPage,
@@ -24,7 +24,6 @@ function PaymentMethodListPage() {
         <ListPage
             pageId="payment-method-list"
             listQuery={paymentMethodListQuery}
-            deleteMutation={deletePaymentMethodDocument}
             route={Route}
             title="Payment Methods"
             defaultVisibility={{

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

@@ -11,7 +11,7 @@ import {
     DeleteProductVariantsBulkAction,
     RemoveProductVariantsFromChannelBulkAction,
 } from './components/product-variant-bulk-actions.js';
-import { deleteProductVariantDocument, productVariantListDocument } from './product-variants.graphql.js';
+import { productVariantListDocument } from './product-variants.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_product-variants/product-variants')({
     component: ProductListPage,
@@ -25,7 +25,6 @@ function ProductListPage() {
             pageId="product-variant-list"
             title={<Trans>Product Variants</Trans>}
             listQuery={productVariantListDocument}
-            deleteMutation={deleteProductVariantDocument}
             bulkActions={[
                 {
                     component: AssignProductVariantsToChannelBulkAction,

+ 1 - 2
packages/dashboard/src/app/routes/_authenticated/_products/products.tsx

@@ -13,7 +13,7 @@ import {
     DuplicateProductsBulkAction,
     RemoveProductsFromChannelBulkAction,
 } from './components/product-bulk-actions.js';
-import { deleteProductDocument, productListDocument } from './products.graphql.js';
+import { productListDocument } from './products.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_products/products')({
     component: ProductListPage,
@@ -25,7 +25,6 @@ function ProductListPage() {
         <ListPage
             pageId="product-list"
             listQuery={productListDocument}
-            deleteMutation={deleteProductDocument}
             title="Products"
             customizeColumns={{
                 name: {

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

@@ -13,7 +13,7 @@ import {
     DuplicatePromotionsBulkAction,
     RemovePromotionsFromChannelBulkAction,
 } from './components/promotion-bulk-actions.js';
-import { deletePromotionDocument, promotionListDocument } from './promotions.graphql.js';
+import { promotionListDocument } from './promotions.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_promotions/promotions')({
     component: PromotionListPage,
@@ -25,7 +25,6 @@ function PromotionListPage() {
         <ListPage
             pageId="promotion-list"
             listQuery={promotionListDocument}
-            deleteMutation={deletePromotionDocument}
             route={Route}
             title="Promotions"
             defaultVisibility={{

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

@@ -12,7 +12,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
 import { LayersIcon, PlusIcon } from 'lucide-react';
 import { ExpandablePermissions } from './components/expandable-permissions.js';
 import { DeleteRolesBulkAction } from './components/role-bulk-actions.js';
-import { deleteRoleDocument, roleListQuery } from './roles.graphql.js';
+import { roleListQuery } from './roles.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_roles/roles')({
     component: RoleListPage,
@@ -27,7 +27,6 @@ function RoleListPage() {
             pageId="role-list"
             title="Roles"
             listQuery={roleListQuery}
-            deleteMutation={deleteRoleDocument}
             route={Route}
             defaultVisibility={{
                 description: true,

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

@@ -7,7 +7,7 @@ import { Trans } from '@/vdb/lib/trans.js';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { DeleteSellersBulkAction } from './components/seller-bulk-actions.js';
-import { deleteSellerDocument, sellerListQuery } from './sellers.graphql.js';
+import { sellerListQuery } from './sellers.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_sellers/sellers')({
     component: SellerListPage,
@@ -19,7 +19,6 @@ function SellerListPage() {
         <ListPage
             pageId="seller-list"
             listQuery={sellerListQuery}
-            deleteMutation={deleteSellerDocument}
             route={Route}
             title="Sellers"
             defaultVisibility={{

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

@@ -12,7 +12,7 @@ import {
     RemoveShippingMethodsFromChannelBulkAction,
 } from './components/shipping-method-bulk-actions.js';
 import { TestShippingMethodDialog } from './components/test-shipping-method-dialog.js';
-import { deleteShippingMethodDocument, shippingMethodListQuery } from './shipping-methods.graphql.js';
+import { shippingMethodListQuery } from './shipping-methods.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_shipping-methods/shipping-methods')({
     component: ShippingMethodListPage,
@@ -24,7 +24,6 @@ function ShippingMethodListPage() {
         <ListPage
             pageId="shipping-method-list"
             listQuery={shippingMethodListQuery}
-            deleteMutation={deleteShippingMethodDocument}
             route={Route}
             title="Shipping Methods"
             defaultVisibility={{

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

@@ -11,7 +11,7 @@ import {
     DeleteStockLocationsBulkAction,
     RemoveStockLocationsFromChannelBulkAction,
 } from './components/stock-location-bulk-actions.js';
-import { deleteStockLocationDocument, stockLocationListQuery } from './stock-locations.graphql.js';
+import { stockLocationListQuery } from './stock-locations.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations')({
     component: StockLocationListPage,
@@ -24,7 +24,6 @@ function StockLocationListPage() {
             pageId="stock-location-list"
             title="Stock Locations"
             listQuery={stockLocationListQuery}
-            deleteMutation={deleteStockLocationDocument}
             route={Route}
             customizeColumns={{
                 name: {

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

@@ -8,7 +8,7 @@ import { Trans } from '@/vdb/lib/trans.js';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
 import { DeleteTaxCategoriesBulkAction } from './components/tax-category-bulk-actions.js';
-import { deleteTaxCategoryDocument, taxCategoryListQuery } from './tax-categories.graphql.js';
+import { taxCategoryListQuery } from './tax-categories.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories')({
     component: TaxCategoryListPage,
@@ -20,7 +20,6 @@ function TaxCategoryListPage() {
         <ListPage
             pageId="tax-category-list"
             listQuery={taxCategoryListQuery}
-            deleteMutation={deleteTaxCategoryDocument}
             route={Route}
             title="Tax Categories"
             defaultVisibility={{

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

@@ -11,7 +11,7 @@ import { PlusIcon } from 'lucide-react';
 import { taxCategoryListQuery } from '../_tax-categories/tax-categories.graphql.js';
 import { zoneListQuery } from '../_zones/zones.graphql.js';
 import { DeleteTaxRatesBulkAction } from './components/tax-rate-bulk-actions.js';
-import { deleteTaxRateDocument, taxRateListQuery } from './tax-rates.graphql.js';
+import { taxRateListQuery } from './tax-rates.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates')({
     component: TaxRateListPage,
@@ -23,7 +23,6 @@ function TaxRateListPage() {
         <ListPage
             pageId="tax-rate-list"
             listQuery={taxRateListQuery}
-            deleteMutation={deleteTaxRateDocument}
             route={Route}
             title="Tax Rates"
             defaultVisibility={{

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

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

+ 5 - 14
packages/dashboard/src/lib/components/data-table/data-table-bulk-actions.tsx

@@ -1,5 +1,4 @@
-'use client';
-
+import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import {
     DropdownMenu,
@@ -7,11 +6,8 @@ import {
     DropdownMenuItem,
     DropdownMenuTrigger,
 } from '@/vdb/components/ui/dropdown-menu.js';
-import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
 import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
 import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
-import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
-import { usePage } from '@/vdb/hooks/use-page.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { Table } from '@tanstack/react-table';
 import { ChevronDown } from 'lucide-react';
@@ -26,9 +22,7 @@ export function DataTableBulkActions<TData>({
     table,
     bulkActions,
 }: Readonly<DataTableBulkActionsProps<TData>>) {
-    const { pageId } = usePage();
-    const pageBlock = usePageBlock();
-    const blockId = pageBlock?.blockId;
+    const allBulkActions = useAllBulkActions(bulkActions);
 
     // Cache to store selected items across page changes
     const selectedItemsCache = useRef<Map<string, TData>>(new Map());
@@ -63,18 +57,15 @@ export function DataTableBulkActions<TData>({
     if (!shouldShow) {
         return null;
     }
-    const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
-    const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
-    allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
 
     return (
         <div
             className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 fixed transform -translate-x-1/2 bg-white shadow-2xl rounded-md border z-50"
-            style={{ 
-                height: 'auto', 
+            style={{
+                height: 'auto',
                 maxHeight: '60px',
                 bottom: position.bottom,
-                left: position.left
+                left: position.left,
             }}
         >
             <span className="text-sm text-muted-foreground">

+ 19 - 0
packages/dashboard/src/lib/components/data-table/use-all-bulk-actions.ts

@@ -0,0 +1,19 @@
+import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
+import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
+import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
+import { usePage } from '@/vdb/hooks/use-page.js';
+
+/**
+ * @description
+ * Augments the provided Bulk Actions with any user-defined actions for the current
+ * page & block, and returns all of the bulk actions sorted by the `order` property.
+ */
+export function useAllBulkActions(bulkActions: BulkAction[]): BulkAction[] {
+    const { pageId } = usePage();
+    const pageBlock = usePageBlock();
+    const blockId = pageBlock?.blockId;
+    const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
+    const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
+    allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
+    return allBulkActions;
+}

+ 12 - 3
packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx

@@ -1,9 +1,11 @@
+import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
 import { DisplayComponent } from '@/vdb/framework/component-registry/display-component.js';
 import {
     FieldInfo,
     getOperationVariablesFields,
     getTypeFieldInfo,
 } from '@/vdb/framework/document-introspection/get-document-structure.js';
+import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
 import { api } from '@/vdb/graphql/api.js';
 import { Trans, useLingui } from '@/vdb/lib/trans.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
@@ -51,6 +53,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
     fields,
     customizeColumns,
     rowActions,
+    bulkActions,
     deleteMutation,
     additionalColumns,
     defaultColumnOrder,
@@ -62,6 +65,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
     fields: FieldInfo[];
     customizeColumns?: CustomizeColumnConfig<T>;
     rowActions?: RowAction<PaginatedListItemFields<T>>[];
+    bulkActions?: BulkAction[];
     deleteMutation?: TypedDocumentNode<any, any>;
     additionalColumns?: AdditionalColumns<T>;
     defaultColumnOrder?: Array<string | number | symbol>;
@@ -71,6 +75,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
     enableSorting?: boolean;
 }>) {
     const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
+    const allBulkActions = useAllBulkActions(bulkActions ?? []);
 
     const { columns, customFieldColumnNames } = useMemo(() => {
         const columnConfigs: Array<{ fieldInfo: FieldInfo; isCustomField: boolean }> = [];
@@ -169,8 +174,8 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
             finalColumns = [...orderedColumns, ...remainingColumns];
         }
 
-        if (includeActionsColumn && (rowActions || deleteMutation)) {
-            const rowActionColumn = getRowActions(rowActions, deleteMutation);
+        if (includeActionsColumn && (rowActions || deleteMutation || bulkActions)) {
+            const rowActionColumn = getRowActions(rowActions, deleteMutation, allBulkActions);
             if (rowActionColumn) {
                 finalColumns.push(rowActionColumn);
             }
@@ -212,13 +217,14 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
 function getRowActions(
     rowActions?: RowAction<any>[],
     deleteMutation?: TypedDocumentNode<any, any>,
+    bulkActions?: BulkAction[],
 ): AccessorKeyColumnDef<any> | undefined {
     return {
         id: 'actions',
         accessorKey: 'actions',
         header: () => <Trans>Actions</Trans>,
         enableColumnFilter: false,
-        cell: ({ row }) => {
+        cell: ({ row, table }) => {
             return (
                 <DropdownMenu>
                     <DropdownMenuTrigger asChild>
@@ -235,6 +241,9 @@ function getRowActions(
                                 {action.label}
                             </DropdownMenuItem>
                         ))}
+                        {bulkActions?.map((action, index) => (
+                            <action.component key={`bulk-action-${index}`} selection={[row]} table={table} />
+                        ))}
                         {deleteMutation && (
                             <DeleteMutationRowAction deleteMutation={deleteMutation} row={row} />
                         )}

+ 1 - 0
packages/dashboard/src/lib/components/shared/paginated-list-data-table.tsx

@@ -437,6 +437,7 @@ export function PaginatedListDataTable<
         fields,
         customizeColumns,
         rowActions,
+        bulkActions,
         deleteMutation,
         additionalColumns,
         defaultColumnOrder,

+ 7 - 0
packages/dashboard/src/lib/framework/page/list-page.tsx

@@ -38,6 +38,13 @@ export interface ListPageProps<
     route: AnyRoute | (() => AnyRoute);
     title: string | React.ReactElement;
     listQuery: T;
+    /**
+     * @description
+     * Providing the `deleteMutation` will automatically add a "delete" menu item to the
+     * actions column dropdown. Note that if this table already has a "delete" bulk action,
+     * you don't need to additionally provide a delete mutation, because the bulk action
+     * will be added to the action column dropdown already.
+     */
     deleteMutation?: TypedDocumentNode<any, { id: string }>;
     transformVariables?: (variables: V) => V;
     onSearchTermChange?: (searchTerm: string) => NonNullable<V['options']>['filter'];