@vendure/dashboard 3.3.8 → 3.4.0

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 (131) hide show
  1. package/README.md +62 -0
  2. package/dist/plugin/api/api-extensions.d.ts +1 -0
  3. package/dist/plugin/api/api-extensions.js +38 -0
  4. package/dist/plugin/api/metrics.resolver.d.ts +8 -0
  5. package/dist/plugin/api/metrics.resolver.js +40 -0
  6. package/dist/plugin/config/metrics-strategies.d.ts +39 -0
  7. package/dist/plugin/config/metrics-strategies.js +74 -0
  8. package/dist/plugin/constants.d.ts +4 -3
  9. package/dist/plugin/constants.js +10 -277
  10. package/dist/plugin/dashboard.plugin.d.ts +95 -0
  11. package/dist/plugin/dashboard.plugin.js +168 -0
  12. package/dist/plugin/index.d.ts +2 -1
  13. package/dist/plugin/index.js +18 -1
  14. package/dist/plugin/package.json +3 -0
  15. package/dist/plugin/service/metrics.service.d.ts +15 -0
  16. package/dist/plugin/service/metrics.service.js +145 -0
  17. package/dist/plugin/types.d.ts +20 -37
  18. package/dist/plugin/types.js +13 -1
  19. package/dist/vite/constants.d.ts +5 -0
  20. package/dist/vite/constants.js +277 -0
  21. package/dist/vite/index.d.ts +1 -0
  22. package/dist/vite/index.js +1 -0
  23. package/dist/vite/types.d.ts +40 -0
  24. package/dist/vite/utils/config-loader.js +1 -0
  25. package/dist/{plugin → vite}/utils/plugin-discovery.js +1 -1
  26. package/dist/vite/utils/ui-config.d.ts +3 -0
  27. package/dist/vite/utils/ui-config.js +30 -0
  28. package/dist/vite/vite-plugin-ui-config.d.ts +123 -0
  29. package/dist/{plugin → vite}/vite-plugin-ui-config.js +3 -11
  30. package/dist/{plugin → vite}/vite-plugin-vendure-dashboard.js +1 -1
  31. package/index.html +1 -1
  32. package/package.json +16 -7
  33. package/src/app/app-providers.tsx +1 -1
  34. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
  35. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +20 -35
  36. package/src/app/routes/_authenticated/_facets/facets.graphql.ts +40 -0
  37. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +147 -0
  38. package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +380 -33
  39. package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +1 -1
  40. package/src/app/routes/_authenticated/_system/healthchecks.tsx +1 -1
  41. package/src/app/routes/_authenticated/_system/job-queue.tsx +1 -0
  42. package/src/app/routes/_authenticated/index.tsx +2 -2
  43. package/src/app/routes/_authenticated.tsx +1 -1
  44. package/src/lib/components/data-input/rich-text-input.tsx +14 -8
  45. package/src/lib/components/data-table/data-table-bulk-actions.tsx +17 -4
  46. package/src/lib/components/layout/app-layout.tsx +2 -7
  47. package/src/lib/components/layout/channel-switcher.tsx +166 -57
  48. package/src/lib/components/layout/dev-mode-indicator.tsx +18 -0
  49. package/src/lib/components/layout/language-dialog.tsx +2 -1
  50. package/src/lib/components/layout/manage-languages-dialog.tsx +77 -40
  51. package/src/lib/components/layout/nav-item-wrapper.tsx +107 -0
  52. package/src/lib/components/layout/nav-main.tsx +196 -107
  53. package/src/lib/components/login/login-form.tsx +80 -45
  54. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +19 -4
  55. package/src/lib/components/shared/asset/asset-gallery.tsx +2 -2
  56. package/src/lib/components/shared/detail-page-button.tsx +42 -0
  57. package/src/lib/components/shared/history-timeline/history-entry-date.tsx +37 -0
  58. package/src/lib/components/shared/history-timeline/history-entry.tsx +135 -65
  59. package/src/lib/components/shared/history-timeline/history-note-input.tsx +4 -4
  60. package/src/lib/components/shared/history-timeline/history-timeline.tsx +7 -54
  61. package/src/lib/components/shared/translatable-form-field.tsx +16 -2
  62. package/src/lib/framework/defaults.ts +4 -10
  63. package/src/lib/framework/extension-api/define-dashboard-extension.ts +4 -0
  64. package/src/lib/framework/extension-api/extension-api-types.ts +11 -2
  65. package/src/lib/framework/extension-api/logic/index.ts +1 -0
  66. package/src/lib/framework/extension-api/logic/login.ts +17 -0
  67. package/src/lib/framework/extension-api/logic/navigation.ts +1 -0
  68. package/src/lib/framework/extension-api/types/data-table.ts +12 -3
  69. package/src/lib/framework/extension-api/types/detail-forms.ts +13 -0
  70. package/src/lib/framework/extension-api/types/form-components.ts +11 -0
  71. package/src/lib/framework/extension-api/types/index.ts +1 -0
  72. package/src/lib/framework/extension-api/types/layout.ts +3 -6
  73. package/src/lib/framework/extension-api/types/login.ts +96 -0
  74. package/src/lib/framework/extension-api/types/navigation.ts +57 -0
  75. package/src/lib/framework/extension-api/types/widgets.ts +0 -4
  76. package/src/lib/framework/extension-api/use-login-extensions.ts +26 -0
  77. package/src/lib/framework/layout-engine/dev-mode-button.tsx +24 -0
  78. package/src/lib/framework/layout-engine/location-wrapper.tsx +5 -12
  79. package/src/lib/framework/registry/global-registry.ts +4 -0
  80. package/src/lib/framework/registry/registry-types.ts +2 -0
  81. package/src/lib/graphql/api.ts +25 -3
  82. package/src/lib/graphql/graphql-env.d.ts +28 -28
  83. package/src/lib/graphql/settings-store-operations.ts +17 -0
  84. package/src/lib/hooks/use-floating-bulk-actions.ts +82 -0
  85. package/src/lib/hooks/use-local-format.ts +20 -5
  86. package/src/lib/index.ts +2 -1
  87. package/src/lib/providers/channel-provider.tsx +13 -11
  88. package/src/lib/providers/user-settings.tsx +78 -3
  89. package/src/lib/virtual.d.ts +26 -2
  90. package/src/vite-env.d.ts +2 -0
  91. package/vite/utils/plugin-discovery.ts +1 -1
  92. package/vite/utils/ui-config.ts +30 -42
  93. package/vite/vite-plugin-ui-config.ts +119 -17
  94. package/vite/vite-plugin-vendure-dashboard.ts +1 -1
  95. package/dist/plugin/utils/ui-config.d.ts +0 -3
  96. package/dist/plugin/utils/ui-config.js +0 -34
  97. package/dist/plugin/vite-plugin-ui-config.d.ts +0 -15
  98. package/src/app/routes/_authenticated/_facets/components/add-facet-value-dialog.tsx +0 -146
  99. package/src/lib/components/shared/rich-text-editor.tsx +0 -0
  100. /package/dist/{plugin/utils/ast-utils.spec.d.ts → vite/types.js} +0 -0
  101. /package/dist/{plugin → vite}/utils/ast-utils.d.ts +0 -0
  102. /package/dist/{plugin → vite}/utils/ast-utils.js +0 -0
  103. /package/dist/{plugin/utils/config-loader.d.ts → vite/utils/ast-utils.spec.d.ts} +0 -0
  104. /package/dist/{plugin → vite}/utils/ast-utils.spec.js +0 -0
  105. /package/dist/{plugin → vite}/utils/compiler.d.ts +0 -0
  106. /package/dist/{plugin → vite}/utils/compiler.js +0 -0
  107. /package/dist/{plugin/utils/config-loader.js → vite/utils/config-loader.d.ts} +0 -0
  108. /package/dist/{plugin → vite}/utils/logger.d.ts +0 -0
  109. /package/dist/{plugin → vite}/utils/logger.js +0 -0
  110. /package/dist/{plugin → vite}/utils/plugin-discovery.d.ts +0 -0
  111. /package/dist/{plugin → vite}/utils/schema-generator.d.ts +0 -0
  112. /package/dist/{plugin → vite}/utils/schema-generator.js +0 -0
  113. /package/dist/{plugin → vite}/utils/tsconfig-utils.d.ts +0 -0
  114. /package/dist/{plugin → vite}/utils/tsconfig-utils.js +0 -0
  115. /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.d.ts +0 -0
  116. /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.js +0 -0
  117. /package/dist/{plugin → vite}/vite-plugin-config-loader.d.ts +0 -0
  118. /package/dist/{plugin → vite}/vite-plugin-config-loader.js +0 -0
  119. /package/dist/{plugin → vite}/vite-plugin-config.d.ts +0 -0
  120. /package/dist/{plugin → vite}/vite-plugin-config.js +0 -0
  121. /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.d.ts +0 -0
  122. /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.js +0 -0
  123. /package/dist/{plugin → vite}/vite-plugin-gql-tada.d.ts +0 -0
  124. /package/dist/{plugin → vite}/vite-plugin-gql-tada.js +0 -0
  125. /package/dist/{plugin → vite}/vite-plugin-tailwind-source.d.ts +0 -0
  126. /package/dist/{plugin → vite}/vite-plugin-tailwind-source.js +0 -0
  127. /package/dist/{plugin → vite}/vite-plugin-theme.d.ts +0 -0
  128. /package/dist/{plugin → vite}/vite-plugin-theme.js +0 -0
  129. /package/dist/{plugin → vite}/vite-plugin-transform-index.d.ts +0 -0
  130. /package/dist/{plugin → vite}/vite-plugin-transform-index.js +0 -0
  131. /package/dist/{plugin → vite}/vite-plugin-vendure-dashboard.d.ts +0 -0
@@ -14,9 +14,10 @@ import {
14
14
  NavMenuSection,
15
15
  NavMenuSectionPlacement,
16
16
  } from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
17
- import { Link, useLocation } from '@tanstack/react-router';
17
+ import { Link, useRouter, useRouterState } from '@tanstack/react-router';
18
18
  import { ChevronRight } from 'lucide-react';
19
19
  import * as React from 'react';
20
+ import { NavItemWrapper } from './nav-item-wrapper.js';
20
21
 
21
22
  // Utility to sort items & sections by the optional `order` prop (ascending) and then alphabetically by title
22
23
  function sortByOrder<T extends { order?: number; title: string }>(a: T, b: T) {
@@ -28,10 +29,73 @@ function sortByOrder<T extends { order?: number; title: string }>(a: T, b: T) {
28
29
  return orderA - orderB;
29
30
  }
30
31
 
32
+ /**
33
+ * Escapes special regex characters in a string to be used as a literal pattern
34
+ */
35
+ function escapeRegexChars(str: string): string {
36
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
+ }
38
+
31
39
  export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavMenuItem> }>) {
32
- const location = useLocation();
33
- // State to track which bottom section is currently open
34
- const [openBottomSectionId, setOpenBottomSectionId] = React.useState<string | null>(null);
40
+ const router = useRouter();
41
+ const routerState = useRouterState();
42
+ const currentPath = routerState.location.pathname;
43
+ const basePath = router.basepath || '';
44
+
45
+ // Helper to check if a path is active
46
+ const isPathActive = React.useCallback(
47
+ (itemUrl: string) => {
48
+ // Remove basepath prefix from current path for comparison
49
+ const normalizedCurrentPath = basePath ? currentPath.replace(new RegExp(`^${escapeRegexChars(basePath)}`), '') : currentPath;
50
+
51
+ // Ensure normalized path starts with /
52
+ const cleanPath = normalizedCurrentPath.startsWith('/') ? normalizedCurrentPath : `/${normalizedCurrentPath}`;
53
+
54
+ // Special handling for root path
55
+ if (itemUrl === '/') {
56
+ return cleanPath === '/' || cleanPath === '';
57
+ }
58
+
59
+ // For other paths, check exact match or prefix match
60
+ return cleanPath === itemUrl || cleanPath.startsWith(`${itemUrl}/`);
61
+ },
62
+ [currentPath, basePath],
63
+ );
64
+
65
+ // Helper to find sections containing active routes
66
+ const findActiveSections = React.useCallback(
67
+ (sections: Array<NavMenuSection | NavMenuItem>) => {
68
+ const activeTopSections = new Set<string>();
69
+ let activeBottomSection: string | null = null;
70
+
71
+ for (const section of sections) {
72
+ if ('items' in section && section.items) {
73
+ const hasActiveItem = section.items.some(item => isPathActive(item.url));
74
+ if (hasActiveItem) {
75
+ if (section.placement === 'top') {
76
+ activeTopSections.add(section.id);
77
+ } else if (section.placement === 'bottom' && !activeBottomSection) {
78
+ activeBottomSection = section.id;
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ return { activeTopSections, activeBottomSection };
85
+ },
86
+ [isPathActive],
87
+ );
88
+
89
+ // Initialize state with active sections on mount
90
+ const [openBottomSectionId, setOpenBottomSectionId] = React.useState<string | null>(() => {
91
+ const { activeBottomSection } = findActiveSections(items);
92
+ return activeBottomSection;
93
+ });
94
+
95
+ const [openTopSectionIds, setOpenTopSectionIds] = React.useState<Set<string>>(() => {
96
+ const { activeTopSections } = findActiveSections(items);
97
+ return activeTopSections;
98
+ });
35
99
 
36
100
  // Helper to build a sorted list of sections for a given placement, memoized for stability
37
101
  const getSortedSections = React.useCallback(
@@ -52,6 +116,17 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
52
116
  const topSections = React.useMemo(() => getSortedSections('top'), [getSortedSections]);
53
117
  const bottomSections = React.useMemo(() => getSortedSections('bottom'), [getSortedSections]);
54
118
 
119
+ // Handle top section open/close (only one section open at a time)
120
+ const handleTopSectionToggle = (sectionId: string, isOpen: boolean) => {
121
+ if (isOpen) {
122
+ // When opening a section, close all others
123
+ setOpenTopSectionIds(new Set([sectionId]));
124
+ } else {
125
+ // When closing a section, remove it from the set
126
+ setOpenTopSectionIds(new Set());
127
+ }
128
+ };
129
+
55
130
  // Handle bottom section open/close
56
131
  const handleBottomSectionToggle = (sectionId: string, isOpen: boolean) => {
57
132
  if (isOpen) {
@@ -61,77 +136,81 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
61
136
  }
62
137
  };
63
138
 
64
- // Auto-open the bottom section that contains the current route
139
+ // Update open sections when route changes (for client-side navigation)
65
140
  React.useEffect(() => {
66
- const currentPath = location.pathname;
67
-
68
- // Check if the current path is in any bottom section
69
- for (const section of bottomSections) {
70
- const matchingItem =
71
- 'items' in section
72
- ? section.items?.find(
73
- item => currentPath === item.url || currentPath.startsWith(`${item.url}/`),
74
- )
75
- : null;
76
-
77
- if (matchingItem) {
78
- setOpenBottomSectionId(section.id);
79
- return;
80
- }
141
+ const { activeTopSections, activeBottomSection } = findActiveSections(items);
142
+
143
+ // Replace open sections with only the active one
144
+ setOpenTopSectionIds(activeTopSections);
145
+
146
+ if (activeBottomSection) {
147
+ setOpenBottomSectionId(activeBottomSection);
81
148
  }
82
- }, [location.pathname, bottomSections]);
149
+ }, [currentPath, items, findActiveSections]);
83
150
 
84
151
  // Render a top navigation section
85
152
  const renderTopSection = (item: NavMenuSection | NavMenuItem) => {
86
153
  if ('url' in item) {
87
154
  return (
88
- <SidebarMenuItem key={item.title}>
89
- <SidebarMenuButton tooltip={item.title} asChild isActive={location.pathname === item.url}>
90
- <Link to={item.url}>
91
- {item.icon && <item.icon />}
92
- <span>{item.title}</span>
93
- </Link>
94
- </SidebarMenuButton>
95
- </SidebarMenuItem>
155
+ <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
156
+ <SidebarMenuItem>
157
+ <SidebarMenuButton
158
+ tooltip={item.title}
159
+ asChild
160
+ isActive={isPathActive(item.url)}
161
+ >
162
+ <Link to={item.url}>
163
+ {item.icon && <item.icon />}
164
+ <span>{item.title}</span>
165
+ </Link>
166
+ </SidebarMenuButton>
167
+ </SidebarMenuItem>
168
+ </NavItemWrapper>
96
169
  );
97
170
  }
98
171
 
99
172
  return (
100
- <Collapsible
101
- key={item.title}
102
- asChild
103
- defaultOpen={item.defaultOpen}
104
- className="group/collapsible"
105
- >
106
- <SidebarMenuItem>
107
- <CollapsibleTrigger asChild>
108
- <SidebarMenuButton tooltip={item.title}>
109
- {item.icon && <item.icon />}
110
- <span>{item.title}</span>
111
- <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
112
- </SidebarMenuButton>
113
- </CollapsibleTrigger>
114
- <CollapsibleContent>
115
- <SidebarMenuSub>
116
- {item.items?.map(subItem => (
117
- <SidebarMenuSubItem key={subItem.title}>
118
- <SidebarMenuSubButton
119
- asChild
120
- isActive={
121
- location.pathname === subItem.url ||
122
- location.pathname.startsWith(`${subItem.url}/`)
123
- }
173
+ <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
174
+ <Collapsible
175
+ asChild
176
+ open={openTopSectionIds.has(item.id)}
177
+ onOpenChange={isOpen => handleTopSectionToggle(item.id, isOpen)}
178
+ className="group/collapsible"
179
+ >
180
+ <SidebarMenuItem>
181
+ <CollapsibleTrigger asChild>
182
+ <SidebarMenuButton tooltip={item.title}>
183
+ {item.icon && <item.icon />}
184
+ <span>{item.title}</span>
185
+ <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
186
+ </SidebarMenuButton>
187
+ </CollapsibleTrigger>
188
+ <CollapsibleContent>
189
+ <SidebarMenuSub>
190
+ {item.items?.map(subItem => (
191
+ <NavItemWrapper
192
+ key={subItem.title}
193
+ locationId={subItem.id}
194
+ order={subItem.order}
195
+ parentLocationId={item.id}
124
196
  >
125
- <Link to={subItem.url}>
126
- <span>{subItem.title}</span>
127
- </Link>
128
- </SidebarMenuSubButton>
129
- </SidebarMenuSubItem>
130
- ))}
131
- </SidebarMenuSub>
132
- </CollapsibleContent>
133
- </SidebarMenuItem>
134
- </Collapsible>
197
+ <SidebarMenuSubItem>
198
+ <SidebarMenuSubButton
199
+ asChild
200
+ isActive={isPathActive(subItem.url)}
201
+ >
202
+ <Link to={subItem.url}>
203
+ <span>{subItem.title}</span>
204
+ </Link>
205
+ </SidebarMenuSubButton>
206
+ </SidebarMenuSubItem>
207
+ </NavItemWrapper>
208
+ ))}
209
+ </SidebarMenuSub>
210
+ </CollapsibleContent>
211
+ </SidebarMenuItem>
212
+ </Collapsible>
213
+ </NavItemWrapper>
135
214
  );
136
215
  };
137
216
 
@@ -139,53 +218,64 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
139
218
  const renderBottomSection = (item: NavMenuSection | NavMenuItem) => {
140
219
  if ('url' in item) {
141
220
  return (
142
- <SidebarMenuItem key={item.title}>
143
- <SidebarMenuButton tooltip={item.title} asChild isActive={location.pathname === item.url}>
144
- <Link to={item.url}>
145
- {item.icon && <item.icon />}
146
- <span>{item.title}</span>
147
- </Link>
148
- </SidebarMenuButton>
149
- </SidebarMenuItem>
221
+ <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
222
+ <SidebarMenuItem>
223
+ <SidebarMenuButton
224
+ tooltip={item.title}
225
+ asChild
226
+ isActive={isPathActive(item.url)}
227
+ >
228
+ <Link to={item.url}>
229
+ {item.icon && <item.icon />}
230
+ <span>{item.title}</span>
231
+ </Link>
232
+ </SidebarMenuButton>
233
+ </SidebarMenuItem>
234
+ </NavItemWrapper>
150
235
  );
151
236
  }
152
237
  return (
153
- <Collapsible
154
- key={item.title}
155
- asChild
156
- open={openBottomSectionId === item.id}
157
- onOpenChange={isOpen => handleBottomSectionToggle(item.id, isOpen)}
158
- className="group/collapsible"
159
- >
160
- <SidebarMenuItem>
161
- <CollapsibleTrigger asChild>
162
- <SidebarMenuButton tooltip={item.title}>
163
- {item.icon && <item.icon />}
164
- <span>{item.title}</span>
165
- <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
166
- </SidebarMenuButton>
167
- </CollapsibleTrigger>
168
- <CollapsibleContent>
169
- <SidebarMenuSub>
170
- {item.items?.map(subItem => (
171
- <SidebarMenuSubItem key={subItem.title}>
172
- <SidebarMenuSubButton
173
- asChild
174
- isActive={
175
- location.pathname === subItem.url ||
176
- location.pathname.startsWith(`${subItem.url}/`)
177
- }
238
+ <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
239
+ <Collapsible
240
+ asChild
241
+ open={openBottomSectionId === item.id}
242
+ onOpenChange={isOpen => handleBottomSectionToggle(item.id, isOpen)}
243
+ className="group/collapsible"
244
+ >
245
+ <SidebarMenuItem>
246
+ <CollapsibleTrigger asChild>
247
+ <SidebarMenuButton tooltip={item.title}>
248
+ {item.icon && <item.icon />}
249
+ <span>{item.title}</span>
250
+ <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
251
+ </SidebarMenuButton>
252
+ </CollapsibleTrigger>
253
+ <CollapsibleContent>
254
+ <SidebarMenuSub>
255
+ {item.items?.map(subItem => (
256
+ <NavItemWrapper
257
+ key={subItem.title}
258
+ locationId={subItem.id}
259
+ order={subItem.order}
260
+ parentLocationId={item.id}
178
261
  >
179
- <Link to={subItem.url}>
180
- <span>{subItem.title}</span>
181
- </Link>
182
- </SidebarMenuSubButton>
183
- </SidebarMenuSubItem>
184
- ))}
185
- </SidebarMenuSub>
186
- </CollapsibleContent>
187
- </SidebarMenuItem>
188
- </Collapsible>
262
+ <SidebarMenuSubItem>
263
+ <SidebarMenuSubButton
264
+ asChild
265
+ isActive={isPathActive(subItem.url)}
266
+ >
267
+ <Link to={subItem.url}>
268
+ <span>{subItem.title}</span>
269
+ </Link>
270
+ </SidebarMenuSubButton>
271
+ </SidebarMenuSubItem>
272
+ </NavItemWrapper>
273
+ ))}
274
+ </SidebarMenuSub>
275
+ </CollapsibleContent>
276
+ </SidebarMenuItem>
277
+ </Collapsible>
278
+ </NavItemWrapper>
189
279
  );
190
280
  };
191
281
 
@@ -193,7 +283,6 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
193
283
  <>
194
284
  {/* Top sections */}
195
285
  <SidebarGroup>
196
- <SidebarGroupLabel>Platform</SidebarGroupLabel>
197
286
  <SidebarMenu>{topSections.map(renderTopSection)}</SidebarMenu>
198
287
  </SidebarGroup>
199
288
 
@@ -10,8 +10,10 @@ import { useForm } from 'react-hook-form';
10
10
  import { toast } from 'sonner';
11
11
  import { uiConfig } from 'virtual:vendure-ui-config';
12
12
  import { z } from 'zod';
13
+ import { useLoginExtensions } from '../../framework/extension-api/use-login-extensions.js';
13
14
  import { LogoMark } from '../shared/logo-mark.js';
14
15
  import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form.js';
16
+ import { Separator } from '../ui/separator.js';
15
17
 
16
18
  export interface LoginFormProps extends React.ComponentProps<'div'> {
17
19
  loginError?: string;
@@ -19,7 +21,7 @@ export interface LoginFormProps extends React.ComponentProps<'div'> {
19
21
  onFormSubmit?: (username: string, password: string) => void;
20
22
  }
21
23
 
22
- type RemoteLoginImage = {
24
+ export type RemoteLoginImage = {
23
25
  urls: { regular: string };
24
26
  location: { name: string };
25
27
  user: { name: string; links: { html: string } };
@@ -32,6 +34,7 @@ const formSchema = z.object({
32
34
 
33
35
  export function LoginForm({ className, onFormSubmit, isVerifying, loginError, ...props }: LoginFormProps) {
34
36
  const [remoteLoginImage, setRemoteLoginImage] = React.useState<RemoteLoginImage | null>(null);
37
+ const loginExtensions = useLoginExtensions();
35
38
 
36
39
  React.useEffect(() => {
37
40
  if (!uiConfig.loginImageUrl) {
@@ -66,17 +69,39 @@ export function LoginForm({ className, onFormSubmit, isVerifying, loginError, ..
66
69
  >
67
70
  <div className="flex flex-col gap-6">
68
71
  <div className="flex flex-col items-start space-y-4">
69
- {!uiConfig.hideVendureBranding && (
70
- <LogoMark className="text-vendure-brand h-6 w-auto" />
72
+ {loginExtensions.logo ? (
73
+ <>
74
+ <loginExtensions.logo.component />
75
+ {loginExtensions.beforeForm && (
76
+ <>
77
+ <loginExtensions.beforeForm.component />
78
+ <Separator className="w-full" />
79
+ </>
80
+ )}
81
+ </>
82
+ ) : (
83
+ <>
84
+ {!uiConfig.hideVendureBranding && (
85
+ <LogoMark className="text-vendure-brand h-6 w-auto" />
86
+ )}
87
+ <div>
88
+ <h1 className="text-2xl font-medium">
89
+ <Trans>Welcome back!</Trans>
90
+ </h1>
91
+ <p className="text-muted-foreground text-balance">
92
+ Login to your Vendure store
93
+ </p>
94
+ </div>
95
+ {loginExtensions.beforeForm && (
96
+ <>
97
+ <Separator className="w-full" />
98
+ <div className="w-full">
99
+ <loginExtensions.beforeForm.component />
100
+ </div>
101
+ </>
102
+ )}
103
+ </>
71
104
  )}
72
- <div>
73
- <h1 className="text-2xl font-medium">
74
- <Trans>Welcome back!</Trans>
75
- </h1>
76
- <p className="text-muted-foreground text-balance">
77
- Login to your Vendure store
78
- </p>
79
- </div>
80
105
  </div>
81
106
  <FormField
82
107
  control={form.control}
@@ -117,7 +142,6 @@ export function LoginForm({ className, onFormSubmit, isVerifying, loginError, ..
117
142
  </FormItem>
118
143
  )}
119
144
  />
120
-
121
145
  <Button type="submit" disabled={isVerifying}>
122
146
  {isVerifying && (
123
147
  <>
@@ -128,44 +152,55 @@ export function LoginForm({ className, onFormSubmit, isVerifying, loginError, ..
128
152
  {!isVerifying && <span>Login</span>}
129
153
  </Button>
130
154
  </div>
155
+ {loginExtensions.afterForm && (
156
+ <>
157
+ <Separator className="w-full my-4" />
158
+
159
+ <loginExtensions.afterForm.component />
160
+ </>
161
+ )}
131
162
  </form>
132
163
  </Form>
133
- <div className="bg-muted relative hidden md:block lg:min-h-[500px]">
134
- {remoteLoginImage && (
135
- <>
164
+ {loginExtensions.loginImage ? (
165
+ <loginExtensions.loginImage.component />
166
+ ) : (
167
+ <div className="bg-muted relative hidden md:block lg:min-h-[500px]">
168
+ {remoteLoginImage && (
169
+ <>
170
+ <img
171
+ src={remoteLoginImage.urls.regular}
172
+ alt="Image"
173
+ className="absolute inset-0 h-full w-full object-cover"
174
+ />
175
+ <div className="absolute h-full w-full top-0 left-0 flex items-end justify-start bg-gradient-to-b from-transparent to-black/80 p-4 ">
176
+ <div>
177
+ <p className="text-lg font-medium text-white">
178
+ {remoteLoginImage.location.name}
179
+ </p>
180
+ <p className="text-sm text-white/80">
181
+ By
182
+ <a
183
+ className="mx-1 underline"
184
+ href={remoteLoginImage.user.links.html}
185
+ target="_blank"
186
+ >
187
+ {remoteLoginImage.user.name}
188
+ </a>
189
+ on Unsplash
190
+ </p>
191
+ </div>
192
+ </div>
193
+ </>
194
+ )}
195
+ {uiConfig.loginImageUrl && (
136
196
  <img
137
- src={remoteLoginImage.urls.regular}
138
- alt="Image"
197
+ src={uiConfig.loginImageUrl}
198
+ alt="Login image"
139
199
  className="absolute inset-0 h-full w-full object-cover"
140
200
  />
141
- <div className="absolute h-full w-full top-0 left-0 flex items-end justify-start bg-gradient-to-b from-transparent to-black/80 p-4 ">
142
- <div>
143
- <p className="text-lg font-medium text-white">
144
- {remoteLoginImage.location.name}
145
- </p>
146
- <p className="text-sm text-white/80">
147
- By
148
- <a
149
- className="mx-1 underline"
150
- href={remoteLoginImage.user.links.html}
151
- target="_blank"
152
- >
153
- {remoteLoginImage.user.name}
154
- </a>
155
- on Unsplash
156
- </p>
157
- </div>
158
- </div>
159
- </>
160
- )}
161
- {uiConfig.loginImageUrl && (
162
- <img
163
- src={uiConfig.loginImageUrl}
164
- alt="Login image"
165
- className="absolute inset-0 h-full w-full object-cover"
166
- />
167
- )}
168
- </div>
201
+ )}
202
+ </div>
203
+ )}
169
204
  </CardContent>
170
205
  </Card>
171
206
  </div>
@@ -8,6 +8,7 @@ import {
8
8
  DropdownMenuTrigger,
9
9
  } from '@/vdb/components/ui/dropdown-menu.js';
10
10
  import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
11
+ import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
11
12
  import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
12
13
  import { usePage } from '@/vdb/hooks/use-page.js';
13
14
  import { Trans } from '@/vdb/lib/trans.js';
@@ -34,9 +35,15 @@ interface AssetBulkActionsProps {
34
35
 
35
36
  export function AssetBulkActions({ selection, bulkActions, refetch }: Readonly<AssetBulkActionsProps>) {
36
37
  const { pageId } = usePage();
37
- const { blockId } = usePageBlock();
38
+ const pageBlock = usePageBlock();
39
+ const blockId = pageBlock?.blockId;
40
+
41
+ const { position, shouldShow } = useFloatingBulkActions({
42
+ selectionCount: selection.length,
43
+ containerSelector: '[data-asset-gallery]'
44
+ });
38
45
 
39
- if (selection.length === 0) {
46
+ if (!shouldShow) {
40
47
  return null;
41
48
  }
42
49
 
@@ -62,13 +69,21 @@ export function AssetBulkActions({ selection, bulkActions, refetch }: Readonly<A
62
69
  allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
63
70
 
64
71
  return (
65
- <div className="flex items-center gap-2 px-2 py-1 mb-2 bg-muted/50 rounded-md border">
72
+ <div
73
+ className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 fixed transform -translate-x-1/2 bg-white shadow-2xl rounded-md border z-50"
74
+ style={{
75
+ height: 'auto',
76
+ maxHeight: '60px',
77
+ bottom: position.bottom,
78
+ left: position.left
79
+ }}
80
+ >
66
81
  <span className="text-sm text-muted-foreground">
67
82
  <Trans>{selection.length} selected</Trans>
68
83
  </span>
69
84
  <DropdownMenu>
70
85
  <DropdownMenuTrigger asChild>
71
- <Button variant="outline" size="sm" className="h-8">
86
+ <Button variant="outline" size="sm" className="h-8 shadow-none">
72
87
  <Trans>With selected...</Trans>
73
88
  <ChevronDown className="ml-2 h-4 w-4" />
74
89
  </Button>
@@ -232,7 +232,7 @@ export function AssetGallery({
232
232
  };
233
233
 
234
234
  return (
235
- <div className={`flex flex-col w-full ${fixedHeight ? 'h-[600px]' : ''} ${className}`}>
235
+ <div className={`relative flex flex-col w-full ${fixedHeight ? 'h-[600px]' : ''} ${className}`}>
236
236
  {showHeader && (
237
237
  <div className="flex flex-col md:flex-row gap-2 mb-4 flex-shrink-0">
238
238
  <div className="relative flex-grow flex items-center gap-2">
@@ -291,7 +291,7 @@ export function AssetGallery({
291
291
  </div>
292
292
  )}
293
293
 
294
- <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 p-1">
294
+ <div data-asset-gallery className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 p-1">
295
295
  {isLoading ? (
296
296
  <div className="col-span-full flex justify-center py-12">
297
297
  <Loader2 className="h-8 w-8 animate-spin text-primary" />