@vendure/dashboard 3.3.6-master-202507010922 → 3.3.6-master-202507020234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/package.json +131 -131
  2. package/src/app/common/delete-bulk-action.tsx +147 -0
  3. package/src/app/common/duplicate-bulk-action.tsx +1 -1
  4. package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +9 -0
  5. package/src/app/routes/_authenticated/_administrators/administrators.tsx +7 -0
  6. package/src/app/routes/_authenticated/_administrators/components/administrator-bulk-actions.tsx +15 -0
  7. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +11 -0
  8. package/src/app/routes/_authenticated/_assets/assets.tsx +10 -2
  9. package/src/app/routes/_authenticated/_assets/components/asset-bulk-actions.tsx +45 -0
  10. package/src/app/routes/_authenticated/_channels/channels.graphql.ts +9 -0
  11. package/src/app/routes/_authenticated/_channels/channels.tsx +7 -0
  12. package/src/app/routes/_authenticated/_channels/components/channel-bulk-actions.tsx +15 -0
  13. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +39 -110
  14. package/src/app/routes/_authenticated/_countries/components/country-bulk-actions.tsx +15 -0
  15. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +9 -0
  16. package/src/app/routes/_authenticated/_countries/countries.tsx +7 -0
  17. package/src/app/routes/_authenticated/_customer-groups/components/customer-group-bulk-actions.tsx +15 -0
  18. package/src/app/routes/_authenticated/_customer-groups/customer-groups.graphql.ts +9 -0
  19. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +7 -0
  20. package/src/app/routes/_authenticated/_customers/components/customer-bulk-actions.tsx +15 -0
  21. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +9 -1
  22. package/src/app/routes/_authenticated/_customers/customers.tsx +7 -0
  23. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +104 -0
  24. package/src/app/routes/_authenticated/_facets/facets.graphql.ts +30 -0
  25. package/src/app/routes/_authenticated/_facets/facets.tsx +24 -0
  26. package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +58 -0
  27. package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +27 -0
  28. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +30 -8
  29. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +4 -1
  30. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +36 -110
  31. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +36 -105
  32. package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +82 -0
  33. package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +25 -0
  34. package/src/app/routes/_authenticated/_promotions/promotions.tsx +24 -0
  35. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
  36. package/src/app/routes/_authenticated/_roles/components/role-bulk-actions.tsx +15 -0
  37. package/src/app/routes/_authenticated/_roles/roles.graphql.ts +9 -0
  38. package/src/app/routes/_authenticated/_roles/roles.tsx +7 -0
  39. package/src/app/routes/_authenticated/_sellers/components/seller-bulk-actions.tsx +15 -0
  40. package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +9 -0
  41. package/src/app/routes/_authenticated/_sellers/sellers.tsx +7 -0
  42. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -1
  43. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +61 -0
  44. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +27 -0
  45. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +19 -0
  46. package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +58 -0
  47. package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +25 -0
  48. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +19 -0
  49. package/src/app/routes/_authenticated/_tax-categories/components/tax-category-bulk-actions.tsx +15 -0
  50. package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +9 -0
  51. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +7 -0
  52. package/src/app/routes/_authenticated/_tax-rates/components/tax-rate-bulk-actions.tsx +15 -0
  53. package/src/app/routes/_authenticated/_tax-rates/tax-rates.graphql.ts +9 -0
  54. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +7 -0
  55. package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +15 -0
  56. package/src/app/routes/_authenticated/_zones/zones.graphql.ts +9 -0
  57. package/src/app/routes/_authenticated/_zones/zones.tsx +7 -0
  58. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +90 -0
  59. package/src/lib/components/shared/asset/asset-gallery.tsx +12 -7
  60. package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +70 -0
  61. package/src/{app/routes/_authenticated/_products/components → lib/components/shared}/assign-to-channel-dialog.tsx +48 -30
  62. package/src/lib/components/shared/detail-page-button.tsx +1 -1
  63. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +89 -0
  64. package/src/lib/framework/extension-api/use-dashboard-extensions.ts +2 -1
  65. package/src/lib/framework/form-engine/use-generated-form.tsx +5 -8
  66. package/src/lib/framework/page/use-detail-page.ts +4 -4
  67. package/src/lib/framework/page/use-extended-router.tsx +4 -5
  68. package/src/lib/hooks/use-channel.ts +2 -1
  69. package/src/lib/hooks/use-extended-detail-query.ts +2 -1
  70. package/src/lib/hooks/use-extended-list-query.ts +3 -2
  71. package/src/lib/hooks/use-grouped-permissions.ts +4 -2
  72. package/src/lib/hooks/use-page-block.tsx +1 -1
  73. package/src/lib/hooks/use-page.tsx +2 -2
  74. package/src/lib/hooks/use-permissions.ts +3 -2
  75. package/src/lib/hooks/use-server-config.ts +2 -1
  76. package/src/lib/hooks/use-theme.ts +2 -1
  77. package/src/lib/providers/auth.tsx +34 -11
  78. package/src/app/routes/_authenticated/_collections/components/assign-collections-to-channel-dialog.tsx +0 -110
@@ -0,0 +1,61 @@
1
+ import { AssignToChannelBulkAction } from '@/components/shared/assign-to-channel-bulk-action.js';
2
+ import { RemoveFromChannelBulkAction } from '@/components/shared/remove-from-channel-bulk-action.js';
3
+ import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
4
+ import { api } from '@/graphql/api.js';
5
+ import { useChannel } from '@/index.js';
6
+ import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
7
+
8
+ import {
9
+ assignShippingMethodsToChannelDocument,
10
+ deleteShippingMethodsDocument,
11
+ removeShippingMethodsFromChannelDocument,
12
+ } from '../shipping-methods.graphql.js';
13
+
14
+ export const DeleteShippingMethodsBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
15
+ return (
16
+ <DeleteBulkAction
17
+ mutationDocument={deleteShippingMethodsDocument}
18
+ entityName="shipping methods"
19
+ requiredPermissions={['DeleteShippingMethod']}
20
+ selection={selection}
21
+ table={table}
22
+ />
23
+ );
24
+ };
25
+
26
+ export const AssignShippingMethodsToChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
27
+ return (
28
+ <AssignToChannelBulkAction
29
+ selection={selection}
30
+ table={table}
31
+ entityType="shipping methods"
32
+ mutationFn={api.mutate(assignShippingMethodsToChannelDocument)}
33
+ requiredPermissions={['UpdateShippingMethod']}
34
+ buildInput={(channelId: string) => ({
35
+ shippingMethodIds: selection.map(s => s.id),
36
+ channelId,
37
+ })}
38
+ />
39
+ );
40
+ };
41
+
42
+ export const RemoveShippingMethodsFromChannelBulkAction: BulkActionComponent<any> = ({
43
+ selection,
44
+ table,
45
+ }) => {
46
+ const { selectedChannel } = useChannel();
47
+
48
+ return (
49
+ <RemoveFromChannelBulkAction
50
+ selection={selection}
51
+ table={table}
52
+ entityType="shipping methods"
53
+ mutationFn={api.mutate(removeShippingMethodsFromChannelDocument)}
54
+ requiredPermissions={['UpdateShippingMethod']}
55
+ buildInput={() => ({
56
+ shippingMethodIds: selection.map(s => s.id),
57
+ channelId: selectedChannel?.id,
58
+ })}
59
+ />
60
+ );
61
+ };
@@ -81,3 +81,30 @@ export const deleteShippingMethodDocument = graphql(`
81
81
  }
82
82
  }
83
83
  `);
84
+
85
+ export const deleteShippingMethodsDocument = graphql(`
86
+ mutation DeleteShippingMethods($ids: [ID!]!) {
87
+ deleteShippingMethods(ids: $ids) {
88
+ result
89
+ message
90
+ }
91
+ }
92
+ `);
93
+
94
+ export const assignShippingMethodsToChannelDocument = graphql(`
95
+ mutation AssignShippingMethodsToChannel($input: AssignShippingMethodsToChannelInput!) {
96
+ assignShippingMethodsToChannel(input: $input) {
97
+ id
98
+ name
99
+ }
100
+ }
101
+ `);
102
+
103
+ export const removeShippingMethodsFromChannelDocument = graphql(`
104
+ mutation RemoveShippingMethodsFromChannel($input: RemoveShippingMethodsFromChannelInput!) {
105
+ removeShippingMethodsFromChannel(input: $input) {
106
+ id
107
+ name
108
+ }
109
+ }
110
+ `);
@@ -6,6 +6,11 @@ import { ListPage } from '@/framework/page/list-page.js';
6
6
  import { Trans } from '@/lib/trans.js';
7
7
  import { createFileRoute, Link } from '@tanstack/react-router';
8
8
  import { PlusIcon } from 'lucide-react';
9
+ import {
10
+ AssignShippingMethodsToChannelBulkAction,
11
+ DeleteShippingMethodsBulkAction,
12
+ RemoveShippingMethodsFromChannelBulkAction,
13
+ } from './components/shipping-method-bulk-actions.js';
9
14
  import { TestShippingMethodDialog } from './components/test-shipping-method-dialog.js';
10
15
  import { deleteShippingMethodDocument, shippingMethodListQuery } from './shipping-methods.graphql.js';
11
16
 
@@ -38,6 +43,20 @@ function ShippingMethodListPage() {
38
43
  name: { contains: searchTerm },
39
44
  };
40
45
  }}
46
+ bulkActions={[
47
+ {
48
+ component: AssignShippingMethodsToChannelBulkAction,
49
+ order: 100,
50
+ },
51
+ {
52
+ component: RemoveShippingMethodsFromChannelBulkAction,
53
+ order: 200,
54
+ },
55
+ {
56
+ component: DeleteShippingMethodsBulkAction,
57
+ order: 500,
58
+ },
59
+ ]}
41
60
  >
42
61
  <PageActionBarRight>
43
62
  <PermissionGuard requires={['CreateShippingMethod']}>
@@ -0,0 +1,58 @@
1
+ import { AssignToChannelBulkAction } from '@/components/shared/assign-to-channel-bulk-action.js';
2
+ import { RemoveFromChannelBulkAction } from '@/components/shared/remove-from-channel-bulk-action.js';
3
+ import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
4
+ import { api } from '@/graphql/api.js';
5
+ import { useChannel } from '@/index.js';
6
+ import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
7
+
8
+ import {
9
+ assignStockLocationsToChannelDocument,
10
+ deleteStockLocationsDocument,
11
+ removeStockLocationsFromChannelDocument,
12
+ } from '../stock-locations.graphql.js';
13
+
14
+ export const DeleteStockLocationsBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
15
+ return (
16
+ <DeleteBulkAction
17
+ mutationDocument={deleteStockLocationsDocument}
18
+ entityName="stock locations"
19
+ requiredPermissions={['DeleteStockLocation']}
20
+ selection={selection}
21
+ table={table}
22
+ />
23
+ );
24
+ };
25
+
26
+ export const AssignStockLocationsToChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
27
+ return (
28
+ <AssignToChannelBulkAction
29
+ selection={selection}
30
+ table={table}
31
+ entityType="stock locations"
32
+ mutationFn={api.mutate(assignStockLocationsToChannelDocument)}
33
+ requiredPermissions={['UpdateStockLocation']}
34
+ buildInput={(channelId: string) => ({
35
+ stockLocationIds: selection.map(s => s.id),
36
+ channelId,
37
+ })}
38
+ />
39
+ );
40
+ };
41
+
42
+ export const RemoveStockLocationsFromChannelBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
43
+ const { selectedChannel } = useChannel();
44
+
45
+ return (
46
+ <RemoveFromChannelBulkAction
47
+ selection={selection}
48
+ table={table}
49
+ entityType="stock locations"
50
+ mutationFn={api.mutate(removeStockLocationsFromChannelDocument)}
51
+ requiredPermissions={['UpdateStockLocation']}
52
+ buildInput={() => ({
53
+ stockLocationIds: selection.map(s => s.id),
54
+ channelId: selectedChannel?.id,
55
+ })}
56
+ />
57
+ );
58
+ };
@@ -60,3 +60,28 @@ export const deleteStockLocationDocument = graphql(`
60
60
  }
61
61
  }
62
62
  `);
63
+
64
+ export const deleteStockLocationsDocument = graphql(`
65
+ mutation DeleteStockLocations($input: [DeleteStockLocationInput!]!) {
66
+ deleteStockLocations(input: $input) {
67
+ result
68
+ message
69
+ }
70
+ }
71
+ `);
72
+
73
+ export const assignStockLocationsToChannelDocument = graphql(`
74
+ mutation AssignStockLocationsToChannel($input: AssignStockLocationsToChannelInput!) {
75
+ assignStockLocationsToChannel(input: $input) {
76
+ id
77
+ }
78
+ }
79
+ `);
80
+
81
+ export const removeStockLocationsFromChannelDocument = graphql(`
82
+ mutation RemoveStockLocationsFromChannel($input: RemoveStockLocationsFromChannelInput!) {
83
+ removeStockLocationsFromChannel(input: $input) {
84
+ id
85
+ }
86
+ }
87
+ `);
@@ -6,6 +6,11 @@ import { ListPage } from '@/framework/page/list-page.js';
6
6
  import { Trans } from '@/lib/trans.js';
7
7
  import { createFileRoute, Link } from '@tanstack/react-router';
8
8
  import { PlusIcon } from 'lucide-react';
9
+ import {
10
+ AssignStockLocationsToChannelBulkAction,
11
+ DeleteStockLocationsBulkAction,
12
+ RemoveStockLocationsFromChannelBulkAction,
13
+ } from './components/stock-location-bulk-actions.js';
9
14
  import { deleteStockLocationDocument, stockLocationListQuery } from './stock-locations.graphql.js';
10
15
 
11
16
  export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations')({
@@ -32,6 +37,20 @@ function StockLocationListPage() {
32
37
  name: { contains: searchTerm },
33
38
  };
34
39
  }}
40
+ bulkActions={[
41
+ {
42
+ component: AssignStockLocationsToChannelBulkAction,
43
+ order: 100,
44
+ },
45
+ {
46
+ component: RemoveStockLocationsFromChannelBulkAction,
47
+ order: 200,
48
+ },
49
+ {
50
+ component: DeleteStockLocationsBulkAction,
51
+ order: 500,
52
+ },
53
+ ]}
35
54
  >
36
55
  <PageActionBarRight>
37
56
  <PermissionGuard requires={['CreateStockLocation']}>
@@ -0,0 +1,15 @@
1
+ import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
2
+ import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
3
+ import { deleteTaxCategoriesDocument } from '../tax-categories.graphql.js';
4
+
5
+ export const DeleteTaxCategoriesBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
6
+ return (
7
+ <DeleteBulkAction
8
+ mutationDocument={deleteTaxCategoriesDocument}
9
+ entityName="tax categories"
10
+ requiredPermissions={['DeleteTaxCategory']}
11
+ selection={selection}
12
+ table={table}
13
+ />
14
+ );
15
+ };
@@ -61,3 +61,12 @@ export const deleteTaxCategoryDocument = graphql(`
61
61
  }
62
62
  }
63
63
  `);
64
+
65
+ export const deleteTaxCategoriesDocument = graphql(`
66
+ mutation DeleteTaxCategories($ids: [ID!]!) {
67
+ deleteTaxCategories(ids: $ids) {
68
+ result
69
+ message
70
+ }
71
+ }
72
+ `);
@@ -7,6 +7,7 @@ import { ListPage } from '@/framework/page/list-page.js';
7
7
  import { Trans } from '@/lib/trans.js';
8
8
  import { createFileRoute, Link } from '@tanstack/react-router';
9
9
  import { PlusIcon } from 'lucide-react';
10
+ import { DeleteTaxCategoriesBulkAction } from './components/tax-category-bulk-actions.js';
10
11
  import { deleteTaxCategoryDocument, taxCategoryListQuery } from './tax-categories.graphql.js';
11
12
 
12
13
  export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories')({
@@ -49,6 +50,12 @@ function TaxCategoryListPage() {
49
50
  ),
50
51
  },
51
52
  }}
53
+ bulkActions={[
54
+ {
55
+ component: DeleteTaxCategoriesBulkAction,
56
+ order: 500,
57
+ },
58
+ ]}
52
59
  >
53
60
  <PageActionBarRight>
54
61
  <PermissionGuard requires={['CreateTaxCategory']}>
@@ -0,0 +1,15 @@
1
+ import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
2
+ import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
3
+ import { deleteTaxRatesDocument } from '../tax-rates.graphql.js';
4
+
5
+ export const DeleteTaxRatesBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
6
+ return (
7
+ <DeleteBulkAction
8
+ mutationDocument={deleteTaxRatesDocument}
9
+ entityName="tax rates"
10
+ requiredPermissions={['DeleteTaxRate']}
11
+ selection={selection}
12
+ table={table}
13
+ />
14
+ );
15
+ };
@@ -73,3 +73,12 @@ export const deleteTaxRateDocument = graphql(`
73
73
  }
74
74
  }
75
75
  `);
76
+
77
+ export const deleteTaxRatesDocument = graphql(`
78
+ mutation DeleteTaxRates($ids: [ID!]!) {
79
+ deleteTaxRates(ids: $ids) {
80
+ result
81
+ message
82
+ }
83
+ }
84
+ `);
@@ -10,6 +10,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
10
10
  import { PlusIcon } from 'lucide-react';
11
11
  import { taxCategoryListQuery } from '../_tax-categories/tax-categories.graphql.js';
12
12
  import { zoneListQuery } from '../_zones/zones.graphql.js';
13
+ import { DeleteTaxRatesBulkAction } from './components/tax-rate-bulk-actions.js';
13
14
  import { deleteTaxRateDocument, taxRateListQuery } from './tax-rates.graphql.js';
14
15
 
15
16
  export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates')({
@@ -92,6 +93,12 @@ function TaxRateListPage() {
92
93
  cell: ({ row }) => `${row.original.value}%`,
93
94
  },
94
95
  }}
96
+ bulkActions={[
97
+ {
98
+ component: DeleteTaxRatesBulkAction,
99
+ order: 500,
100
+ },
101
+ ]}
95
102
  >
96
103
  <PageActionBarRight>
97
104
  <PermissionGuard requires={['CreateTaxRate']}>
@@ -0,0 +1,15 @@
1
+ import { BulkActionComponent } from '@/framework/data-table/data-table-types.js';
2
+ import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
3
+ import { deleteZonesDocument } from '../zones.graphql.js';
4
+
5
+ export const DeleteZonesBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
6
+ return (
7
+ <DeleteBulkAction
8
+ mutationDocument={deleteZonesDocument}
9
+ entityName="zones"
10
+ requiredPermissions={['DeleteZone']}
11
+ selection={selection}
12
+ table={table}
13
+ />
14
+ );
15
+ };
@@ -94,3 +94,12 @@ export const deleteZoneDocument = graphql(`
94
94
  }
95
95
  }
96
96
  `);
97
+
98
+ export const deleteZonesDocument = graphql(`
99
+ mutation DeleteZones($ids: [ID!]!) {
100
+ deleteZones(ids: $ids) {
101
+ result
102
+ message
103
+ }
104
+ }
105
+ `);
@@ -6,6 +6,7 @@ import { ListPage } from '@/framework/page/list-page.js';
6
6
  import { Trans } from '@/lib/trans.js';
7
7
  import { createFileRoute, Link } from '@tanstack/react-router';
8
8
  import { PlusIcon } from 'lucide-react';
9
+ import { DeleteZonesBulkAction } from './components/zone-bulk-actions.js';
9
10
  import { ZoneCountriesSheet } from './components/zone-countries-sheet.js';
10
11
  import { deleteZoneDocument, zoneListQuery } from './zones.graphql.js';
11
12
 
@@ -41,6 +42,12 @@ function ZoneListPage() {
41
42
  ),
42
43
  },
43
44
  }}
45
+ bulkActions={[
46
+ {
47
+ component: DeleteZonesBulkAction,
48
+ order: 500,
49
+ },
50
+ ]}
44
51
  >
45
52
  <PageActionBarRight>
46
53
  <PermissionGuard requires={['CreateZone']}>
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button.js';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '@/components/ui/dropdown-menu.js';
10
+ import { getBulkActions } from '@/framework/data-table/data-table-extensions.js';
11
+ import { usePageBlock } from '@/hooks/use-page-block.js';
12
+ import { usePage } from '@/hooks/use-page.js';
13
+ import { Trans } from '@/lib/trans.js';
14
+ import { ChevronDown } from 'lucide-react';
15
+ import { Asset } from './asset-gallery.js';
16
+
17
+ export type AssetBulkActionContext = {
18
+ selection: Asset[];
19
+ refetch: () => void;
20
+ };
21
+
22
+ export type AssetBulkActionComponent = React.FunctionComponent<AssetBulkActionContext>;
23
+
24
+ export type AssetBulkAction = {
25
+ order?: number;
26
+ component: AssetBulkActionComponent;
27
+ };
28
+
29
+ interface AssetBulkActionsProps {
30
+ selection: Asset[];
31
+ bulkActions?: AssetBulkAction[];
32
+ refetch: () => void;
33
+ }
34
+
35
+ export function AssetBulkActions({ selection, bulkActions, refetch }: AssetBulkActionsProps) {
36
+ const { pageId } = usePage();
37
+ const { blockId } = usePageBlock();
38
+
39
+ if (selection.length === 0) {
40
+ return null;
41
+ }
42
+
43
+ // Get extended bulk actions from the registry
44
+ const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
45
+
46
+ // Convert DataTable bulk actions to Asset bulk actions
47
+ const convertedBulkActions: AssetBulkAction[] = extendedBulkActions.map(action => ({
48
+ order: action.order,
49
+ component: ({ selection }) => {
50
+ // Create a mock table context for compatibility
51
+ const mockTable = {
52
+ getState: () => ({ rowSelection: {} }),
53
+ getRow: () => null,
54
+ } as any;
55
+
56
+ const ActionComponent = action.component;
57
+ return <ActionComponent selection={selection} table={mockTable} />;
58
+ },
59
+ }));
60
+
61
+ const allBulkActions = [...convertedBulkActions, ...(bulkActions ?? [])];
62
+ allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
63
+
64
+ return (
65
+ <div className="flex items-center gap-2 px-2 py-1 mb-2 bg-muted/50 rounded-md border">
66
+ <span className="text-sm text-muted-foreground">
67
+ <Trans>{selection.length} selected</Trans>
68
+ </span>
69
+ <DropdownMenu>
70
+ <DropdownMenuTrigger asChild>
71
+ <Button variant="outline" size="sm" className="h-8">
72
+ <Trans>With selected...</Trans>
73
+ <ChevronDown className="ml-2 h-4 w-4" />
74
+ </Button>
75
+ </DropdownMenuTrigger>
76
+ <DropdownMenuContent align="start">
77
+ {allBulkActions.length > 0 ? (
78
+ allBulkActions.map((action, index) => (
79
+ <action.component key={`asset-bulk-action-${index}`} selection={selection} refetch={refetch} />
80
+ ))
81
+ ) : (
82
+ <DropdownMenuItem className="text-muted-foreground" disabled>
83
+ <Trans>No actions available</Trans>
84
+ </DropdownMenuItem>
85
+ )}
86
+ </DropdownMenuContent>
87
+ </DropdownMenu>
88
+ </div>
89
+ );
90
+ }
@@ -16,14 +16,15 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
16
16
  import { api } from '@/graphql/api.js';
17
17
  import { assetFragment, AssetFragment } from '@/graphql/fragments.js';
18
18
  import { graphql } from '@/graphql/graphql.js';
19
- import { formatFileSize } from '@/lib/utils.js';
20
19
  import { Trans } from '@/lib/trans.js';
20
+ import { formatFileSize } from '@/lib/utils.js';
21
21
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
22
+ import { useDebounce } from '@uidotdev/usehooks';
22
23
  import { Loader2, Search, Upload, X } from 'lucide-react';
23
24
  import { useCallback, useState } from 'react';
24
25
  import { useDropzone } from 'react-dropzone';
25
- import { useDebounce } from '@uidotdev/usehooks';
26
26
  import { DetailPageButton } from '../detail-page-button.js';
27
+ import { AssetBulkAction, AssetBulkActions } from './asset-bulk-actions.js';
27
28
 
28
29
  const getAssetListDocument = graphql(
29
30
  `
@@ -76,7 +77,7 @@ export interface AssetGalleryProps {
76
77
  /**
77
78
  * @description
78
79
  * Defines whether multiple assets can be selected.
79
- *
80
+ *
80
81
  * If set to 'auto', the asset selection will be toggled when the user clicks on an asset.
81
82
  * If set to 'manual', multiple selection will occur only if the user holds down the control/cmd key.
82
83
  */
@@ -87,6 +88,7 @@ export interface AssetGalleryProps {
87
88
  showHeader?: boolean;
88
89
  className?: string;
89
90
  onFilesDropped?: (files: File[]) => void;
91
+ bulkActions?: AssetBulkAction[];
90
92
  }
91
93
 
92
94
  export function AssetGallery({
@@ -99,6 +101,7 @@ export function AssetGallery({
99
101
  showHeader = true,
100
102
  className = '',
101
103
  onFilesDropped,
104
+ bulkActions,
102
105
  }: AssetGalleryProps) {
103
106
  // State
104
107
  const [page, setPage] = useState(1);
@@ -111,7 +114,7 @@ export function AssetGallery({
111
114
  const queryKey = ['AssetGallery', page, pageSize, debouncedSearch, assetType];
112
115
 
113
116
  // Query for assets
114
- const { data, isLoading } = useQuery({
117
+ const { data, isLoading, refetch } = useQuery({
115
118
  queryKey,
116
119
  queryFn: () => {
117
120
  const filter: Record<string, any> = {};
@@ -173,7 +176,6 @@ export function AssetGallery({
173
176
  return;
174
177
  }
175
178
 
176
-
177
179
  // Manual mode - check for modifier key
178
180
  const isModifierKeyPressed = event.metaKey || event.ctrlKey;
179
181
 
@@ -269,6 +271,9 @@ export function AssetGallery({
269
271
  </div>
270
272
  )}
271
273
 
274
+ {/* Bulk actions bar */}
275
+ <AssetBulkActions selection={selected} bulkActions={bulkActions} refetch={refetch} />
276
+
272
277
  <div
273
278
  {...getRootProps()}
274
279
  className={`
@@ -300,7 +305,7 @@ export function AssetGallery({
300
305
  ${isSelected(asset as Asset) ? 'ring-2 ring-primary' : ''}
301
306
  flex flex-col min-w-[120px]
302
307
  `}
303
- onClick={(e) => handleSelect(asset as Asset, e)}
308
+ onClick={e => handleSelect(asset as Asset, e)}
304
309
  >
305
310
  <div
306
311
  className="relative w-full bg-muted/30"
@@ -324,7 +329,7 @@ export function AssetGallery({
324
329
  <p className="text-xs line-clamp-2 min-h-[2.5rem]" title={asset.name}>
325
330
  {asset.name}
326
331
  </p>
327
- <div className='flex justify-between items-center'>
332
+ <div className="flex justify-between items-center">
328
333
  {asset.fileSize && (
329
334
  <p className="text-xs text-muted-foreground mt-1">
330
335
  {formatFileSize(asset.fileSize)}
@@ -0,0 +1,70 @@
1
+ import { LayersIcon } from 'lucide-react';
2
+ import { useState } from 'react';
3
+
4
+ import { DataTableBulkActionItem } from '@/components/data-table/data-table-bulk-action-item.js';
5
+ import { AssignToChannelDialog } from '@/components/shared/assign-to-channel-dialog.js';
6
+ import { useChannel, usePaginatedList } from '@/index.js';
7
+ import { Trans } from '@/lib/trans.js';
8
+
9
+ interface AssignToChannelBulkActionProps {
10
+ selection: any[];
11
+ table: any;
12
+ entityType: string;
13
+ mutationFn: (variables: any) => Promise<any>;
14
+ requiredPermissions: string[];
15
+ buildInput: (channelId: string, additionalData?: Record<string, any>) => Record<string, any>;
16
+ additionalFields?: React.ReactNode;
17
+ additionalData?: Record<string, any>;
18
+ /**
19
+ * Additional callback to run on success, after the standard refetch and reset
20
+ */
21
+ onSuccess?: () => void;
22
+ }
23
+
24
+ export function AssignToChannelBulkAction({
25
+ selection,
26
+ table,
27
+ entityType,
28
+ mutationFn,
29
+ requiredPermissions,
30
+ buildInput,
31
+ additionalFields,
32
+ additionalData = {},
33
+ onSuccess,
34
+ }: Readonly<AssignToChannelBulkActionProps>) {
35
+ const { refetchPaginatedList } = usePaginatedList();
36
+ const { channels } = useChannel();
37
+ const [dialogOpen, setDialogOpen] = useState(false);
38
+
39
+ if (channels.length < 2) {
40
+ return null;
41
+ }
42
+
43
+ const handleSuccess = () => {
44
+ refetchPaginatedList();
45
+ table.resetRowSelection();
46
+ onSuccess?.();
47
+ };
48
+
49
+ return (
50
+ <>
51
+ <DataTableBulkActionItem
52
+ requiresPermission={requiredPermissions}
53
+ onClick={() => setDialogOpen(true)}
54
+ label={<Trans>Assign to channel</Trans>}
55
+ icon={LayersIcon}
56
+ />
57
+ <AssignToChannelDialog
58
+ open={dialogOpen}
59
+ onOpenChange={setDialogOpen}
60
+ entityIds={selection.map(s => s.id)}
61
+ entityType={entityType}
62
+ mutationFn={mutationFn}
63
+ onSuccess={handleSuccess}
64
+ buildInput={buildInput}
65
+ additionalFields={additionalFields}
66
+ additionalData={additionalData}
67
+ />
68
+ </>
69
+ );
70
+ }