@vendure/dashboard 3.3.4-master-202506181251 → 3.3.4-master-202506181504
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/lib/components/layout/app-sidebar.tsx +2 -1
- package/src/lib/components/layout/nav-main.tsx +35 -6
- package/src/lib/framework/defaults.ts +7 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +11 -1
- package/src/lib/framework/extension-api/extension-api-types.ts +13 -0
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +7 -0
- package/src/lib/framework/page/detail-page.tsx +57 -38
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.3.4-master-
|
|
4
|
+
"version": "3.3.4-master-202506181504",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"@types/react-dom": "^19.0.4",
|
|
87
87
|
"@types/react-grid-layout": "^1.3.5",
|
|
88
88
|
"@uidotdev/usehooks": "^2.4.1",
|
|
89
|
-
"@vendure/common": "^3.3.4-master-
|
|
90
|
-
"@vendure/core": "^3.3.4-master-
|
|
89
|
+
"@vendure/common": "^3.3.4-master-202506181504",
|
|
90
|
+
"@vendure/core": "^3.3.4-master-202506181504",
|
|
91
91
|
"@vitejs/plugin-react": "^4.3.4",
|
|
92
92
|
"awesome-graphql-client": "^2.1.0",
|
|
93
93
|
"class-variance-authority": "^0.7.1",
|
|
@@ -130,5 +130,5 @@
|
|
|
130
130
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
131
131
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
132
132
|
},
|
|
133
|
-
"gitHead": "
|
|
133
|
+
"gitHead": "6b6d55f050655378698459826c8f4786e5648afb"
|
|
134
134
|
}
|
|
@@ -7,14 +7,15 @@ import {
|
|
|
7
7
|
SidebarHeader,
|
|
8
8
|
SidebarRail,
|
|
9
9
|
} from '@/components/ui/sidebar.js';
|
|
10
|
-
import { getNavMenuConfig } from '@/framework/nav-menu/nav-menu-extensions.js';
|
|
11
10
|
import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
|
|
11
|
+
import { getNavMenuConfig } from '@/framework/nav-menu/nav-menu-extensions.js';
|
|
12
12
|
import * as React from 'react';
|
|
13
13
|
import { ChannelSwitcher } from './channel-switcher.js';
|
|
14
14
|
|
|
15
15
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
16
16
|
const { extensionsLoaded } = useDashboardExtensions();
|
|
17
17
|
const { sections } = getNavMenuConfig();
|
|
18
|
+
|
|
18
19
|
return (
|
|
19
20
|
extensionsLoaded && (
|
|
20
21
|
<Sidebar collapsible="icon" {...props}>
|
|
@@ -9,19 +9,48 @@ import {
|
|
|
9
9
|
SidebarMenuSubButton,
|
|
10
10
|
SidebarMenuSubItem,
|
|
11
11
|
} from '@/components/ui/sidebar.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
NavMenuItem,
|
|
14
|
+
NavMenuSection,
|
|
15
|
+
NavMenuSectionPlacement,
|
|
16
|
+
} from '@/framework/nav-menu/nav-menu-extensions.js';
|
|
17
|
+
import { Link, useLocation } from '@tanstack/react-router';
|
|
14
18
|
import { ChevronRight } from 'lucide-react';
|
|
15
19
|
import * as React from 'react';
|
|
16
20
|
|
|
21
|
+
// Utility to sort items & sections by the optional `order` prop (ascending) and then alphabetically by title
|
|
22
|
+
function sortByOrder<T extends { order?: number; title: string }>(a: T, b: T) {
|
|
23
|
+
const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
24
|
+
const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
25
|
+
if (orderA === orderB) {
|
|
26
|
+
return a.title.localeCompare(b.title);
|
|
27
|
+
}
|
|
28
|
+
return orderA - orderB;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export function NavMain({ items }: { items: Array<NavMenuSection | NavMenuItem> }) {
|
|
18
32
|
const location = useLocation();
|
|
19
33
|
// State to track which bottom section is currently open
|
|
20
34
|
const [openBottomSectionId, setOpenBottomSectionId] = React.useState<string | null>(null);
|
|
21
35
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
36
|
+
// Helper to build a sorted list of sections for a given placement, memoized for stability
|
|
37
|
+
const getSortedSections = React.useCallback(
|
|
38
|
+
(placement: NavMenuSectionPlacement) => {
|
|
39
|
+
return items
|
|
40
|
+
.filter(item => item.placement === placement)
|
|
41
|
+
.slice()
|
|
42
|
+
.sort(sortByOrder)
|
|
43
|
+
.map(section =>
|
|
44
|
+
'items' in section
|
|
45
|
+
? { ...section, items: section.items?.slice().sort(sortByOrder) }
|
|
46
|
+
: section,
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
[items],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const topSections = React.useMemo(() => getSortedSections('top'), [getSortedSections]);
|
|
53
|
+
const bottomSections = React.useMemo(() => getSortedSections('bottom'), [getSortedSections]);
|
|
25
54
|
|
|
26
55
|
// Handle bottom section open/close
|
|
27
56
|
const handleBottomSectionToggle = (sectionId: string, isOpen: boolean) => {
|
|
@@ -50,7 +79,7 @@ export function NavMain({ items }: { items: Array<NavMenuSection | NavMenuItem>
|
|
|
50
79
|
return;
|
|
51
80
|
}
|
|
52
81
|
}
|
|
53
|
-
}, [location.pathname]);
|
|
82
|
+
}, [location.pathname, bottomSections]);
|
|
54
83
|
|
|
55
84
|
// Render a top navigation section
|
|
56
85
|
const renderTopSection = (item: NavMenuSection | NavMenuItem) => {
|
|
@@ -23,6 +23,7 @@ export function registerDefaults() {
|
|
|
23
23
|
placement: 'top',
|
|
24
24
|
icon: LayoutDashboardIcon,
|
|
25
25
|
url: '/',
|
|
26
|
+
order: 100,
|
|
26
27
|
},
|
|
27
28
|
{
|
|
28
29
|
id: 'catalog',
|
|
@@ -30,6 +31,7 @@ export function registerDefaults() {
|
|
|
30
31
|
icon: SquareTerminal,
|
|
31
32
|
defaultOpen: true,
|
|
32
33
|
placement: 'top',
|
|
34
|
+
order: 200,
|
|
33
35
|
items: [
|
|
34
36
|
{
|
|
35
37
|
id: 'products',
|
|
@@ -64,6 +66,7 @@ export function registerDefaults() {
|
|
|
64
66
|
icon: ShoppingCart,
|
|
65
67
|
defaultOpen: true,
|
|
66
68
|
placement: 'top',
|
|
69
|
+
order: 300,
|
|
67
70
|
items: [
|
|
68
71
|
{
|
|
69
72
|
id: 'orders',
|
|
@@ -78,6 +81,7 @@ export function registerDefaults() {
|
|
|
78
81
|
icon: Users,
|
|
79
82
|
defaultOpen: false,
|
|
80
83
|
placement: 'top',
|
|
84
|
+
order: 400,
|
|
81
85
|
items: [
|
|
82
86
|
{
|
|
83
87
|
id: 'customers',
|
|
@@ -97,6 +101,7 @@ export function registerDefaults() {
|
|
|
97
101
|
icon: Mail,
|
|
98
102
|
defaultOpen: false,
|
|
99
103
|
placement: 'top',
|
|
104
|
+
order: 500,
|
|
100
105
|
items: [
|
|
101
106
|
{
|
|
102
107
|
id: 'promotions',
|
|
@@ -111,6 +116,7 @@ export function registerDefaults() {
|
|
|
111
116
|
icon: Terminal,
|
|
112
117
|
defaultOpen: false,
|
|
113
118
|
placement: 'bottom',
|
|
119
|
+
order: 100,
|
|
114
120
|
items: [
|
|
115
121
|
{
|
|
116
122
|
id: 'job-queue',
|
|
@@ -135,6 +141,7 @@ export function registerDefaults() {
|
|
|
135
141
|
icon: Settings2,
|
|
136
142
|
defaultOpen: false,
|
|
137
143
|
placement: 'bottom',
|
|
144
|
+
order: 200,
|
|
138
145
|
items: [
|
|
139
146
|
{
|
|
140
147
|
id: 'sellers',
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
registerDashboardActionBarItem,
|
|
4
4
|
registerDashboardPageBlock,
|
|
5
5
|
} from '../layout-engine/layout-extensions.js';
|
|
6
|
-
import { addNavMenuItem, NavMenuItem } from '../nav-menu/nav-menu-extensions.js';
|
|
6
|
+
import { addNavMenuItem, addNavMenuSection, NavMenuItem } from '../nav-menu/nav-menu-extensions.js';
|
|
7
7
|
import { registerRoute } from '../page/page-api.js';
|
|
8
8
|
import { globalRegistry } from '../registry/global-registry.js';
|
|
9
9
|
|
|
@@ -34,6 +34,16 @@ export function executeDashboardExtensionCallbacks() {
|
|
|
34
34
|
*/
|
|
35
35
|
export function defineDashboardExtension(extension: DashboardExtension) {
|
|
36
36
|
globalRegistry.get('registerDashboardExtensionCallbacks').add(() => {
|
|
37
|
+
if (extension.navSections) {
|
|
38
|
+
for (const section of extension.navSections) {
|
|
39
|
+
addNavMenuSection({
|
|
40
|
+
...section,
|
|
41
|
+
placement: 'top',
|
|
42
|
+
order: section.order ?? 999,
|
|
43
|
+
items: [],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
37
47
|
if (extension.routes) {
|
|
38
48
|
for (const route of extension.routes) {
|
|
39
49
|
if (route.navMenuItem) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PageContextValue } from '@/framework/layout-engine/page-provider.js';
|
|
2
2
|
import { AnyRoute, RouteOptions } from '@tanstack/react-router';
|
|
3
|
+
import { LucideIcon } from 'lucide-react';
|
|
3
4
|
import type React from 'react';
|
|
4
5
|
|
|
5
6
|
import { DashboardAlertDefinition } from '../alert/types.js';
|
|
@@ -18,6 +19,13 @@ export interface ActionBarButtonState {
|
|
|
18
19
|
visible: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export interface DashboardNavSectionDefinition {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
icon?: LucideIcon;
|
|
26
|
+
order?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* @description
|
|
23
31
|
* **Status: Developer Preview**
|
|
@@ -103,6 +111,11 @@ export interface DashboardExtension {
|
|
|
103
111
|
* Allows you to define custom routes such as list or detail views.
|
|
104
112
|
*/
|
|
105
113
|
routes?: DashboardRouteDefinition[];
|
|
114
|
+
/**
|
|
115
|
+
* @description
|
|
116
|
+
* Allows you to define custom nav sections for the dashboard.
|
|
117
|
+
*/
|
|
118
|
+
navSections?: DashboardNavSectionDefinition[];
|
|
106
119
|
/**
|
|
107
120
|
* @description
|
|
108
121
|
* Allows you to define custom page blocks for any page in the dashboard.
|
|
@@ -9,6 +9,7 @@ interface NavMenuBaseItem {
|
|
|
9
9
|
id: string;
|
|
10
10
|
title: string;
|
|
11
11
|
icon?: LucideIcon;
|
|
12
|
+
order?: number;
|
|
12
13
|
placement?: NavMenuSectionPlacement;
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -64,3 +65,9 @@ export function addNavMenuItem(item: NavMenuItem, sectionId: string) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
export function addNavMenuSection(section: NavMenuSection) {
|
|
70
|
+
const navMenuConfig = getNavMenuConfig();
|
|
71
|
+
navMenuConfig.sections = [...navMenuConfig.sections];
|
|
72
|
+
navMenuConfig.sections.push(section);
|
|
73
|
+
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
import { DateTimeInput } from '@/components/data-input/datetime-input.js';
|
|
1
2
|
import { FormFieldWrapper } from '@/components/shared/form-field-wrapper.js';
|
|
3
|
+
import { Button } from '@/components/ui/button.js';
|
|
4
|
+
import { Checkbox } from '@/components/ui/checkbox.js';
|
|
2
5
|
import { Input } from '@/components/ui/input.js';
|
|
6
|
+
import { NEW_ENTITY_PATH } from '@/constants.js';
|
|
3
7
|
import { useDetailPage } from '@/framework/page/use-detail-page.js';
|
|
4
8
|
import { Trans } from '@/lib/trans.js';
|
|
5
9
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
6
10
|
import { AnyRoute, useNavigate } from '@tanstack/react-router';
|
|
7
11
|
import { ResultOf, VariablesOf } from 'gql.tada';
|
|
8
|
-
import { DateTimeInput } from '@/components/data-input/datetime-input.js';
|
|
9
|
-
import { Button } from '@/components/ui/button.js';
|
|
10
|
-
import { Checkbox } from '@/components/ui/checkbox.js';
|
|
11
|
-
import { NEW_ENTITY_PATH } from '@/constants.js';
|
|
12
12
|
import { toast } from 'sonner';
|
|
13
13
|
import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
+
CustomFieldsPageBlock,
|
|
16
17
|
DetailFormGrid,
|
|
17
18
|
Page,
|
|
18
19
|
PageActionBar,
|
|
@@ -37,6 +38,11 @@ export interface DetailPageProps<
|
|
|
37
38
|
U extends TypedDocumentNode<any, any>,
|
|
38
39
|
EntityField extends keyof ResultOf<T> = DetailEntityPath<T>,
|
|
39
40
|
> {
|
|
41
|
+
/**
|
|
42
|
+
* @description
|
|
43
|
+
* The name of the entity.
|
|
44
|
+
*/
|
|
45
|
+
entityName?: string;
|
|
40
46
|
/**
|
|
41
47
|
* @description
|
|
42
48
|
* A unique identifier for the page.
|
|
@@ -94,6 +100,7 @@ export function DetailPage<
|
|
|
94
100
|
>({
|
|
95
101
|
pageId,
|
|
96
102
|
route,
|
|
103
|
+
entityName,
|
|
97
104
|
queryDocument,
|
|
98
105
|
createDocument,
|
|
99
106
|
updateDocument,
|
|
@@ -110,7 +117,7 @@ export function DetailPage<
|
|
|
110
117
|
createDocument,
|
|
111
118
|
params: { id: params.id },
|
|
112
119
|
setValuesForUpdate,
|
|
113
|
-
onSuccess: async
|
|
120
|
+
onSuccess: async data => {
|
|
114
121
|
toast.success('Updated successfully');
|
|
115
122
|
resetForm();
|
|
116
123
|
const id = (data as any).id;
|
|
@@ -143,41 +150,53 @@ export function DetailPage<
|
|
|
143
150
|
<PageLayout>
|
|
144
151
|
<PageBlock column="main" blockId="main-form">
|
|
145
152
|
<DetailFormGrid>
|
|
146
|
-
{updateFields
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
153
|
+
{updateFields
|
|
154
|
+
.filter(fieldInfo => fieldInfo.name !== 'customFields')
|
|
155
|
+
.map(fieldInfo => {
|
|
156
|
+
if (fieldInfo.name === 'id' && fieldInfo.type === 'ID') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
<FormFieldWrapper
|
|
161
|
+
key={fieldInfo.name}
|
|
162
|
+
control={form.control}
|
|
163
|
+
name={fieldInfo.name as never}
|
|
164
|
+
label={fieldInfo.name}
|
|
165
|
+
render={({ field }) => {
|
|
166
|
+
switch (fieldInfo.type) {
|
|
167
|
+
case 'Int':
|
|
168
|
+
case 'Float':
|
|
169
|
+
return (
|
|
170
|
+
<Input
|
|
171
|
+
type="number"
|
|
172
|
+
value={field.value}
|
|
173
|
+
onChange={e =>
|
|
174
|
+
field.onChange(e.target.valueAsNumber)
|
|
175
|
+
}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
case 'DateTime':
|
|
179
|
+
return <DateTimeInput {...field} />;
|
|
180
|
+
case 'Boolean':
|
|
181
|
+
return (
|
|
182
|
+
<Checkbox
|
|
183
|
+
value={field.value}
|
|
184
|
+
onCheckedChange={field.onChange}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
case 'String':
|
|
188
|
+
default:
|
|
189
|
+
return <Input {...field} />;
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
179
195
|
</DetailFormGrid>
|
|
180
196
|
</PageBlock>
|
|
197
|
+
{entityName && (
|
|
198
|
+
<CustomFieldsPageBlock column="main" entityType={entityName} control={form.control} />
|
|
199
|
+
)}
|
|
181
200
|
</PageLayout>
|
|
182
201
|
</Page>
|
|
183
202
|
);
|