@vendure/dashboard 3.4.3-master-202509190229 → 3.4.3-master-202509230228

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 (60) hide show
  1. package/dist/vite/vite-plugin-config.js +1 -0
  2. package/package.json +4 -4
  3. package/src/app/routes/_authenticated/_administrators/administrators.tsx +1 -2
  4. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +39 -0
  5. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +18 -7
  6. package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +206 -0
  7. package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +226 -0
  8. package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +217 -0
  9. package/src/app/routes/_authenticated/_channels/channels.tsx +1 -2
  10. package/src/app/routes/_authenticated/_collections/collections.tsx +2 -16
  11. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
  12. package/src/app/routes/_authenticated/_countries/countries.tsx +1 -2
  13. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
  14. package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
  15. package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
  16. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +302 -0
  17. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -0
  18. package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +61 -0
  19. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +17 -10
  20. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +31 -0
  21. package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +50 -0
  22. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +17 -290
  23. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +7 -39
  24. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +4 -26
  25. package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +129 -0
  26. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +8 -0
  27. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
  28. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
  29. package/src/app/routes/_authenticated/_products/products.tsx +1 -2
  30. package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
  31. package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +251 -0
  32. package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
  33. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -3
  34. package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
  35. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
  36. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
  37. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
  38. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
  39. package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
  40. package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
  41. package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
  42. package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
  43. package/src/lib/components/layout/nav-main.tsx +50 -25
  44. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  45. package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
  46. package/src/lib/components/shared/detail-page-button.tsx +6 -4
  47. package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
  48. package/src/lib/components/shared/vendure-image.tsx +9 -1
  49. package/src/lib/framework/defaults.ts +24 -0
  50. package/src/lib/framework/extension-api/types/navigation.ts +8 -0
  51. package/src/lib/framework/layout-engine/page-layout.tsx +96 -9
  52. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
  53. package/src/lib/framework/page/list-page.tsx +7 -0
  54. package/src/lib/hooks/use-custom-field-config.ts +19 -2
  55. package/src/lib/index.ts +7 -1
  56. package/src/lib/providers/channel-provider.tsx +22 -6
  57. package/src/lib/providers/server-config.tsx +1 -0
  58. package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
  59. package/src/app/routes/_authenticated/_roles/components/permissions-grid.tsx +0 -120
  60. package/src/lib/components/shared/asset/focal-point-control.tsx +0 -57
@@ -15,11 +15,11 @@ import {
15
15
  PageLayout,
16
16
  PageTitle,
17
17
  } from '@/vdb/framework/layout-engine/page-layout.js';
18
- import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
18
+ import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
19
19
  import { api } from '@/vdb/graphql/api.js';
20
20
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
21
21
  import { useMutation, useQuery } from '@tanstack/react-query';
22
- import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
22
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
23
23
  import { ResultOf } from 'gql.tada';
24
24
  import { User } from 'lucide-react';
25
25
  import { toast } from 'sonner';
@@ -44,33 +44,11 @@ import {
44
44
  unsetBillingAddressForDraftOrderDocument,
45
45
  unsetShippingAddressForDraftOrderDocument,
46
46
  } from './orders.graphql.js';
47
+ import { loadDraftOrder } from './utils/order-detail-loaders.js';
47
48
 
48
49
  export const Route = createFileRoute('/_authenticated/_orders/orders_/draft/$id')({
49
50
  component: DraftOrderPage,
50
- loader: async ({ context, params }) => {
51
- if (!params.id) {
52
- throw new Error('ID param is required');
53
- }
54
-
55
- const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
56
- getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
57
- { id: params.id },
58
- );
59
-
60
- if (!result.order) {
61
- throw new Error(`Order with the ID ${params.id} was not found`);
62
- }
63
-
64
- if (result.order.state !== 'Draft') {
65
- throw redirect({
66
- to: `/orders/${params.id}`,
67
- });
68
- }
69
-
70
- return {
71
- breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, result.order.code],
72
- };
73
- },
51
+ loader: ({ context, params }) => loadDraftOrder(context, params),
74
52
  errorComponent: ({ error }) => <ErrorPage message={error.message} />,
75
53
  });
76
54
 
@@ -0,0 +1,129 @@
1
+ import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
2
+ import { getDetailQueryOptions } from '@/vdb/framework/page/use-detail-page.js';
3
+ import { ResultOf } from '@/vdb/graphql/graphql.js';
4
+ import { Trans } from '@/vdb/lib/trans.js';
5
+ import { redirect } from '@tanstack/react-router';
6
+ import { OrderDetail } from '../components/order-detail-shared.js';
7
+ import { orderDetailDocument } from '../orders.graphql.js';
8
+
9
+ export async function commonRegularOrderLoader(context: any, params: { id: string }): Promise<OrderDetail> {
10
+ if (!params.id) {
11
+ throw new Error('ID param is required');
12
+ }
13
+
14
+ const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
15
+ getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
16
+ );
17
+
18
+ if (!result.order) {
19
+ throw new Error(`Order with the ID ${params.id} was not found`);
20
+ }
21
+
22
+ if (result.order.state === 'Draft') {
23
+ throw redirect({
24
+ to: `/orders/draft/${params.id}`,
25
+ });
26
+ }
27
+ return result.order;
28
+ }
29
+
30
+ export async function loadRegularOrder(context: any, params: { id: string }) {
31
+ const order = await commonRegularOrderLoader(context, params);
32
+
33
+ if (order.state === 'Modifying') {
34
+ throw redirect({
35
+ to: `/orders/${params.id}/modify`,
36
+ });
37
+ }
38
+
39
+ return {
40
+ breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, order.code],
41
+ };
42
+ }
43
+
44
+ export async function loadDraftOrder(context: any, params: { id: string }) {
45
+ const order = await commonRegularOrderLoader(context, params);
46
+
47
+ if (order.state !== 'Draft') {
48
+ throw redirect({
49
+ to: `/orders/${params.id}`,
50
+ });
51
+ }
52
+
53
+ return {
54
+ breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, order.code],
55
+ };
56
+ }
57
+
58
+ export async function loadModifyingOrder(context: any, params: { id: string }) {
59
+ const order = await commonRegularOrderLoader(context, params);
60
+ if (order.state !== 'Modifying') {
61
+ throw redirect({
62
+ to: `/orders/${params.id}`,
63
+ });
64
+ }
65
+
66
+ return {
67
+ breadcrumb: [
68
+ { path: '/orders', label: <Trans>Orders</Trans> },
69
+ order.code,
70
+ { label: <Trans>Modify</Trans> },
71
+ ],
72
+ };
73
+ }
74
+
75
+ export async function loadSellerOrder(
76
+ context: any,
77
+ params: { aggregateOrderId: string; sellerOrderId: string },
78
+ ) {
79
+ if (!params.sellerOrderId || !params.aggregateOrderId) {
80
+ throw new Error('Both seller order ID and aggregate order ID params are required');
81
+ }
82
+
83
+ const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
84
+ getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.sellerOrderId }),
85
+ );
86
+
87
+ if (!result.order) {
88
+ throw new Error(`Seller order with the ID ${params.sellerOrderId} was not found`);
89
+ }
90
+
91
+ // Verify this is actually a seller order by checking if it has an aggregateOrder
92
+ if (!result.order.aggregateOrder) {
93
+ throw new Error(`Order ${params.sellerOrderId} is not a seller order`);
94
+ }
95
+
96
+ // Verify the aggregate order ID matches
97
+ if (result.order.aggregateOrder.id !== params.aggregateOrderId) {
98
+ throw new Error(
99
+ `Seller order ${params.sellerOrderId} does not belong to aggregate order ${params.aggregateOrderId}`,
100
+ );
101
+ }
102
+
103
+ if (result.order.state === 'Draft') {
104
+ throw redirect({
105
+ to: `/orders/draft/${params.sellerOrderId}`,
106
+ });
107
+ }
108
+
109
+ if (result.order.state === 'Modifying') {
110
+ throw redirect({
111
+ to: `/orders/${params.sellerOrderId}/modify`,
112
+ });
113
+ }
114
+
115
+ return {
116
+ breadcrumb: [
117
+ { path: '/orders', label: <Trans>Orders</Trans> },
118
+ {
119
+ path: `/orders/${params.aggregateOrderId}`,
120
+ label: result.order.aggregateOrder.code,
121
+ },
122
+ {
123
+ path: `/orders/${params.aggregateOrderId}`,
124
+ label: 'Seller orders',
125
+ },
126
+ result.order.code,
127
+ ],
128
+ };
129
+ }
@@ -1,3 +1,5 @@
1
+ import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
2
+
1
3
  import { Fulfillment, Order, Payment } from './order-types.js';
2
4
 
3
5
  /**
@@ -76,3 +78,9 @@ export function canAddFulfillment(order: Order): boolean {
76
78
  isFulfillableState
77
79
  );
78
80
  }
81
+
82
+ export function getSeller<T>(order: { channels: Array<{ code: string; seller: T }> }) {
83
+ // Find the seller channel (non-default channel)
84
+ const sellerChannel = order.channels.find(channel => channel.code !== DEFAULT_CHANNEL_CODE);
85
+ return sellerChannel?.seller;
86
+ }
@@ -12,7 +12,7 @@ import {
12
12
  DeletePaymentMethodsBulkAction,
13
13
  RemovePaymentMethodsFromChannelBulkAction,
14
14
  } from './components/payment-method-bulk-actions.js';
15
- import { deletePaymentMethodDocument, paymentMethodListQuery } from './payment-methods.graphql.js';
15
+ import { paymentMethodListQuery } from './payment-methods.graphql.js';
16
16
 
17
17
  export const Route = createFileRoute('/_authenticated/_payment-methods/payment-methods')({
18
18
  component: PaymentMethodListPage,
@@ -24,7 +24,6 @@ function PaymentMethodListPage() {
24
24
  <ListPage
25
25
  pageId="payment-method-list"
26
26
  listQuery={paymentMethodListQuery}
27
- deleteMutation={deletePaymentMethodDocument}
28
27
  route={Route}
29
28
  title="Payment Methods"
30
29
  defaultVisibility={{
@@ -11,7 +11,7 @@ import {
11
11
  DeleteProductVariantsBulkAction,
12
12
  RemoveProductVariantsFromChannelBulkAction,
13
13
  } from './components/product-variant-bulk-actions.js';
14
- import { deleteProductVariantDocument, productVariantListDocument } from './product-variants.graphql.js';
14
+ import { productVariantListDocument } from './product-variants.graphql.js';
15
15
 
16
16
  export const Route = createFileRoute('/_authenticated/_product-variants/product-variants')({
17
17
  component: ProductListPage,
@@ -25,7 +25,6 @@ function ProductListPage() {
25
25
  pageId="product-variant-list"
26
26
  title={<Trans>Product Variants</Trans>}
27
27
  listQuery={productVariantListDocument}
28
- deleteMutation={deleteProductVariantDocument}
29
28
  bulkActions={[
30
29
  {
31
30
  component: AssignProductVariantsToChannelBulkAction,
@@ -13,7 +13,7 @@ import {
13
13
  DuplicateProductsBulkAction,
14
14
  RemoveProductsFromChannelBulkAction,
15
15
  } from './components/product-bulk-actions.js';
16
- import { deleteProductDocument, productListDocument } from './products.graphql.js';
16
+ import { productListDocument } from './products.graphql.js';
17
17
 
18
18
  export const Route = createFileRoute('/_authenticated/_products/products')({
19
19
  component: ProductListPage,
@@ -25,7 +25,6 @@ function ProductListPage() {
25
25
  <ListPage
26
26
  pageId="product-list"
27
27
  listQuery={productListDocument}
28
- deleteMutation={deleteProductDocument}
29
28
  title="Products"
30
29
  customizeColumns={{
31
30
  name: {
@@ -13,7 +13,7 @@ import {
13
13
  DuplicatePromotionsBulkAction,
14
14
  RemovePromotionsFromChannelBulkAction,
15
15
  } from './components/promotion-bulk-actions.js';
16
- import { deletePromotionDocument, promotionListDocument } from './promotions.graphql.js';
16
+ import { promotionListDocument } from './promotions.graphql.js';
17
17
 
18
18
  export const Route = createFileRoute('/_authenticated/_promotions/promotions')({
19
19
  component: PromotionListPage,
@@ -25,7 +25,6 @@ function PromotionListPage() {
25
25
  <ListPage
26
26
  pageId="promotion-list"
27
27
  listQuery={promotionListDocument}
28
- deleteMutation={deletePromotionDocument}
29
28
  route={Route}
30
29
  title="Promotions"
31
30
  defaultVisibility={{
@@ -0,0 +1,251 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import { Switch } from '@/vdb/components/ui/switch.js';
3
+ import { Table, TableBody, TableCell, TableRow } from '@/vdb/components/ui/table.js';
4
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/vdb/components/ui/tooltip.js';
5
+ import { useGroupedPermissions } from '@/vdb/hooks/use-grouped-permissions.js';
6
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
7
+ import { ServerConfig } from '@/vdb/providers/server-config.js';
8
+ import { InfoIcon } from 'lucide-react';
9
+
10
+ interface PermissionsTableGridProps {
11
+ value: string[];
12
+ onChange: (permissions: string[]) => void;
13
+ readonly?: boolean;
14
+ }
15
+
16
+ export function PermissionsTableGrid({
17
+ value,
18
+ onChange,
19
+ readonly = false,
20
+ }: Readonly<PermissionsTableGridProps>) {
21
+ const { i18n } = useLingui();
22
+ const groupedPermissions = useGroupedPermissions();
23
+
24
+ const setPermission = (permission: string, checked: boolean) => {
25
+ if (readonly) return;
26
+
27
+ const newPermissions = checked ? [...value, permission] : value.filter(p => p !== permission);
28
+ onChange(newPermissions);
29
+ };
30
+
31
+ const toggleAll = (defs: ServerConfig['permissions']) => {
32
+ if (readonly) return;
33
+
34
+ const shouldEnable = defs.some(d => !value.includes(d.name));
35
+ const newPermissions = shouldEnable
36
+ ? [...new Set([...value, ...defs.map(d => d.name)])]
37
+ : value.filter(p => !defs.some(d => d.name === p));
38
+ onChange(newPermissions);
39
+ };
40
+
41
+ // Extract CRUD operation from permission name (e.g., "CreateAdministrator" -> "Create")
42
+ const getPermissionLabel = (permission: ServerConfig['permissions'][0], groupLabel: string) => {
43
+ const name = permission.name;
44
+ const crudPrefixes = ['Create', 'Read', 'Update', 'Delete'];
45
+
46
+ for (const prefix of crudPrefixes) {
47
+ if (name.startsWith(prefix)) {
48
+ // Check if the rest matches the group name (singular form)
49
+ const remainder = name.substring(prefix.length);
50
+ const groupSingular = groupLabel.replace(/s$/, ''); // Simple singularization
51
+ if (remainder.toLowerCase() === groupSingular.toLowerCase().replace(/\s/g, '')) {
52
+ return prefix;
53
+ }
54
+ }
55
+ }
56
+
57
+ // Fallback to full name if not a CRUD operation
58
+ return i18n.t(name);
59
+ };
60
+
61
+ return (
62
+ <div className="w-full">
63
+ {/* Desktop Table View */}
64
+ <div className="hidden md:block border rounded-lg">
65
+ <Table>
66
+ <TableBody>
67
+ {groupedPermissions.map((section, index) => (
68
+ <TableRow key={index} className="hover:bg-transparent">
69
+ <TableCell className="bg-muted/50 p-3 align-top w-[150px] min-w-[150px] border-r">
70
+ <div className="space-y-2">
71
+ <div className="flex items-center gap-2">
72
+ <span className="font-semibold text-sm">
73
+ {i18n.t(section.label)}
74
+ </span>
75
+ <TooltipProvider>
76
+ <Tooltip>
77
+ <TooltipTrigger asChild>
78
+ <InfoIcon className="h-3 w-3 text-muted-foreground" />
79
+ </TooltipTrigger>
80
+ <TooltipContent side="right" className="max-w-[250px]">
81
+ <p className="text-xs">
82
+ {i18n.t(section.description)}
83
+ </p>
84
+ </TooltipContent>
85
+ </Tooltip>
86
+ </TooltipProvider>
87
+ </div>
88
+ {section.permissions.length > 1 && !readonly && (
89
+ <Button
90
+ variant="outline"
91
+ size="sm"
92
+ onClick={() => toggleAll(section.permissions)}
93
+ className="h-6 px-2 text-xs"
94
+ >
95
+ <Trans>Toggle all</Trans>
96
+ </Button>
97
+ )}
98
+ </div>
99
+ </TableCell>
100
+ {sortPermissions(section.permissions).map((permission, permIndex) => (
101
+ <TableCell
102
+ key={permission.name}
103
+ className="p-2 text-center align-top min-w-[80px]"
104
+ colSpan={section.permissions.length === 1 ? 4 : 1}
105
+ >
106
+ <TooltipProvider>
107
+ <Tooltip>
108
+ <TooltipTrigger asChild>
109
+ <div className="flex flex-col items-center space-y-1.5">
110
+ <Switch
111
+ id={`${section.id}-${permission.name}`}
112
+ checked={value.includes(permission.name)}
113
+ onCheckedChange={checked =>
114
+ setPermission(permission.name, checked)
115
+ }
116
+ disabled={readonly}
117
+ className="scale-90"
118
+ />
119
+ <label
120
+ htmlFor={`${section.id}-${permission.name}`}
121
+ className="text-xs text-center cursor-pointer leading-tight"
122
+ >
123
+ {getPermissionLabel(permission, section.label)}
124
+ </label>
125
+ </div>
126
+ </TooltipTrigger>
127
+ <TooltipContent side="top" className="max-w-[250px]">
128
+ <div className="text-xs">
129
+ <div className="font-medium">
130
+ {i18n.t(permission.name)}
131
+ </div>
132
+ <div className="text-accent-foreground/70 mt-1">
133
+ {i18n.t(permission.description)}
134
+ </div>
135
+ </div>
136
+ </TooltipContent>
137
+ </Tooltip>
138
+ </TooltipProvider>
139
+ </TableCell>
140
+ ))}
141
+ {/* Fill remaining columns if less than 4 permissions */}
142
+ {section.permissions.length < 4 &&
143
+ section.permissions.length > 1 &&
144
+ Array.from({ length: 4 - section.permissions.length }).map(
145
+ (_, fillIndex) => (
146
+ <TableCell key={`fill-${fillIndex}`} className="p-3" />
147
+ ),
148
+ )}
149
+ </TableRow>
150
+ ))}
151
+ </TableBody>
152
+ </Table>
153
+ </div>
154
+
155
+ {/* Mobile Card View */}
156
+ <div className="md:hidden space-y-4">
157
+ {groupedPermissions.map((section, index) => (
158
+ <div key={index} className="border rounded-lg p-4 bg-card">
159
+ <div className="mb-3">
160
+ <div className="flex items-center gap-2 mb-2">
161
+ <span className="font-semibold text-sm">{i18n.t(section.label)}</span>
162
+ <TooltipProvider>
163
+ <Tooltip>
164
+ <TooltipTrigger asChild>
165
+ <InfoIcon className="h-3 w-3 text-muted-foreground" />
166
+ </TooltipTrigger>
167
+ <TooltipContent side="right" className="max-w-[250px]">
168
+ <p className="text-xs">{i18n.t(section.description)}</p>
169
+ </TooltipContent>
170
+ </Tooltip>
171
+ </TooltipProvider>
172
+ </div>
173
+ {section.permissions.length > 1 && !readonly && (
174
+ <Button
175
+ variant="outline"
176
+ size="sm"
177
+ onClick={() => toggleAll(section.permissions)}
178
+ className="h-6 px-2 text-xs"
179
+ >
180
+ <Trans>Toggle all</Trans>
181
+ </Button>
182
+ )}
183
+ </div>
184
+ <div className="grid grid-cols-2 gap-3">
185
+ {sortPermissions(section.permissions).map(permission => (
186
+ <div
187
+ key={permission.name}
188
+ className="flex items-center space-x-3 p-2 rounded border"
189
+ >
190
+ <Switch
191
+ id={`mobile-${section.id}-${permission.name}`}
192
+ checked={value.includes(permission.name)}
193
+ onCheckedChange={checked => setPermission(permission.name, checked)}
194
+ disabled={readonly}
195
+ />
196
+ <div className="flex-1 min-w-0">
197
+ <TooltipProvider>
198
+ <Tooltip>
199
+ <TooltipTrigger asChild>
200
+ <label
201
+ htmlFor={`mobile-${section.id}-${permission.name}`}
202
+ className="text-xs cursor-pointer block truncate"
203
+ >
204
+ {getPermissionLabel(permission, section.label)}
205
+ </label>
206
+ </TooltipTrigger>
207
+ <TooltipContent side="top" className="max-w-[250px]">
208
+ <div className="text-xs">
209
+ <div className="font-medium">
210
+ {i18n.t(permission.name)}
211
+ </div>
212
+ <div className="text-muted-foreground mt-1">
213
+ {i18n.t(permission.description)}
214
+ </div>
215
+ </div>
216
+ </TooltipContent>
217
+ </Tooltip>
218
+ </TooltipProvider>
219
+ </div>
220
+ </div>
221
+ ))}
222
+ </div>
223
+ </div>
224
+ ))}
225
+ </div>
226
+ </div>
227
+ );
228
+ }
229
+
230
+ // Sort permissions in CRUD order
231
+ const sortPermissions = (permissions: ServerConfig['permissions']) => {
232
+ const crudOrder = ['Create', 'Read', 'Update', 'Delete'];
233
+
234
+ return [...permissions].sort((a, b) => {
235
+ // Find the CRUD prefix for each permission
236
+ const aPrefix = crudOrder.find(prefix => a.name.startsWith(prefix));
237
+ const bPrefix = crudOrder.find(prefix => b.name.startsWith(prefix));
238
+
239
+ // If both have CRUD prefixes, sort by CRUD order
240
+ if (aPrefix && bPrefix) {
241
+ return crudOrder.indexOf(aPrefix) - crudOrder.indexOf(bPrefix);
242
+ }
243
+
244
+ // If only one has CRUD prefix, put it first
245
+ if (aPrefix && !bPrefix) return -1;
246
+ if (!aPrefix && bPrefix) return 1;
247
+
248
+ // Otherwise, keep original order
249
+ return 0;
250
+ });
251
+ };
@@ -12,7 +12,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
12
12
  import { LayersIcon, PlusIcon } from 'lucide-react';
13
13
  import { ExpandablePermissions } from './components/expandable-permissions.js';
14
14
  import { DeleteRolesBulkAction } from './components/role-bulk-actions.js';
15
- import { deleteRoleDocument, roleListQuery } from './roles.graphql.js';
15
+ import { roleListQuery } from './roles.graphql.js';
16
16
 
17
17
  export const Route = createFileRoute('/_authenticated/_roles/roles')({
18
18
  component: RoleListPage,
@@ -27,7 +27,6 @@ function RoleListPage() {
27
27
  pageId="role-list"
28
28
  title="Roles"
29
29
  listQuery={roleListQuery}
30
- deleteMutation={deleteRoleDocument}
31
30
  route={Route}
32
31
  defaultVisibility={{
33
32
  description: true,
@@ -19,7 +19,7 @@ import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
19
19
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
20
20
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
21
21
  import { toast } from 'sonner';
22
- import { PermissionsGrid } from './components/permissions-grid.js';
22
+ import { PermissionsTableGrid } from './components/permissions-table-grid.js';
23
23
  import { createRoleDocument, roleDetailDocument, updateRoleDocument } from './roles.graphql.js';
24
24
 
25
25
  const pageId = 'role-detail';
@@ -61,7 +61,9 @@ function RoleDetailPage() {
61
61
  },
62
62
  params: { id: params.id },
63
63
  onSuccess: async data => {
64
- toast.success(i18n.t(creatingNewEntity ? 'Successfully created role' : 'Successfully updated role'));
64
+ toast.success(
65
+ i18n.t(creatingNewEntity ? 'Successfully created role' : 'Successfully updated role'),
66
+ );
65
67
  resetForm();
66
68
  if (creatingNewEntity) {
67
69
  await navigate({ to: `../$id`, params: { id: data.id } });
@@ -132,7 +134,7 @@ function RoleDetailPage() {
132
134
  name="permissions"
133
135
  label={<Trans>Permissions</Trans>}
134
136
  render={({ field }) => (
135
- <PermissionsGrid
137
+ <PermissionsTableGrid
136
138
  value={field.value ?? []}
137
139
  onChange={value => field.onChange(value)}
138
140
  />
@@ -7,7 +7,7 @@ import { Trans } from '@/vdb/lib/trans.js';
7
7
  import { createFileRoute, Link } from '@tanstack/react-router';
8
8
  import { PlusIcon } from 'lucide-react';
9
9
  import { DeleteSellersBulkAction } from './components/seller-bulk-actions.js';
10
- import { deleteSellerDocument, sellerListQuery } from './sellers.graphql.js';
10
+ import { sellerListQuery } from './sellers.graphql.js';
11
11
 
12
12
  export const Route = createFileRoute('/_authenticated/_sellers/sellers')({
13
13
  component: SellerListPage,
@@ -19,7 +19,6 @@ function SellerListPage() {
19
19
  <ListPage
20
20
  pageId="seller-list"
21
21
  listQuery={sellerListQuery}
22
- deleteMutation={deleteSellerDocument}
23
22
  route={Route}
24
23
  title="Sellers"
25
24
  defaultVisibility={{
@@ -12,7 +12,7 @@ import {
12
12
  RemoveShippingMethodsFromChannelBulkAction,
13
13
  } from './components/shipping-method-bulk-actions.js';
14
14
  import { TestShippingMethodDialog } from './components/test-shipping-method-dialog.js';
15
- import { deleteShippingMethodDocument, shippingMethodListQuery } from './shipping-methods.graphql.js';
15
+ import { shippingMethodListQuery } from './shipping-methods.graphql.js';
16
16
 
17
17
  export const Route = createFileRoute('/_authenticated/_shipping-methods/shipping-methods')({
18
18
  component: ShippingMethodListPage,
@@ -24,7 +24,6 @@ function ShippingMethodListPage() {
24
24
  <ListPage
25
25
  pageId="shipping-method-list"
26
26
  listQuery={shippingMethodListQuery}
27
- deleteMutation={deleteShippingMethodDocument}
28
27
  route={Route}
29
28
  title="Shipping Methods"
30
29
  defaultVisibility={{
@@ -11,7 +11,7 @@ import {
11
11
  DeleteStockLocationsBulkAction,
12
12
  RemoveStockLocationsFromChannelBulkAction,
13
13
  } from './components/stock-location-bulk-actions.js';
14
- import { deleteStockLocationDocument, stockLocationListQuery } from './stock-locations.graphql.js';
14
+ import { stockLocationListQuery } from './stock-locations.graphql.js';
15
15
 
16
16
  export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations')({
17
17
  component: StockLocationListPage,
@@ -24,7 +24,6 @@ function StockLocationListPage() {
24
24
  pageId="stock-location-list"
25
25
  title="Stock Locations"
26
26
  listQuery={stockLocationListQuery}
27
- deleteMutation={deleteStockLocationDocument}
28
27
  route={Route}
29
28
  customizeColumns={{
30
29
  name: {
@@ -8,7 +8,7 @@ import { Trans } from '@/vdb/lib/trans.js';
8
8
  import { createFileRoute, Link } from '@tanstack/react-router';
9
9
  import { PlusIcon } from 'lucide-react';
10
10
  import { DeleteTaxCategoriesBulkAction } from './components/tax-category-bulk-actions.js';
11
- import { deleteTaxCategoryDocument, taxCategoryListQuery } from './tax-categories.graphql.js';
11
+ import { taxCategoryListQuery } from './tax-categories.graphql.js';
12
12
 
13
13
  export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories')({
14
14
  component: TaxCategoryListPage,
@@ -20,7 +20,6 @@ function TaxCategoryListPage() {
20
20
  <ListPage
21
21
  pageId="tax-category-list"
22
22
  listQuery={taxCategoryListQuery}
23
- deleteMutation={deleteTaxCategoryDocument}
24
23
  route={Route}
25
24
  title="Tax Categories"
26
25
  defaultVisibility={{
@@ -11,7 +11,7 @@ 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
13
  import { DeleteTaxRatesBulkAction } from './components/tax-rate-bulk-actions.js';
14
- import { deleteTaxRateDocument, taxRateListQuery } from './tax-rates.graphql.js';
14
+ import { taxRateListQuery } from './tax-rates.graphql.js';
15
15
 
16
16
  export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates')({
17
17
  component: TaxRateListPage,
@@ -23,7 +23,6 @@ function TaxRateListPage() {
23
23
  <ListPage
24
24
  pageId="tax-rate-list"
25
25
  listQuery={taxRateListQuery}
26
- deleteMutation={deleteTaxRateDocument}
27
26
  route={Route}
28
27
  title="Tax Rates"
29
28
  defaultVisibility={{