@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
@@ -2,6 +2,48 @@ import { Link } from '@tanstack/react-router';
2
2
  import { ChevronRight } from 'lucide-react';
3
3
  import { Button } from '../ui/button.js';
4
4
 
5
+ /**
6
+ * DetailPageButton is a reusable navigation component designed to provide consistent UX
7
+ * across list views when linking to detail pages. It renders as a ghost button with
8
+ * a chevron indicator, making it easy for users to identify clickable links that
9
+ * navigate to detail views.
10
+ *
11
+ * @component
12
+ * @example
13
+ * // Basic usage with ID (relative navigation)
14
+ * <DetailPageButton id="123" label="Product Name" />
15
+ *
16
+ * @example
17
+ * // Custom href with search params
18
+ * <DetailPageButton
19
+ * href="/products/detail/456"
20
+ * label="Custom Product"
21
+ * search={{ tab: 'variants' }}
22
+ * />
23
+ *
24
+ * @example
25
+ * // Disabled state
26
+ * <DetailPageButton
27
+ * id="789"
28
+ * label="Unavailable Item"
29
+ * disabled={true}
30
+ * />
31
+ *
32
+ * @param {Object} props - Component props
33
+ * @param {string|React.ReactNode} props.label - The text or content to display in the button
34
+ * @param {string} [props.id] - The ID for relative navigation (creates href as `./${id}`)
35
+ * @param {string} [props.href] - Custom href for navigation (takes precedence over id)
36
+ * @param {boolean} [props.disabled=false] - Whether the button is disabled (prevents navigation)
37
+ * @param {Record<string, string>} [props.search] - Search parameters to include in the navigation
38
+ *
39
+ * @returns {React.ReactElement} A styled button component that navigates to detail pages
40
+ *
41
+ * @remarks
42
+ * - Uses TanStack Router's Link component for client-side navigation
43
+ * - Includes a chevron icon (hidden when disabled) to indicate navigation
44
+ * - Preloading is disabled by default for performance optimization
45
+ * - Styled as a ghost button variant for subtle, consistent appearance
46
+ */
5
47
  export function DetailPageButton({
6
48
  id,
7
49
  href,
@@ -0,0 +1,37 @@
1
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/vdb/components/ui/tooltip.js';
2
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
+
4
+ interface HistoryEntryDateProps {
5
+ date: string;
6
+ className?: string;
7
+ }
8
+
9
+ export function HistoryEntryDate({ date, className }: Readonly<HistoryEntryDateProps>) {
10
+ const { formatRelativeDate, formatDate } = useLocalFormat();
11
+
12
+ const formatFullDateTime = (dateString: string) => {
13
+ return formatDate(dateString, {
14
+ year: 'numeric',
15
+ month: 'long',
16
+ day: 'numeric',
17
+ hour: 'numeric',
18
+ minute: 'numeric',
19
+ second: 'numeric',
20
+ });
21
+ };
22
+
23
+ return (
24
+ <TooltipProvider>
25
+ <Tooltip>
26
+ <TooltipTrigger asChild>
27
+ <div className={className}>
28
+ {formatRelativeDate(date)}
29
+ </div>
30
+ </TooltipTrigger>
31
+ <TooltipContent>
32
+ <p>{formatFullDateTime(date)}</p>
33
+ </TooltipContent>
34
+ </Tooltip>
35
+ </TooltipProvider>
36
+ );
37
+ }
@@ -1,3 +1,4 @@
1
+ import { Badge } from '@/vdb/components/ui/badge.js';
1
2
  import { Button } from '@/vdb/components/ui/button.js';
2
3
  import {
3
4
  DropdownMenu,
@@ -6,11 +7,10 @@ import {
6
7
  DropdownMenuTrigger,
7
8
  } from '@/vdb/components/ui/dropdown-menu.js';
8
9
  import { Separator } from '@/vdb/components/ui/separator.js';
9
- import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
10
10
  import { Trans } from '@/vdb/lib/trans.js';
11
+ import { cn } from '@/vdb/lib/utils.js';
11
12
  import { MoreVerticalIcon, PencilIcon, TrashIcon } from 'lucide-react';
12
- import { useCallback } from 'react';
13
- import { useHistoryTimeline } from './history-timeline.js';
13
+ import { HistoryEntryDate } from './history-entry-date.js';
14
14
 
15
15
  export interface HistoryEntryItem {
16
16
  id: string;
@@ -25,12 +25,21 @@ export interface HistoryEntryItem {
25
25
  data: any;
26
26
  }
27
27
 
28
+ interface OrderCustomer {
29
+ firstName: string;
30
+ lastName: string;
31
+ }
32
+
28
33
  interface HistoryEntryProps {
29
34
  entry: HistoryEntryItem;
30
35
  isNoteEntry: boolean;
31
36
  timelineIcon: React.ReactNode;
32
37
  title: string | React.ReactNode;
33
38
  children: React.ReactNode;
39
+ isPrimary?: boolean;
40
+ customer?: OrderCustomer | null;
41
+ onEditNote?: (noteId: string, note: string, isPrivate: boolean) => void;
42
+ onDeleteNote?: (noteId: string) => void;
34
43
  }
35
44
 
36
45
  export function HistoryEntry({
@@ -39,80 +48,141 @@ export function HistoryEntry({
39
48
  timelineIcon,
40
49
  title,
41
50
  children,
51
+ isPrimary = true,
52
+ customer,
53
+ onEditNote,
54
+ onDeleteNote,
42
55
  }: Readonly<HistoryEntryProps>) {
43
- const { formatDate } = useLocalFormat();
44
- const { editNote, deleteNote } = useHistoryTimeline();
56
+ const getIconColor = (type: string) => {
57
+ // Check for success states (payment settled, order delivered)
58
+ if (type === 'ORDER_PAYMENT_TRANSITION' && entry.data.to === 'Settled') {
59
+ return 'bg-success text-success-foreground';
60
+ }
61
+ if (type === 'ORDER_STATE_TRANSITION' && entry.data.to === 'Delivered') {
62
+ return 'bg-success text-success-foreground';
63
+ }
64
+ if (type === 'ORDER_FULFILLMENT_TRANSITION' && entry.data.to === 'Delivered') {
65
+ return 'bg-success text-success-foreground';
66
+ }
67
+
68
+ // Check for destructive states (cancellations)
69
+ if (type === 'ORDER_CANCELLATION') {
70
+ return 'bg-destructive text-destructive-foreground';
71
+ }
72
+ if (type === 'ORDER_STATE_TRANSITION' && entry.data.to === 'Cancelled') {
73
+ return 'bg-destructive text-destructive-foreground';
74
+ }
75
+ if (type === 'ORDER_PAYMENT_TRANSITION' && (entry.data.to === 'Declined' || entry.data.to === 'Cancelled')) {
76
+ return 'bg-destructive text-destructive-foreground';
77
+ }
78
+
79
+ // All other entries use neutral colors
80
+ return 'bg-muted text-muted-foreground';
81
+ };
45
82
 
46
- const formatDateTime = useCallback(
47
- (date: string) => {
48
- return formatDate(date, {
49
- year: 'numeric',
50
- month: 'long',
51
- day: 'numeric',
52
- hour: 'numeric',
53
- minute: 'numeric',
54
- second: 'numeric',
55
- });
56
- },
57
- [formatDate],
58
- );
83
+ const getActorName = () => {
84
+ if (entry.administrator) {
85
+ return `${entry.administrator.firstName} ${entry.administrator.lastName}`;
86
+ } else if (customer) {
87
+ return `${customer.firstName} ${customer.lastName}`;
88
+ }
89
+ return '';
90
+ };
59
91
 
60
92
  return (
61
- <div key={entry.id} className="relative mb-4 pl-11">
62
- <div className="absolute left-0 w-10 flex items-center justify-center">
63
- <div className={`rounded-full flex items-center justify-center h-6 w-6`}>
93
+ <div key={entry.id} className="relative group">
94
+ <div
95
+ className={`flex gap-3 p-3 rounded-lg hover:bg-muted/30 transition-colors ${!isPrimary ? 'opacity-75' : ''}`}
96
+ >
97
+ <div className={cn(`relative z-10 flex-shrink-0 ${isNoteEntry ? 'ml-2' : ''}`, isPrimary ? '-ml-1' : '')}>
64
98
  <div
65
- className={`rounded-full bg-muted text-muted-foreground flex items-center justify-center h-6 w-6`}
99
+ className={`rounded-full flex items-center justify-center ${isPrimary ? 'h-8 w-8' : 'h-6 w-6'} ${getIconColor(entry.type)} border-2 border-background ${isPrimary ? 'shadow-sm' : 'shadow-none'}`}
66
100
  >
67
- {timelineIcon}
101
+ <div className={isPrimary ? 'text-current' : 'text-current scale-75'}>
102
+ {timelineIcon}
103
+ </div>
68
104
  </div>
69
105
  </div>
70
- </div>
71
106
 
72
- <div className="px-4 rounded-md">
73
- <div className="mt-2 text-sm text-muted-foreground flex items-center">
74
- <span>{formatDateTime(entry.createdAt)}</span>
75
- {entry.administrator && (
76
- <span className="ml-2">
77
- {entry.administrator.firstName} {entry.administrator.lastName}
78
- </span>
79
- )}
80
- </div>
81
- <div className="flex items-start justify-between">
82
- <div>
83
- <div className="font-medium text-sm">{title}</div>
84
- {children}
85
- </div>
107
+ <div className="flex-1 min-w-0">
108
+ <div className="flex items-start justify-between">
109
+ <div className="flex-1 min-w-0">
110
+ <h4
111
+ className={`text-sm ${isPrimary ? 'font-medium text-foreground' : 'font-normal text-muted-foreground'}`}
112
+ >
113
+ {title}
114
+ </h4>
115
+ <div className="mt-1">
116
+ {entry.type === 'ORDER_NOTE' ? (
117
+ <div className={`space-y-${isPrimary ? '2' : '1'}`}>
118
+ <p className={`${isPrimary ? 'text-sm' : 'text-xs'} text-foreground`}>
119
+ {entry.data.note}
120
+ </p>
121
+ <div className="flex items-center gap-2">
122
+ <Badge
123
+ variant={entry.isPublic ? 'outline' : 'secondary'}
124
+ className="text-xs"
125
+ >
126
+ {entry.isPublic ? 'Public' : 'Private'}
127
+ </Badge>
128
+ {isPrimary && onEditNote && onDeleteNote && (
129
+ <DropdownMenu>
130
+ <DropdownMenuTrigger asChild>
131
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0">
132
+ <MoreVerticalIcon className="h-3 w-3" />
133
+ </Button>
134
+ </DropdownMenuTrigger>
135
+ <DropdownMenuContent align="end">
136
+ <DropdownMenuItem
137
+ onClick={() => {
138
+ onEditNote(
139
+ entry.id,
140
+ entry.data.note,
141
+ !entry.isPublic,
142
+ );
143
+ }}
144
+ className="cursor-pointer"
145
+ >
146
+ <PencilIcon className="mr-2 h-4 w-4" />
147
+ <Trans>Edit</Trans>
148
+ </DropdownMenuItem>
149
+ <Separator className="my-1" />
150
+ <DropdownMenuItem
151
+ onClick={() => onDeleteNote(entry.id)}
152
+ className="cursor-pointer text-red-600 focus:text-red-600"
153
+ >
154
+ <TrashIcon className="mr-2 h-4 w-4" />
155
+ <span>Delete</span>
156
+ </DropdownMenuItem>
157
+ </DropdownMenuContent>
158
+ </DropdownMenu>
159
+ )}
160
+ </div>
161
+ </div>
162
+ ) : (
163
+ children
164
+ )}
165
+ </div>
166
+ </div>
86
167
 
87
- {isNoteEntry && (
88
- <DropdownMenu>
89
- <DropdownMenuTrigger asChild>
90
- <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
91
- <MoreVerticalIcon className="h-4 w-4" />
92
- </Button>
93
- </DropdownMenuTrigger>
94
- <DropdownMenuContent align="end">
95
- <DropdownMenuItem
96
- onClick={() => editNote(entry.id, entry.data.note, !entry.isPublic)}
97
- className="cursor-pointer"
98
- >
99
- <PencilIcon className="mr-2 h-4 w-4" />
100
- <Trans>Edit</Trans>
101
- </DropdownMenuItem>
102
- <Separator className="my-1" />
103
- <DropdownMenuItem
104
- onClick={() => deleteNote(entry.id)}
105
- className="cursor-pointer text-red-600 focus:text-red-600"
106
- >
107
- <TrashIcon className="mr-2 h-4 w-4" />
108
- <span>Delete</span>
109
- </DropdownMenuItem>
110
- </DropdownMenuContent>
111
- </DropdownMenu>
112
- )}
168
+ <div className="flex items-center gap-2 ml-4 flex-shrink-0">
169
+ <div className="text-right">
170
+ <HistoryEntryDate
171
+ date={entry.createdAt}
172
+ className={`text-xs cursor-help ${isPrimary ? 'text-muted-foreground' : 'text-muted-foreground/70'}`}
173
+ />
174
+ {getActorName() && (
175
+ <div
176
+ className={`text-xs ${isPrimary ? 'text-muted-foreground' : 'text-muted-foreground/70'}`}
177
+ >
178
+ {getActorName()}
179
+ </div>
180
+ )}
181
+ </div>
182
+ </div>
183
+ </div>
113
184
  </div>
114
185
  </div>
115
- <div className="border-b border-muted my-4 mx-4"></div>
116
186
  </div>
117
187
  );
118
188
  }
@@ -19,17 +19,17 @@ export function HistoryNoteInput({ onAddNote }: Readonly<HistoryNoteInputProps>)
19
19
  };
20
20
 
21
21
  return (
22
- <div className="border rounded-md p-4">
23
- <div className="flex flex-col space-y-4">
22
+ <div className="bg-muted/20 rounded-lg p-3 mb-4">
23
+ <div className="flex flex-col space-y-2">
24
24
  <Textarea
25
25
  placeholder="Add a note..."
26
26
  value={note}
27
27
  onChange={e => setNote(e.target.value)}
28
- className="min-h-[80px] resize-none"
28
+ className="min-h-[50px] resize-none bg-background/50 focus:bg-background transition-colors text-sm"
29
29
  />
30
30
  <div className="flex items-center justify-between">
31
31
  <HistoryNoteCheckbox value={noteIsPrivate} onChange={setNoteIsPrivate} />
32
- <Button onClick={handleAddNote} disabled={!note.trim()} size="sm">
32
+ <Button onClick={handleAddNote} disabled={!note.trim()} size="sm" className="h-7 px-3 text-xs">
33
33
  Add note
34
34
  </Button>
35
35
  </div>
@@ -1,63 +1,16 @@
1
1
  import { ScrollArea } from '@/vdb/components/ui/scroll-area.js';
2
- import { createContext, useContext, useState } from 'react';
3
- import { HistoryNoteEditor } from './history-note-editor.js';
4
2
 
5
3
  interface HistoryTimelineProps {
6
4
  children: React.ReactNode;
7
- onEditNote?: (entryId: string, note: string, isPublic: boolean) => void;
8
- onDeleteNote?: (entryId: string) => void;
9
5
  }
10
6
 
11
- // Use context to make the note editing functions available to the child
12
- // HistoryEntry component
13
- const HistoryTimelineContext = createContext<{
14
- editNote: (noteId: string, note: string, isPrivate: boolean) => void;
15
- deleteNote: (noteId: string) => void;
16
- }>({
17
- editNote: () => {},
18
- deleteNote: () => {},
19
- });
20
-
21
- type NoteEditorNote = { noteId: string; note: string; isPrivate: boolean };
22
-
23
- export function useHistoryTimeline() {
24
- return useContext(HistoryTimelineContext);
25
- }
26
-
27
- export function HistoryTimeline({ children, onEditNote, onDeleteNote }: Readonly<HistoryTimelineProps>) {
28
- const [noteEditorOpen, setNoteEditorOpen] = useState(false);
29
- const [noteEditorNote, setNoteEditorNote] = useState<NoteEditorNote>({
30
- noteId: '',
31
- note: '',
32
- isPrivate: true,
33
- });
34
-
35
- const editNote = (noteId: string, note: string, isPrivate: boolean) => {
36
- setNoteEditorNote({ noteId, note, isPrivate });
37
- setNoteEditorOpen(true);
38
- };
39
-
40
- const deleteNote = (noteId: string) => {
41
- setNoteEditorNote({ noteId, note: '', isPrivate: true });
42
- };
43
-
7
+ export function HistoryTimeline({ children }: Readonly<HistoryTimelineProps>) {
44
8
  return (
45
- <HistoryTimelineContext.Provider value={{ editNote, deleteNote }}>
46
- <ScrollArea className=" pr-4">
47
- <div className="relative">
48
- <div className="absolute left-5 top-0 bottom-[44px] w-0.5 bg-gray-200" />
49
- {children}
50
- </div>
51
- </ScrollArea>
52
- <HistoryNoteEditor
53
- key={noteEditorNote.noteId}
54
- note={noteEditorNote.note}
55
- onNoteChange={(...args) => onEditNote?.(...args)}
56
- open={noteEditorOpen}
57
- onOpenChange={setNoteEditorOpen}
58
- noteId={noteEditorNote.noteId}
59
- isPrivate={noteEditorNote.isPrivate}
60
- />
61
- </HistoryTimelineContext.Provider>
9
+ <ScrollArea className="pr-2">
10
+ <div className="relative">
11
+ <div className="absolute left-6 top-6 bottom-0 w-px bg-gradient-to-b from-border via-border/50 to-transparent" />
12
+ <div className="space-y-0.5">{children}</div>
13
+ </div>
14
+ </ScrollArea>
62
15
  );
63
16
  }
@@ -1,6 +1,8 @@
1
1
  import { OverriddenFormComponent } from '@/vdb/framework/form-engine/overridden-form-component.js';
2
2
  import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.js';
3
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
4
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
5
+ import { Trans } from '@/vdb/lib/trans.js';
4
6
  import { Controller, ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
5
7
  import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '../ui/form.js';
6
8
  import { FormFieldWrapper } from './form-field-wrapper.js';
@@ -13,6 +15,7 @@ export type TranslatableFormFieldProps<TFieldValues extends TranslatableEntity |
13
15
  ControllerProps<TFieldValues>,
14
16
  'name'
15
17
  > & {
18
+ label?: React.ReactNode;
16
19
  name: TFieldValues extends TranslatableEntity
17
20
  ? keyof Omit<NonNullable<TFieldValues['translations']>[number], 'languageCode'>
18
21
  : TFieldValues extends TranslatableEntity[]
@@ -24,16 +27,26 @@ export const TranslatableFormField = <
24
27
  TFieldValues extends TranslatableEntity | TranslatableEntity[] = TranslatableEntity,
25
28
  >({
26
29
  name,
30
+ label,
27
31
  ...props
28
32
  }: TranslatableFormFieldProps<TFieldValues>) => {
33
+ const { formatLanguageName } = useLocalFormat();
29
34
  const { contentLanguage } = useUserSettings().settings;
30
35
  const formValues = props.control?._formValues;
31
36
  const translations = Array.isArray(formValues) ? formValues?.[0].translations : formValues?.translations;
32
- const index = translations?.findIndex(
37
+ const existingIndex = translations?.findIndex(
33
38
  (translation: any) => translation?.languageCode === contentLanguage,
34
39
  );
40
+ const index = existingIndex === -1 ? translations?.length : existingIndex;
35
41
  if (index === undefined || index === -1) {
36
- return null;
42
+ return (
43
+ <FormItem>
44
+ {label && <FormLabel>{label}</FormLabel>}
45
+ <div className="text-sm text-muted-foreground">
46
+ <Trans>No translation found for {formatLanguageName(contentLanguage)}</Trans>
47
+ </div>
48
+ </FormItem>
49
+ );
37
50
  }
38
51
  const translationName = `translations.${index}.${String(name)}` as FieldPath<TFieldValues>;
39
52
  return <Controller {...props} name={translationName} key={translationName} />;
@@ -57,6 +70,7 @@ export const TranslatableFormFieldWrapper = <
57
70
  return (
58
71
  <LocationWrapper identifier={name as string}>
59
72
  <TranslatableFormField
73
+ label={label}
60
74
  control={props.control}
61
75
  name={name}
62
76
  render={renderArgs => (
@@ -18,8 +18,8 @@ export function registerDefaults() {
18
18
  setNavMenuConfig({
19
19
  sections: [
20
20
  {
21
- id: 'dashboard',
22
- title: 'Dashboard',
21
+ id: 'insights',
22
+ title: 'Insights',
23
23
  placement: 'top',
24
24
  icon: LayoutDashboardIcon,
25
25
  url: '/',
@@ -29,7 +29,6 @@ export function registerDefaults() {
29
29
  id: 'catalog',
30
30
  title: 'Catalog',
31
31
  icon: SquareTerminal,
32
- defaultOpen: true,
33
32
  placement: 'top',
34
33
  order: 200,
35
34
  items: [
@@ -69,7 +68,6 @@ export function registerDefaults() {
69
68
  id: 'sales',
70
69
  title: 'Sales',
71
70
  icon: ShoppingCart,
72
- defaultOpen: true,
73
71
  placement: 'top',
74
72
  order: 300,
75
73
  items: [
@@ -85,7 +83,6 @@ export function registerDefaults() {
85
83
  id: 'customers',
86
84
  title: 'Customers',
87
85
  icon: Users,
88
- defaultOpen: false,
89
86
  placement: 'top',
90
87
  order: 400,
91
88
  items: [
@@ -107,7 +104,6 @@ export function registerDefaults() {
107
104
  id: 'marketing',
108
105
  title: 'Marketing',
109
106
  icon: Mail,
110
- defaultOpen: false,
111
107
  placement: 'top',
112
108
  order: 500,
113
109
  items: [
@@ -123,9 +119,8 @@ export function registerDefaults() {
123
119
  id: 'system',
124
120
  title: 'System',
125
121
  icon: Terminal,
126
- defaultOpen: false,
127
122
  placement: 'bottom',
128
- order: 100,
123
+ order: 200,
129
124
  items: [
130
125
  {
131
126
  id: 'job-queue',
@@ -151,9 +146,8 @@ export function registerDefaults() {
151
146
  id: 'settings',
152
147
  title: 'Settings',
153
148
  icon: Settings2,
154
- defaultOpen: false,
155
149
  placement: 'bottom',
156
- order: 200,
150
+ order: 100,
157
151
  items: [
158
152
  {
159
153
  id: 'sellers',
@@ -7,6 +7,7 @@ import {
7
7
  registerDetailFormExtensions,
8
8
  registerFormComponentExtensions,
9
9
  registerLayoutExtensions,
10
+ registerLoginExtensions,
10
11
  registerNavigationExtensions,
11
12
  registerWidgetExtensions,
12
13
  } from './logic/index.js';
@@ -57,6 +58,9 @@ export function defineDashboardExtension(extension: DashboardExtension) {
57
58
  // Register alert extensions
58
59
  registerAlertExtensions(extension.alerts);
59
60
 
61
+ // Register login extensions
62
+ registerLoginExtensions(extension.login);
63
+
60
64
  // Execute extension source change callbacks
61
65
  const callbacks = globalRegistry.get('extensionSourceChangeCallbacks');
62
66
  if (callbacks.size) {
@@ -5,6 +5,7 @@ import {
5
5
  DashboardCustomFormComponents,
6
6
  DashboardDataTableExtensionDefinition,
7
7
  DashboardDetailFormExtensionDefinition,
8
+ DashboardLoginExtensions,
8
9
  DashboardNavSectionDefinition,
9
10
  DashboardPageBlockDefinition,
10
11
  DashboardRouteDefinition,
@@ -54,8 +55,7 @@ export interface DashboardExtension {
54
55
  widgets?: DashboardWidgetDefinition[];
55
56
  /**
56
57
  * @description
57
- * Unified registration for custom form components including custom field components,
58
- * input components, and display components.
58
+ * Unified registration for custom form custom field components.
59
59
  */
60
60
  customFormComponents?: DashboardCustomFormComponents;
61
61
  /**
@@ -63,5 +63,14 @@ export interface DashboardExtension {
63
63
  * Allows you to customize aspects of existing data tables in the dashboard.
64
64
  */
65
65
  dataTables?: DashboardDataTableExtensionDefinition[];
66
+ /**
67
+ * @description
68
+ * Allows you to customize the detail form for any page in the dashboard.
69
+ */
66
70
  detailForms?: DashboardDetailFormExtensionDefinition[];
71
+ /**
72
+ * @description
73
+ * Allows you to customize the login page with custom components.
74
+ */
75
+ login?: DashboardLoginExtensions;
67
76
  }
@@ -4,5 +4,6 @@ export * from './data-table.js';
4
4
  export * from './detail-forms.js';
5
5
  export * from './form-components.js';
6
6
  export * from './layout.js';
7
+ export * from './login.js';
7
8
  export * from './navigation.js';
8
9
  export * from './widgets.js';
@@ -0,0 +1,17 @@
1
+ import { globalRegistry } from '../../registry/global-registry.js';
2
+ import { DashboardLoginExtensions } from '../types/login.js';
3
+
4
+ export function registerLoginExtensions(loginExtensions?: DashboardLoginExtensions) {
5
+ if (!loginExtensions) {
6
+ return;
7
+ }
8
+
9
+ const registryKey = 'loginExtensions';
10
+
11
+ globalRegistry.set(registryKey, (oldValue: DashboardLoginExtensions) => {
12
+ return {
13
+ ...oldValue,
14
+ ...loginExtensions,
15
+ };
16
+ });
17
+ }
@@ -25,6 +25,7 @@ export function registerNavigationExtensions(
25
25
  url: route.navMenuItem.url ?? route.path,
26
26
  id: route.navMenuItem.id ?? route.path,
27
27
  title: route.navMenuItem.title ?? route.path,
28
+ order: route.navMenuItem.order,
28
29
  };
29
30
  addNavMenuItem(item, route.navMenuItem.sectionId);
30
31
  }