@vendure/dashboard 3.4.3-master-202509200226 → 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.
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +302 -0
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -0
- package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +61 -0
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +17 -10
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +31 -0
- package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +50 -0
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +17 -290
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +7 -39
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +4 -26
- package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +129 -0
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +8 -0
- package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +251 -0
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -3
- package/src/lib/components/shared/detail-page-button.tsx +6 -4
- package/src/lib/framework/layout-engine/page-layout.tsx +96 -9
- package/src/lib/index.ts +7 -0
- package/src/app/routes/_authenticated/_roles/components/permissions-grid.tsx +0 -120
|
@@ -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
|
+
};
|
|
@@ -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 {
|
|
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(
|
|
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
|
-
<
|
|
137
|
+
<PermissionsTableGrid
|
|
136
138
|
value={field.value ?? []}
|
|
137
139
|
onChange={value => field.onChange(value)}
|
|
138
140
|
/>
|
|
@@ -13,7 +13,7 @@ import { Button } from '../ui/button.js';
|
|
|
13
13
|
* ```tsx
|
|
14
14
|
* // Basic usage with ID (relative navigation)
|
|
15
15
|
* <DetailPageButton id="123" label="Product Name" />
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
*
|
|
18
18
|
* @example
|
|
19
19
|
* ```tsx
|
|
@@ -36,18 +36,20 @@ export function DetailPageButton({
|
|
|
36
36
|
label,
|
|
37
37
|
disabled,
|
|
38
38
|
search,
|
|
39
|
-
|
|
39
|
+
className,
|
|
40
|
+
}: Readonly<{
|
|
40
41
|
label: string | React.ReactNode;
|
|
41
42
|
id?: string;
|
|
42
43
|
href?: string;
|
|
43
44
|
disabled?: boolean;
|
|
44
45
|
search?: Record<string, string>;
|
|
45
|
-
|
|
46
|
+
className?: string;
|
|
47
|
+
}>) {
|
|
46
48
|
if (!id && !href) {
|
|
47
49
|
return <span>{label}</span>;
|
|
48
50
|
}
|
|
49
51
|
return (
|
|
50
|
-
<Button asChild variant="ghost" disabled={disabled}>
|
|
52
|
+
<Button asChild variant="ghost" disabled={disabled} className={className}>
|
|
51
53
|
<Link to={href ?? `./${id}`} search={search ?? {}} preload={false}>
|
|
52
54
|
{label}
|
|
53
55
|
{!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
|
|
@@ -6,17 +6,24 @@ import { Form } from '@/vdb/components/ui/form.js';
|
|
|
6
6
|
import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
|
|
7
7
|
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
8
8
|
import { cn } from '@/vdb/lib/utils.js';
|
|
9
|
-
import { useMediaQuery } from '@uidotdev/usehooks';
|
|
10
|
-
import { EllipsisVerticalIcon } from 'lucide-react';
|
|
11
|
-
import React, { ComponentProps, useMemo } from 'react';
|
|
9
|
+
import { useCopyToClipboard, useMediaQuery } from '@uidotdev/usehooks';
|
|
10
|
+
import { CheckIcon, CopyIcon, EllipsisVerticalIcon, InfoIcon } from 'lucide-react';
|
|
11
|
+
import React, { ComponentProps, useMemo, useState } from 'react';
|
|
12
12
|
import { Control, UseFormReturn } from 'react-hook-form';
|
|
13
13
|
|
|
14
14
|
import { DashboardActionBarItem } from '../extension-api/types/layout.js';
|
|
15
15
|
|
|
16
16
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuLabel,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
18
24
|
import { PageBlockContext } from '@/vdb/framework/layout-engine/page-block-provider.js';
|
|
19
25
|
import { PageContext, PageContextValue } from '@/vdb/framework/layout-engine/page-provider.js';
|
|
26
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
20
27
|
import { getDashboardActionBarItems, getDashboardPageBlocks } from './layout-extensions.js';
|
|
21
28
|
import { LocationWrapper } from './location-wrapper.js';
|
|
22
29
|
|
|
@@ -50,13 +57,13 @@ export interface PageProps extends ComponentProps<'div'> {
|
|
|
50
57
|
* - {@link PageTitle}
|
|
51
58
|
* - {@link PageActionBar}
|
|
52
59
|
* - {@link PageLayout}
|
|
53
|
-
*
|
|
60
|
+
*
|
|
54
61
|
* @example
|
|
55
62
|
* ```tsx
|
|
56
63
|
* import { Page, PageTitle, PageActionBar, PageLayout, PageBlock, Button } from '\@vendure/dashboard';
|
|
57
|
-
*
|
|
64
|
+
*
|
|
58
65
|
* const pageId = 'my-page';
|
|
59
|
-
*
|
|
66
|
+
*
|
|
60
67
|
* export function MyPage() {
|
|
61
68
|
* return (
|
|
62
69
|
* <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
|
|
@@ -319,7 +326,7 @@ export function PageActionBar({ children }: Readonly<{ children: React.ReactNode
|
|
|
319
326
|
/**
|
|
320
327
|
* @description
|
|
321
328
|
* The PageActionBarLeft component should be used to display the left content of the action bar.
|
|
322
|
-
*
|
|
329
|
+
*
|
|
323
330
|
* @docsCategory page-layout
|
|
324
331
|
* @docsPage PageActionBar
|
|
325
332
|
* @since 3.3.0
|
|
@@ -330,6 +337,85 @@ export function PageActionBarLeft({ children }: Readonly<{ children: React.React
|
|
|
330
337
|
|
|
331
338
|
type InlineDropdownItem = Omit<DashboardActionBarItem, 'type' | 'pageId'>;
|
|
332
339
|
|
|
340
|
+
function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
|
|
341
|
+
const [copiedField, setCopiedField] = useState<string | null>(null);
|
|
342
|
+
const [, copy] = useCopyToClipboard();
|
|
343
|
+
|
|
344
|
+
const handleCopy = async (text: string, field: string) => {
|
|
345
|
+
await copy(text);
|
|
346
|
+
setCopiedField(field);
|
|
347
|
+
setTimeout(() => setCopiedField(null), 2000);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const formatDate = (dateString: string) => {
|
|
351
|
+
return new Date(dateString).toLocaleString();
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (!entity || !entity.id) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<DropdownMenu>
|
|
360
|
+
<DropdownMenuTrigger asChild>
|
|
361
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground">
|
|
362
|
+
<InfoIcon className="w-4 h-4" />
|
|
363
|
+
</Button>
|
|
364
|
+
</DropdownMenuTrigger>
|
|
365
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
366
|
+
<DropdownMenuLabel>
|
|
367
|
+
<Trans>Entity Information</Trans>
|
|
368
|
+
</DropdownMenuLabel>
|
|
369
|
+
<DropdownMenuSeparator />
|
|
370
|
+
<div className="px-3 py-2">
|
|
371
|
+
<div className="flex items-center justify-between">
|
|
372
|
+
<span className="text-sm font-medium">ID</span>
|
|
373
|
+
<div className="flex items-center gap-1">
|
|
374
|
+
<span className="text-sm font-mono">{entity.id}</span>
|
|
375
|
+
<button
|
|
376
|
+
onClick={() => handleCopy(entity.id, 'id')}
|
|
377
|
+
className="p-1 hover:bg-muted rounded-sm transition-colors"
|
|
378
|
+
>
|
|
379
|
+
{copiedField === 'id' ? (
|
|
380
|
+
<CheckIcon className="h-3 w-3 text-green-500" />
|
|
381
|
+
) : (
|
|
382
|
+
<CopyIcon className="h-3 w-3" />
|
|
383
|
+
)}
|
|
384
|
+
</button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
{entity.createdAt && (
|
|
389
|
+
<>
|
|
390
|
+
<DropdownMenuSeparator />
|
|
391
|
+
<div className="px-3 py-2">
|
|
392
|
+
<div className="text-sm">
|
|
393
|
+
<div className="font-medium text-muted-foreground">
|
|
394
|
+
<Trans>Created</Trans>
|
|
395
|
+
</div>
|
|
396
|
+
<div className="text-xs">{formatDate(entity.createdAt)}</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</>
|
|
400
|
+
)}
|
|
401
|
+
{entity.updatedAt && (
|
|
402
|
+
<>
|
|
403
|
+
<DropdownMenuSeparator />
|
|
404
|
+
<div className="px-3 py-2">
|
|
405
|
+
<div className="text-sm">
|
|
406
|
+
<div className="font-medium text-muted-foreground">
|
|
407
|
+
<Trans>Updated</Trans>
|
|
408
|
+
</div>
|
|
409
|
+
<div className="text-xs">{formatDate(entity.updatedAt)}</div>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</>
|
|
413
|
+
)}
|
|
414
|
+
</DropdownMenuContent>
|
|
415
|
+
</DropdownMenu>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
333
419
|
/**
|
|
334
420
|
* @description
|
|
335
421
|
* The PageActionBarRight component should be used to display the right content of the action bar.
|
|
@@ -366,6 +452,7 @@ export function PageActionBarRight({
|
|
|
366
452
|
{actionBarDropdownItems.length > 0 && (
|
|
367
453
|
<PageActionBarDropdown items={actionBarDropdownItems} page={page} />
|
|
368
454
|
)}
|
|
455
|
+
<EntityInfoDropdown entity={page.entity} />
|
|
369
456
|
</div>
|
|
370
457
|
);
|
|
371
458
|
}
|
|
@@ -449,7 +536,7 @@ export type PageBlockProps = {
|
|
|
449
536
|
* A component for displaying a block of content on a page. This should be used inside the {@link PageLayout} component.
|
|
450
537
|
* It should be provided with a `column` prop to determine which column it should appear in, and a `blockId` prop
|
|
451
538
|
* to identify the block.
|
|
452
|
-
*
|
|
539
|
+
*
|
|
453
540
|
* @example
|
|
454
541
|
* ```tsx
|
|
455
542
|
* <PageBlock column="main" blockId="my-block">
|
package/src/lib/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export * from './components/data-table/filters/data-table-string-filter.js';
|
|
|
44
44
|
export * from './components/data-table/human-readable-operator.js';
|
|
45
45
|
export * from './components/data-table/refresh-button.js';
|
|
46
46
|
export * from './components/data-table/types.js';
|
|
47
|
+
export * from './components/data-table/use-all-bulk-actions.js';
|
|
47
48
|
export * from './components/data-table/use-generated-columns.js';
|
|
48
49
|
export * from './components/labeled-data.js';
|
|
49
50
|
export * from './components/layout/app-layout.js';
|
|
@@ -110,6 +111,12 @@ export * from './components/shared/paginated-list-data-table.js';
|
|
|
110
111
|
export * from './components/shared/permission-guard.js';
|
|
111
112
|
export * from './components/shared/product-variant-selector.js';
|
|
112
113
|
export * from './components/shared/remove-from-channel-bulk-action.js';
|
|
114
|
+
export * from './components/shared/rich-text-editor/image-dialog.js';
|
|
115
|
+
export * from './components/shared/rich-text-editor/link-dialog.js';
|
|
116
|
+
export * from './components/shared/rich-text-editor/responsive-toolbar.js';
|
|
117
|
+
export * from './components/shared/rich-text-editor/rich-text-editor.js';
|
|
118
|
+
export * from './components/shared/rich-text-editor/table-delete-menu.js';
|
|
119
|
+
export * from './components/shared/rich-text-editor/table-edit-icons.js';
|
|
113
120
|
export * from './components/shared/role-code-label.js';
|
|
114
121
|
export * from './components/shared/role-selector.js';
|
|
115
122
|
export * from './components/shared/seller-selector.js';
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Accordion,
|
|
3
|
-
AccordionContent,
|
|
4
|
-
AccordionItem,
|
|
5
|
-
AccordionTrigger,
|
|
6
|
-
} from '@/vdb/components/ui/accordion.js';
|
|
7
|
-
import { Button } from '@/vdb/components/ui/button.js';
|
|
8
|
-
import { Switch } from '@/vdb/components/ui/switch.js';
|
|
9
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/vdb/components/ui/tooltip.js';
|
|
10
|
-
import { useGroupedPermissions } from '@/vdb/hooks/use-grouped-permissions.js';
|
|
11
|
-
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
12
|
-
import { ServerConfig } from '@/vdb/providers/server-config.js';
|
|
13
|
-
import { useState } from 'react';
|
|
14
|
-
|
|
15
|
-
interface PermissionsGridProps {
|
|
16
|
-
value: string[];
|
|
17
|
-
onChange: (permissions: string[]) => void;
|
|
18
|
-
readonly?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function PermissionsGrid({ value, onChange, readonly = false }: Readonly<PermissionsGridProps>) {
|
|
22
|
-
const { i18n } = useLingui();
|
|
23
|
-
const groupedPermissions = useGroupedPermissions();
|
|
24
|
-
|
|
25
|
-
const setPermission = (permission: string, checked: boolean) => {
|
|
26
|
-
if (readonly) return;
|
|
27
|
-
|
|
28
|
-
const newPermissions = checked ? [...value, permission] : value.filter(p => p !== permission);
|
|
29
|
-
onChange(newPermissions);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const toggleAll = (defs: ServerConfig['permissions']) => {
|
|
33
|
-
if (readonly) return;
|
|
34
|
-
|
|
35
|
-
const shouldEnable = defs.some(d => !value.includes(d.name));
|
|
36
|
-
const newPermissions = shouldEnable
|
|
37
|
-
? [...new Set([...value, ...defs.map(d => d.name)])]
|
|
38
|
-
: value.filter(p => !defs.some(d => d.name === p));
|
|
39
|
-
onChange(newPermissions);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Get default expanded sections based on which ones have active permissions
|
|
43
|
-
const defaultExpandedSections = groupedPermissions
|
|
44
|
-
.map(section => ({
|
|
45
|
-
section,
|
|
46
|
-
hasActivePermissions: section.permissions.some(permission => value.includes(permission.name)),
|
|
47
|
-
}))
|
|
48
|
-
.filter(({ hasActivePermissions }) => hasActivePermissions)
|
|
49
|
-
.map(({ section }) => section.id);
|
|
50
|
-
|
|
51
|
-
const [accordionValue, setAccordionValue] = useState<string[]>(defaultExpandedSections);
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className="w-full">
|
|
55
|
-
<Accordion
|
|
56
|
-
type="multiple"
|
|
57
|
-
value={accordionValue.length ? accordionValue : defaultExpandedSections}
|
|
58
|
-
onValueChange={setAccordionValue}
|
|
59
|
-
className="space-y-4"
|
|
60
|
-
>
|
|
61
|
-
{groupedPermissions.map((section, index) => (
|
|
62
|
-
<AccordionItem key={index} value={section.id} className="border rounded-lg px-6">
|
|
63
|
-
<AccordionTrigger className="hover:no-underline">
|
|
64
|
-
<div className="flex flex-col items-start gap-1 text-sm py-2">
|
|
65
|
-
<div>{i18n.t(section.label)}</div>
|
|
66
|
-
<div className="text-muted-foreground text-sm font-normal">
|
|
67
|
-
{i18n.t(section.description)}
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
</AccordionTrigger>
|
|
71
|
-
<AccordionContent>
|
|
72
|
-
<div className="pb-4 space-y-4">
|
|
73
|
-
{section.permissions.length > 1 && !readonly && (
|
|
74
|
-
<Button
|
|
75
|
-
variant="outline"
|
|
76
|
-
type="button"
|
|
77
|
-
size="sm"
|
|
78
|
-
onClick={() => toggleAll(section.permissions)}
|
|
79
|
-
className="w-fit"
|
|
80
|
-
>
|
|
81
|
-
<Trans>Toggle all</Trans>
|
|
82
|
-
</Button>
|
|
83
|
-
)}
|
|
84
|
-
<div className="md:grid md:grid-cols-4 md:gap-2 space-y-2">
|
|
85
|
-
{section.permissions.map(permission => (
|
|
86
|
-
<div key={permission.name} className="flex items-center space-x-2">
|
|
87
|
-
<Switch
|
|
88
|
-
id={permission.name}
|
|
89
|
-
checked={value.includes(permission.name)}
|
|
90
|
-
onCheckedChange={checked =>
|
|
91
|
-
setPermission(permission.name, checked)
|
|
92
|
-
}
|
|
93
|
-
disabled={readonly}
|
|
94
|
-
/>
|
|
95
|
-
<TooltipProvider>
|
|
96
|
-
<Tooltip>
|
|
97
|
-
<TooltipTrigger asChild>
|
|
98
|
-
<label
|
|
99
|
-
htmlFor={permission.name}
|
|
100
|
-
className="text-sm whitespace-nowrap"
|
|
101
|
-
>
|
|
102
|
-
{i18n.t(permission.name)}
|
|
103
|
-
</label>
|
|
104
|
-
</TooltipTrigger>
|
|
105
|
-
<TooltipContent align="end">
|
|
106
|
-
<p>{i18n.t(permission.description)}</p>
|
|
107
|
-
</TooltipContent>
|
|
108
|
-
</Tooltip>
|
|
109
|
-
</TooltipProvider>
|
|
110
|
-
</div>
|
|
111
|
-
))}
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
</AccordionContent>
|
|
115
|
-
</AccordionItem>
|
|
116
|
-
))}
|
|
117
|
-
</Accordion>
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
}
|