@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.3.4-master-202506181251",
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-202506181251",
90
- "@vendure/core": "^3.3.4-master-202506181251",
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": "3f76be479246dfceb21000abc6951790ee280a73"
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 { NavMenuSection, NavMenuItem } from '@/framework/nav-menu/nav-menu-extensions.js';
13
- import { Link, rootRouteId, useLocation, useMatch } from '@tanstack/react-router';
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
- // Split sections into top and bottom groups based on placement property
23
- const topSections = items.filter(item => item.placement === 'top');
24
- const bottomSections = items.filter(item => item.placement === 'bottom');
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 (data) => {
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.map(fieldInfo => {
147
- if (fieldInfo.name === 'id' && fieldInfo.type === 'ID') {
148
- return null;
149
- }
150
- return (
151
- <FormFieldWrapper
152
- key={fieldInfo.name}
153
- control={form.control}
154
- name={fieldInfo.name as never}
155
- label={fieldInfo.name}
156
- render={({ field }) => {
157
- switch (fieldInfo.type) {
158
- case 'Int':
159
- case 'Float':
160
- return (
161
- <Input
162
- type="number"
163
- value={field.value}
164
- onChange={e => field.onChange(e.target.valueAsNumber)}
165
- />
166
- );
167
- case 'DateTime':
168
- return <DateTimeInput {...field} />;
169
- case 'Boolean':
170
- return <Checkbox value={field.value} onCheckedChange={field.onChange} />;
171
- case 'String':
172
- default:
173
- return <Input {...field} />;
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
  );