@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
@@ -1,15 +1,33 @@
1
1
  import { HistoryEntry, HistoryEntryItem } from '@/vdb/components/shared/history-timeline/history-entry.js';
2
+ import { HistoryNoteEditor } from '@/vdb/components/shared/history-timeline/history-note-editor.js';
2
3
  import { HistoryNoteInput } from '@/vdb/components/shared/history-timeline/history-note-input.js';
3
4
  import { HistoryTimeline } from '@/vdb/components/shared/history-timeline/history-timeline.js';
4
5
  import { Badge } from '@/vdb/components/ui/badge.js';
6
+ import { Button } from '@/vdb/components/ui/button.js';
5
7
  import { Trans } from '@/vdb/lib/trans.js';
6
- import { ArrowRightToLine, CheckIcon, CreditCardIcon, SquarePen } from 'lucide-react';
8
+ import {
9
+ ArrowRightToLine,
10
+ Ban,
11
+ CheckIcon,
12
+ ChevronDown,
13
+ ChevronUp,
14
+ CreditCardIcon,
15
+ Edit3,
16
+ SquarePen,
17
+ Truck,
18
+ UserX,
19
+ } from 'lucide-react';
20
+ import { useState } from 'react';
7
21
 
8
22
  interface OrderHistoryProps {
9
23
  order: {
10
24
  id: string;
11
25
  createdAt: string;
12
26
  currencyCode: string;
27
+ customer?: {
28
+ firstName: string;
29
+ lastName: string;
30
+ } | null;
13
31
  };
14
32
  historyEntries: Array<HistoryEntryItem>;
15
33
  onAddNote: (note: string, isPrivate: boolean) => void;
@@ -18,19 +36,142 @@ interface OrderHistoryProps {
18
36
  }
19
37
 
20
38
  export function OrderHistory({
39
+ order,
21
40
  historyEntries,
22
41
  onAddNote,
23
42
  onUpdateNote,
24
43
  onDeleteNote,
25
44
  }: Readonly<OrderHistoryProps>) {
45
+ const [expandedGroups, setExpandedGroups] = useState<Set<number>>(new Set());
46
+ const [noteEditorOpen, setNoteEditorOpen] = useState(false);
47
+ const [noteEditorNote, setNoteEditorNote] = useState<{
48
+ noteId: string;
49
+ note: string;
50
+ isPrivate: boolean;
51
+ }>({
52
+ noteId: '',
53
+ note: '',
54
+ isPrivate: true,
55
+ });
56
+
57
+ const handleEditNote = (noteId: string, note: string, isPrivate: boolean) => {
58
+ setNoteEditorNote({ noteId, note, isPrivate });
59
+ setNoteEditorOpen(true);
60
+ };
61
+
62
+ const handleDeleteNote = (noteId: string) => {
63
+ onDeleteNote?.(noteId);
64
+ };
65
+
66
+ const handleNoteEditorSave = (noteId: string, note: string, isPrivate: boolean) => {
67
+ onUpdateNote?.(noteId, note, isPrivate);
68
+ };
69
+
70
+ const isPrimaryEvent = (entry: HistoryEntryItem) => {
71
+ // Based on Angular component's isFeatured method
72
+ switch (entry.type) {
73
+ case 'ORDER_STATE_TRANSITION':
74
+ return (
75
+ entry.data.to === 'Delivered' ||
76
+ entry.data.to === 'Cancelled' ||
77
+ entry.data.to === 'Settled' ||
78
+ entry.data.from === 'Created'
79
+ );
80
+ case 'ORDER_REFUND_TRANSITION':
81
+ return entry.data.to === 'Settled';
82
+ case 'ORDER_PAYMENT_TRANSITION':
83
+ return entry.data.to === 'Settled' || entry.data.to === 'Cancelled';
84
+ case 'ORDER_FULFILLMENT_TRANSITION':
85
+ return entry.data.to === 'Delivered' || entry.data.to === 'Shipped';
86
+ case 'ORDER_NOTE':
87
+ case 'ORDER_MODIFIED':
88
+ case 'ORDER_CUSTOMER_UPDATED':
89
+ case 'ORDER_CANCELLATION':
90
+ return true;
91
+ default:
92
+ return false; // All other events are secondary
93
+ }
94
+ };
95
+
96
+ // Group consecutive secondary events
97
+ const groupedEntries: Array<
98
+ | { type: 'primary'; entry: HistoryEntryItem; index: number }
99
+ | {
100
+ type: 'secondary-group';
101
+ entries: Array<{ entry: HistoryEntryItem; index: number }>;
102
+ startIndex: number;
103
+ }
104
+ > = [];
105
+ let currentGroup: Array<{ entry: HistoryEntryItem; index: number }> = [];
106
+
107
+ for (let i = 0; i < historyEntries.length; i++) {
108
+ const entry = historyEntries[i];
109
+ const isSecondary = !isPrimaryEvent(entry);
110
+
111
+ if (isSecondary) {
112
+ currentGroup.push({ entry, index: i });
113
+ } else {
114
+ // If we have accumulated secondary events, add them as a group
115
+ if (currentGroup.length > 0) {
116
+ groupedEntries.push({
117
+ type: 'secondary-group',
118
+ entries: currentGroup,
119
+ startIndex: currentGroup[0].index,
120
+ });
121
+ currentGroup = [];
122
+ }
123
+ // Add the primary event
124
+ groupedEntries.push({ type: 'primary', entry, index: i });
125
+ }
126
+ }
127
+
128
+ // Don't forget the last group if it exists
129
+ if (currentGroup.length > 0) {
130
+ groupedEntries.push({
131
+ type: 'secondary-group',
132
+ entries: currentGroup,
133
+ startIndex: currentGroup[0].index,
134
+ });
135
+ }
136
+
137
+ const toggleGroup = (groupIndex: number) => {
138
+ const newExpanded = new Set(expandedGroups);
139
+ if (newExpanded.has(groupIndex)) {
140
+ newExpanded.delete(groupIndex);
141
+ } else {
142
+ newExpanded.add(groupIndex);
143
+ }
144
+ setExpandedGroups(newExpanded);
145
+ };
26
146
  const getTimelineIcon = (entry: OrderHistoryProps['historyEntries'][0]) => {
27
147
  switch (entry.type) {
28
148
  case 'ORDER_PAYMENT_TRANSITION':
29
149
  return <CreditCardIcon className="h-4 w-4" />;
150
+ case 'ORDER_REFUND_TRANSITION':
151
+ return <CreditCardIcon className="h-4 w-4" />;
30
152
  case 'ORDER_NOTE':
31
153
  return <SquarePen className="h-4 w-4" />;
32
154
  case 'ORDER_STATE_TRANSITION':
155
+ if (entry.data.to === 'Delivered') {
156
+ return <CheckIcon className="h-4 w-4" />;
157
+ }
158
+ if (entry.data.to === 'Cancelled') {
159
+ return <Ban className="h-4 w-4" />;
160
+ }
161
+ return <ArrowRightToLine className="h-4 w-4" />;
162
+ case 'ORDER_FULFILLMENT_TRANSITION':
163
+ if (entry.data.to === 'Shipped' || entry.data.to === 'Delivered') {
164
+ return <Truck className="h-4 w-4" />;
165
+ }
33
166
  return <ArrowRightToLine className="h-4 w-4" />;
167
+ case 'ORDER_FULFILLMENT':
168
+ return <Truck className="h-4 w-4" />;
169
+ case 'ORDER_MODIFIED':
170
+ return <Edit3 className="h-4 w-4" />;
171
+ case 'ORDER_CUSTOMER_UPDATED':
172
+ return <UserX className="h-4 w-4" />;
173
+ case 'ORDER_CANCELLATION':
174
+ return <Ban className="h-4 w-4" />;
34
175
  default:
35
176
  return <CheckIcon className="h-4 w-4" />;
36
177
  }
@@ -39,12 +180,26 @@ export function OrderHistory({
39
180
  const getTitle = (entry: OrderHistoryProps['historyEntries'][0]) => {
40
181
  switch (entry.type) {
41
182
  case 'ORDER_PAYMENT_TRANSITION':
42
- return <Trans>Payment settled</Trans>;
183
+ if (entry.data.to === 'Settled') {
184
+ return <Trans>Payment settled</Trans>;
185
+ }
186
+ if (entry.data.to === 'Authorized') {
187
+ return <Trans>Payment authorized</Trans>;
188
+ }
189
+ if (entry.data.to === 'Declined' || entry.data.to === 'Cancelled') {
190
+ return <Trans>Payment failed</Trans>;
191
+ }
192
+ return <Trans>Payment transitioned</Trans>;
193
+ case 'ORDER_REFUND_TRANSITION':
194
+ if (entry.data.to === 'Settled') {
195
+ return <Trans>Refund settled</Trans>;
196
+ }
197
+ return <Trans>Refund transitioned</Trans>;
43
198
  case 'ORDER_NOTE':
44
199
  return <Trans>Note added</Trans>;
45
200
  case 'ORDER_STATE_TRANSITION': {
46
201
  if (entry.data.from === 'Created') {
47
- return <Trans>Order created</Trans>;
202
+ return <Trans>Order placed</Trans>;
48
203
  }
49
204
  if (entry.data.to === 'Delivered') {
50
205
  return <Trans>Order fulfilled</Trans>;
@@ -52,8 +207,27 @@ export function OrderHistory({
52
207
  if (entry.data.to === 'Cancelled') {
53
208
  return <Trans>Order cancelled</Trans>;
54
209
  }
210
+ if (entry.data.to === 'Shipped') {
211
+ return <Trans>Order shipped</Trans>;
212
+ }
55
213
  return <Trans>Order transitioned</Trans>;
56
214
  }
215
+ case 'ORDER_FULFILLMENT_TRANSITION':
216
+ if (entry.data.to === 'Shipped') {
217
+ return <Trans>Order shipped</Trans>;
218
+ }
219
+ if (entry.data.to === 'Delivered') {
220
+ return <Trans>Order delivered</Trans>;
221
+ }
222
+ return <Trans>Fulfillment transitioned</Trans>;
223
+ case 'ORDER_FULFILLMENT':
224
+ return <Trans>Fulfillment created</Trans>;
225
+ case 'ORDER_MODIFIED':
226
+ return <Trans>Order modified</Trans>;
227
+ case 'ORDER_CUSTOMER_UPDATED':
228
+ return <Trans>Customer updated</Trans>;
229
+ case 'ORDER_CANCELLATION':
230
+ return <Trans>Order cancelled</Trans>;
57
231
  default:
58
232
  return <Trans>{entry.type.replace(/_/g, ' ').toLowerCase()}</Trans>;
59
233
  }
@@ -64,38 +238,211 @@ export function OrderHistory({
64
238
  <div className="mb-4">
65
239
  <HistoryNoteInput onAddNote={onAddNote} />
66
240
  </div>
67
- <HistoryTimeline onEditNote={onUpdateNote} onDeleteNote={onDeleteNote}>
68
- {historyEntries.map(entry => (
69
- <HistoryEntry
70
- key={entry.id}
71
- entry={entry}
72
- isNoteEntry={entry.type === 'ORDER_NOTE'}
73
- timelineIcon={getTimelineIcon(entry)}
74
- title={getTitle(entry)}
75
- >
76
- {entry.type === 'ORDER_NOTE' && (
77
- <div className="flex items-center space-x-2">
78
- <Badge variant={entry.isPublic ? 'outline' : 'secondary'} className="text-xs">
79
- {entry.isPublic ? 'Public' : 'Private'}
80
- </Badge>
81
- <span>{entry.data.note}</span>
241
+ <HistoryTimeline>
242
+ {groupedEntries.map((group, groupIndex) => {
243
+ if (group.type === 'primary') {
244
+ const entry = group.entry;
245
+ return (
246
+ <HistoryEntry
247
+ key={entry.id}
248
+ entry={entry}
249
+ isNoteEntry={entry.type === 'ORDER_NOTE'}
250
+ timelineIcon={getTimelineIcon(entry)}
251
+ title={getTitle(entry)}
252
+ isPrimary={true}
253
+ customer={order.customer}
254
+ onEditNote={handleEditNote}
255
+ onDeleteNote={handleDeleteNote}
256
+ >
257
+ {entry.type === 'ORDER_NOTE' && (
258
+ <div className="space-y-2">
259
+ <p className="text-sm text-foreground">{entry.data.note}</p>
260
+ <div className="flex items-center gap-2">
261
+ <Badge
262
+ variant={entry.isPublic ? 'outline' : 'secondary'}
263
+ className="text-xs"
264
+ >
265
+ {entry.isPublic ? 'Public' : 'Private'}
266
+ </Badge>
267
+ </div>
268
+ </div>
269
+ )}
270
+ {entry.type === 'ORDER_STATE_TRANSITION' && entry.data.from !== 'Created' && (
271
+ <p className="text-xs text-muted-foreground">
272
+ <Trans>
273
+ From {entry.data.from} to {entry.data.to}
274
+ </Trans>
275
+ </p>
276
+ )}
277
+ {entry.type === 'ORDER_PAYMENT_TRANSITION' && (
278
+ <p className="text-xs text-muted-foreground">
279
+ <Trans>
280
+ Payment #{entry.data.paymentId} transitioned to {entry.data.to}
281
+ </Trans>
282
+ </p>
283
+ )}
284
+ {entry.type === 'ORDER_REFUND_TRANSITION' && (
285
+ <p className="text-xs text-muted-foreground">
286
+ <Trans>
287
+ Refund #{entry.data.refundId} transitioned to {entry.data.to}
288
+ </Trans>
289
+ </p>
290
+ )}
291
+ {entry.type === 'ORDER_FULFILLMENT_TRANSITION' &&
292
+ entry.data.from !== 'Created' && (
293
+ <p className="text-xs text-muted-foreground">
294
+ <Trans>
295
+ Fulfillment #{entry.data.fulfillmentId} from {entry.data.from}{' '}
296
+ to {entry.data.to}
297
+ </Trans>
298
+ </p>
299
+ )}
300
+ {entry.type === 'ORDER_FULFILLMENT' && (
301
+ <p className="text-xs text-muted-foreground">
302
+ <Trans>Fulfillment #{entry.data.fulfillmentId} created</Trans>
303
+ </p>
304
+ )}
305
+ {entry.type === 'ORDER_MODIFIED' && (
306
+ <p className="text-xs text-muted-foreground">
307
+ <Trans>Order modification #{entry.data.modificationId}</Trans>
308
+ </p>
309
+ )}
310
+ {entry.type === 'ORDER_CUSTOMER_UPDATED' && (
311
+ <p className="text-xs text-muted-foreground">
312
+ <Trans>Customer information updated</Trans>
313
+ </p>
314
+ )}
315
+ {entry.type === 'ORDER_CANCELLATION' && (
316
+ <p className="text-xs text-muted-foreground">
317
+ <Trans>Order cancelled</Trans>
318
+ </p>
319
+ )}
320
+ </HistoryEntry>
321
+ );
322
+ } else {
323
+ // Secondary group
324
+ const shouldCollapse = group.entries.length > 2;
325
+ const isExpanded = expandedGroups.has(groupIndex);
326
+ const visibleEntries =
327
+ shouldCollapse && !isExpanded ? group.entries.slice(0, 2) : group.entries;
328
+
329
+ return (
330
+ <div key={`group-${groupIndex}`}>
331
+ {visibleEntries.map(({ entry }) => (
332
+ <HistoryEntry
333
+ key={entry.id}
334
+ entry={entry}
335
+ isNoteEntry={entry.type === 'ORDER_NOTE'}
336
+ timelineIcon={getTimelineIcon(entry)}
337
+ title={getTitle(entry)}
338
+ isPrimary={false}
339
+ customer={order.customer}
340
+ onEditNote={handleEditNote}
341
+ onDeleteNote={handleDeleteNote}
342
+ >
343
+ {entry.type === 'ORDER_NOTE' && (
344
+ <div className="space-y-1">
345
+ <p className="text-xs text-foreground">{entry.data.note}</p>
346
+ <Badge
347
+ variant={entry.isPublic ? 'outline' : 'secondary'}
348
+ className="text-xs"
349
+ >
350
+ {entry.isPublic ? 'Public' : 'Private'}
351
+ </Badge>
352
+ </div>
353
+ )}
354
+ {entry.type === 'ORDER_STATE_TRANSITION' &&
355
+ entry.data.from !== 'Created' && (
356
+ <p className="text-xs text-muted-foreground">
357
+ <Trans>
358
+ From {entry.data.from} to {entry.data.to}
359
+ </Trans>
360
+ </p>
361
+ )}
362
+ {entry.type === 'ORDER_PAYMENT_TRANSITION' && (
363
+ <p className="text-xs text-muted-foreground">
364
+ <Trans>
365
+ Payment #{entry.data.paymentId} transitioned to{' '}
366
+ {entry.data.to}
367
+ </Trans>
368
+ </p>
369
+ )}
370
+ {entry.type === 'ORDER_REFUND_TRANSITION' && (
371
+ <p className="text-xs text-muted-foreground">
372
+ <Trans>
373
+ Refund #{entry.data.refundId} transitioned to{' '}
374
+ {entry.data.to}
375
+ </Trans>
376
+ </p>
377
+ )}
378
+ {entry.type === 'ORDER_FULFILLMENT_TRANSITION' &&
379
+ entry.data.from !== 'Created' && (
380
+ <p className="text-xs text-muted-foreground">
381
+ <Trans>
382
+ Fulfillment #{entry.data.fulfillmentId} from{' '}
383
+ {entry.data.from} to {entry.data.to}
384
+ </Trans>
385
+ </p>
386
+ )}
387
+ {entry.type === 'ORDER_FULFILLMENT' && (
388
+ <p className="text-xs text-muted-foreground">
389
+ <Trans>Fulfillment #{entry.data.fulfillmentId} created</Trans>
390
+ </p>
391
+ )}
392
+ {entry.type === 'ORDER_MODIFIED' && (
393
+ <p className="text-xs text-muted-foreground">
394
+ <Trans>Order modification #{entry.data.modificationId}</Trans>
395
+ </p>
396
+ )}
397
+ {entry.type === 'ORDER_CUSTOMER_UPDATED' && (
398
+ <p className="text-xs text-muted-foreground">
399
+ <Trans>Customer information updated</Trans>
400
+ </p>
401
+ )}
402
+ {entry.type === 'ORDER_CANCELLATION' && (
403
+ <p className="text-xs text-muted-foreground">
404
+ <Trans>Order cancelled</Trans>
405
+ </p>
406
+ )}
407
+ </HistoryEntry>
408
+ ))}
409
+
410
+ {shouldCollapse && (
411
+ <div className="flex justify-center py-2">
412
+ <Button
413
+ variant="ghost"
414
+ size="sm"
415
+ onClick={() => toggleGroup(groupIndex)}
416
+ className="text-muted-foreground hover:text-foreground h-6 text-xs"
417
+ >
418
+ {isExpanded ? (
419
+ <>
420
+ <ChevronUp className="w-3 h-3 mr-1" />
421
+ <Trans>Show less</Trans>
422
+ </>
423
+ ) : (
424
+ <>
425
+ <ChevronDown className="w-3 h-3 mr-1" />
426
+ <Trans>Show all ({group.entries.length})</Trans>
427
+ </>
428
+ )}
429
+ </Button>
430
+ </div>
431
+ )}
82
432
  </div>
83
- )}
84
- <div className="text-sm text-muted-foreground">
85
- {entry.type === 'ORDER_STATE_TRANSITION' && entry.data.from !== 'Created' && (
86
- <Trans>
87
- From {entry.data.from} to {entry.data.to}
88
- </Trans>
89
- )}
90
- {entry.type === 'ORDER_PAYMENT_TRANSITION' && (
91
- <Trans>
92
- Payment #{entry.data.paymentId} transitioned to {entry.data.to}
93
- </Trans>
94
- )}
95
- </div>
96
- </HistoryEntry>
97
- ))}
433
+ );
434
+ }
435
+ })}
98
436
  </HistoryTimeline>
437
+ <HistoryNoteEditor
438
+ key={noteEditorNote.noteId}
439
+ note={noteEditorNote.note}
440
+ onNoteChange={handleNoteEditorSave}
441
+ open={noteEditorOpen}
442
+ onOpenChange={setNoteEditorOpen}
443
+ noteId={noteEditorNote.noteId}
444
+ isPrivate={noteEditorNote.isPrivate}
445
+ />
99
446
  </div>
100
447
  );
101
448
  }
@@ -47,7 +47,7 @@ export function OptionValueInput({
47
47
 
48
48
  const handleAddValue = () => {
49
49
  if (newValue.trim() && !fields.some(f => f.value === newValue.trim())) {
50
- append({ value: newValue.trim(), id: crypto.randomUUID() });
50
+ append({ value: newValue.trim(), id: Date.now().toString() });
51
51
  setNewValue('');
52
52
  }
53
53
  };
@@ -28,7 +28,7 @@ function HealthchecksPage() {
28
28
  queryKey: ['healthchecks'],
29
29
  queryFn: async () => {
30
30
  const schemeAndHost =
31
- uiConfig.apiHost + (uiConfig.apiPort !== 'auto' ? `:${uiConfig.apiPort}` : '');
31
+ uiConfig.api.host + (uiConfig.api.port !== 'auto' ? `:${uiConfig.api.port}` : '');
32
32
 
33
33
  const res = await fetch(`${schemeAndHost}/health`);
34
34
  return res.json() as Promise<HealthcheckResponse>;
@@ -94,6 +94,7 @@ function JobQueuePage() {
94
94
  <ListPage
95
95
  pageId="job-queue-list"
96
96
  title="Job Queue"
97
+ defaultSort={[{ id: 'createdAt', desc: true }]}
97
98
  listQuery={jobListDocument}
98
99
  route={Route}
99
100
  customizeColumns={{
@@ -168,8 +168,8 @@ function DashboardPage() {
168
168
  }, [layoutWidth, editMode, widgets]);
169
169
 
170
170
  return (
171
- <Page pageId="dashboard">
172
- <PageTitle>Dashboard</PageTitle>
171
+ <Page pageId="insights">
172
+ <PageTitle>Insights</PageTitle>
173
173
  <PageActionBar>
174
174
  <PageActionBarRight>
175
175
  <Button variant="outline" onClick={() => setEditMode(prev => !prev)}>
@@ -15,7 +15,7 @@ export const Route = createFileRoute(AUTHENTICATED_ROUTE_PREFIX)({
15
15
  }
16
16
  },
17
17
  loader: () => ({
18
- breadcrumb: 'Dashboard',
18
+ breadcrumb: 'Insights',
19
19
  }),
20
20
  component: AuthLayout,
21
21
  });
@@ -1,9 +1,8 @@
1
- import { BubbleMenu, Editor, EditorContent, useCurrentEditor, useEditor } from '@tiptap/react';
2
- import StarterKit from '@tiptap/starter-kit';
3
- import ListItem from '@tiptap/extension-list-item';
4
1
  import TextStyle from '@tiptap/extension-text-style';
2
+ import { BubbleMenu, Editor, EditorContent, useEditor } from '@tiptap/react';
3
+ import StarterKit from '@tiptap/starter-kit';
5
4
  import { BoldIcon, ItalicIcon, StrikethroughIcon } from 'lucide-react';
6
- import { useEffect, useLayoutEffect } from 'react';
5
+ import { useLayoutEffect, useRef } from 'react';
7
6
  import { Button } from '../ui/button.js';
8
7
 
9
8
  // define your extension array
@@ -27,6 +26,8 @@ export interface RichTextInputProps {
27
26
  }
28
27
 
29
28
  export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>) {
29
+ const isInternalUpdate = useRef(false);
30
+
30
31
  const editor = useEditor({
31
32
  parseOptions: {
32
33
  preserveWhitespace: 'full',
@@ -34,6 +35,7 @@ export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>)
34
35
  extensions: extensions,
35
36
  content: value,
36
37
  onUpdate: ({ editor }) => {
38
+ isInternalUpdate.current = true;
37
39
  onChange(editor.getHTML());
38
40
  },
39
41
  editorProps: {
@@ -44,11 +46,15 @@ export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>)
44
46
  });
45
47
 
46
48
  useLayoutEffect(() => {
47
- if (editor) {
48
- const { from, to } = editor.state.selection;
49
- editor.commands.setContent(value, false);
50
- editor.commands.setTextSelection({ from, to });
49
+ if (editor && !isInternalUpdate.current) {
50
+ const currentContent = editor.getHTML();
51
+ if (currentContent !== value) {
52
+ const { from, to } = editor.state.selection;
53
+ editor.commands.setContent(value, false);
54
+ editor.commands.setTextSelection({ from, to });
55
+ }
51
56
  }
57
+ isInternalUpdate.current = false;
52
58
  }, [value, editor]);
53
59
 
54
60
  if (!editor) {
@@ -9,6 +9,7 @@ import {
9
9
  } from '@/vdb/components/ui/dropdown-menu.js';
10
10
  import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
11
11
  import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
12
+ import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
12
13
  import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
13
14
  import { usePage } from '@/vdb/hooks/use-page.js';
14
15
  import { Trans } from '@/vdb/lib/trans.js';
@@ -26,7 +27,8 @@ export function DataTableBulkActions<TData>({
26
27
  bulkActions,
27
28
  }: Readonly<DataTableBulkActionsProps<TData>>) {
28
29
  const { pageId } = usePage();
29
- const { blockId } = usePageBlock();
30
+ const pageBlock = usePageBlock();
31
+ const blockId = pageBlock?.blockId;
30
32
 
31
33
  // Cache to store selected items across page changes
32
34
  const selectedItemsCache = useRef<Map<string, TData>>(new Map());
@@ -52,7 +54,13 @@ export function DataTableBulkActions<TData>({
52
54
  })
53
55
  .filter((item): item is TData => item !== undefined);
54
56
 
55
- if (selection.length === 0) {
57
+ const { position, shouldShow } = useFloatingBulkActions({
58
+ selectionCount: selection.length,
59
+ containerSelector: '[data-table-root], .data-table-container, table',
60
+ bottomOffset: 40,
61
+ });
62
+
63
+ if (!shouldShow) {
56
64
  return null;
57
65
  }
58
66
  const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
@@ -61,8 +69,13 @@ export function DataTableBulkActions<TData>({
61
69
 
62
70
  return (
63
71
  <div
64
- className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 absolute bottom-10 left-1/2 transform -translate-x-1/2 bg-white shadow-2xl rounded-md border"
65
- style={{ height: 'auto', maxHeight: '60px' }}
72
+ 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"
73
+ style={{
74
+ height: 'auto',
75
+ maxHeight: '60px',
76
+ bottom: position.bottom,
77
+ left: position.left
78
+ }}
66
79
  >
67
80
  <span className="text-sm text-muted-foreground">
68
81
  <Trans>{selection.length} selected</Trans>
@@ -1,11 +1,10 @@
1
1
  import { AppSidebar } from '@/vdb/components/layout/app-sidebar.js';
2
+ import { DevModeIndicator } from '@/vdb/components/layout/dev-mode-indicator.js';
2
3
  import { GeneratedBreadcrumbs } from '@/vdb/components/layout/generated-breadcrumbs.js';
3
4
  import { PrereleasePopup } from '@/vdb/components/layout/prerelease-popup.js';
4
- import { Badge } from '@/vdb/components/ui/badge.js';
5
5
  import { Separator } from '@/vdb/components/ui/separator.js';
6
6
  import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/vdb/components/ui/sidebar.js';
7
7
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
8
- import { Trans } from '@/vdb/lib/trans.js';
9
8
  import { Outlet } from '@tanstack/react-router';
10
9
  import { Alerts } from '../shared/alerts.js';
11
10
 
@@ -24,11 +23,7 @@ export function AppLayout() {
24
23
  <GeneratedBreadcrumbs />
25
24
  </div>
26
25
  <div className="flex items-center justify-end gap-2">
27
- {settings.devMode && (
28
- <Badge variant="destructive">
29
- <Trans>Dev Mode</Trans>
30
- </Badge>
31
- )}
26
+ {settings.devMode && <DevModeIndicator />}
32
27
  <Alerts />
33
28
  </div>
34
29
  </div>