@vendure/dashboard 3.4.3-master-202509230228 → 3.4.3-master-202509250229
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +11 -12
- package/package.json +4 -4
- package/src/app/common/use-page-title.test.ts +263 -0
- package/src/app/common/use-page-title.ts +86 -0
- package/src/app/routes/__root.tsx +4 -4
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx +2 -2
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-types.ts +5 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-utils.tsx +124 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history.tsx +91 -59
- package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +176 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +4 -2
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +2 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +98 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-container.tsx +9 -7
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-types.ts +5 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +173 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +64 -408
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +4 -0
- package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +9 -4
- package/src/app/routes/_authenticated/_shipping-methods/components/metadata-badges.tsx +15 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/price-display.tsx +21 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-test-result-wrapper.tsx +87 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-address-form.tsx +255 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-order-builder.tsx +243 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-result.tsx +97 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-sheet.tsx +41 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods.tsx +74 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-single-method-result.tsx +90 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method-sheet.tsx +56 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method.tsx +82 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/use-shipping-method-test-state.ts +67 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +27 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +24 -4
- package/src/lib/components/shared/history-timeline/history-note-entry.tsx +65 -0
- package/src/lib/components/shared/history-timeline/history-timeline-with-grouping.tsx +141 -0
- package/src/lib/components/shared/history-timeline/use-history-note-editor.ts +26 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +5 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +7 -0
- package/src/lib/framework/extension-api/logic/history-entries.ts +24 -0
- package/src/lib/framework/extension-api/logic/index.ts +1 -0
- package/src/lib/framework/extension-api/types/history-entries.ts +120 -0
- package/src/lib/framework/extension-api/types/index.ts +1 -0
- package/src/lib/framework/history-entry/history-entry-extensions.ts +11 -0
- package/src/lib/framework/history-entry/history-entry.tsx +129 -0
- package/src/lib/framework/registry/registry-types.ts +2 -0
- package/src/lib/index.ts +5 -1
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-method-dialog.tsx +0 -32
- package/src/lib/components/shared/history-timeline/history-entry.tsx +0 -188
package/index.html
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</body>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta name="description" content="Vendure Admin Dashboard" />
|
|
8
|
+
<meta name="author" content="Vendure" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"></div>
|
|
12
|
+
<script type="module" src="/src/app/main.jsx"></script>
|
|
13
|
+
</body>
|
|
15
14
|
</html>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.4.3-master-
|
|
4
|
+
"version": "3.4.3-master-202509250229",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"@types/react": "^19.0.10",
|
|
105
105
|
"@types/react-dom": "^19.0.4",
|
|
106
106
|
"@uidotdev/usehooks": "^2.4.1",
|
|
107
|
-
"@vendure/common": "^3.4.3-master-
|
|
108
|
-
"@vendure/core": "^3.4.3-master-
|
|
107
|
+
"@vendure/common": "^3.4.3-master-202509250229",
|
|
108
|
+
"@vendure/core": "^3.4.3-master-202509250229",
|
|
109
109
|
"@vitejs/plugin-react": "^4.3.4",
|
|
110
110
|
"acorn": "^8.11.3",
|
|
111
111
|
"acorn-walk": "^8.3.2",
|
|
@@ -156,5 +156,5 @@
|
|
|
156
156
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
157
157
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
158
158
|
},
|
|
159
|
-
"gitHead": "
|
|
159
|
+
"gitHead": "7555f68cec1c4a90ca20bbc2b79798b027dc7c4f"
|
|
160
160
|
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { normalizeBreadcrumb } from './use-page-title.js';
|
|
5
|
+
|
|
6
|
+
describe('normalizeBreadcrumb', () => {
|
|
7
|
+
describe('null and undefined handling', () => {
|
|
8
|
+
it('should return empty string for null', () => {
|
|
9
|
+
expect(normalizeBreadcrumb(null)).toBe('');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should return empty string for undefined', () => {
|
|
13
|
+
expect(normalizeBreadcrumb(undefined)).toBe('');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return empty string for empty array', () => {
|
|
17
|
+
expect(normalizeBreadcrumb([])).toBe('');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('string handling', () => {
|
|
22
|
+
it('should return string as-is', () => {
|
|
23
|
+
expect(normalizeBreadcrumb('Home')).toBe('Home');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return empty string as-is', () => {
|
|
27
|
+
expect(normalizeBreadcrumb('')).toBe('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle strings with special characters', () => {
|
|
31
|
+
expect(normalizeBreadcrumb('Settings & Config')).toBe('Settings & Config');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('number handling', () => {
|
|
36
|
+
it('should convert number to string', () => {
|
|
37
|
+
expect(normalizeBreadcrumb(42)).toBe('42');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle zero', () => {
|
|
41
|
+
expect(normalizeBreadcrumb(0)).toBe('0');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle negative numbers', () => {
|
|
45
|
+
expect(normalizeBreadcrumb(-123)).toBe('-123');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle decimal numbers', () => {
|
|
49
|
+
expect(normalizeBreadcrumb(3.14)).toBe('3.14');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('function handling', () => {
|
|
54
|
+
it('should call function and normalize the result', () => {
|
|
55
|
+
const fn = () => 'Dashboard';
|
|
56
|
+
expect(normalizeBreadcrumb(fn)).toBe('Dashboard');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should handle nested function returns', () => {
|
|
60
|
+
const fn = () => () => 'Nested Function';
|
|
61
|
+
expect(normalizeBreadcrumb(fn)).toBe('Nested Function');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle function returning React element', () => {
|
|
65
|
+
const mockReactElement = React.createElement('div', {}, 'React Content');
|
|
66
|
+
const fn = () => mockReactElement;
|
|
67
|
+
expect(normalizeBreadcrumb(fn)).toBe('React Content');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle function returning array', () => {
|
|
71
|
+
const fn = () => ['First', 'Second', 'Third'];
|
|
72
|
+
expect(normalizeBreadcrumb(fn)).toBe('Third');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle function returning object with label', () => {
|
|
76
|
+
const fn = () => ({ label: 'Settings', path: '/settings' });
|
|
77
|
+
expect(normalizeBreadcrumb(fn)).toBe('Settings');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('array handling', () => {
|
|
82
|
+
it('should return last element of string array', () => {
|
|
83
|
+
expect(normalizeBreadcrumb(['Home', 'Products', 'Details'])).toBe('Details');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle single element array', () => {
|
|
87
|
+
expect(normalizeBreadcrumb(['Single'])).toBe('Single');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle array with React element at the end', () => {
|
|
91
|
+
const mockReactElement = React.createElement('span', {}, 'Last Item');
|
|
92
|
+
expect(normalizeBreadcrumb(['First', mockReactElement])).toBe('Last Item');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle array with object containing label at the end', () => {
|
|
96
|
+
const breadcrumbs = [
|
|
97
|
+
'Home',
|
|
98
|
+
{ label: 'Products', path: '/products' },
|
|
99
|
+
{ label: 'Details', path: '/products/123' },
|
|
100
|
+
];
|
|
101
|
+
expect(normalizeBreadcrumb(breadcrumbs)).toBe('Details');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle array with function at the end', () => {
|
|
105
|
+
const breadcrumbs = ['Home', () => 'Dynamic Content'];
|
|
106
|
+
expect(normalizeBreadcrumb(breadcrumbs)).toBe('Dynamic Content');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle nested arrays', () => {
|
|
110
|
+
const breadcrumbs = ['First', ['Nested1', 'Nested2']];
|
|
111
|
+
expect(normalizeBreadcrumb(breadcrumbs)).toBe('Nested2');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('object with label property handling', () => {
|
|
116
|
+
it('should extract string label from object', () => {
|
|
117
|
+
const breadcrumb = { label: 'Settings', path: '/settings' };
|
|
118
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Settings');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle React element as label', () => {
|
|
122
|
+
const mockReactElement = React.createElement('span', {}, 'Global Settings');
|
|
123
|
+
const breadcrumb = { label: mockReactElement, path: '/global-settings' };
|
|
124
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Global Settings');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle function as label', () => {
|
|
128
|
+
const breadcrumb = {
|
|
129
|
+
label: () => 'Dynamic Label',
|
|
130
|
+
path: '/dynamic',
|
|
131
|
+
};
|
|
132
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Dynamic Label');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle nested object with label', () => {
|
|
136
|
+
const breadcrumb = {
|
|
137
|
+
label: {
|
|
138
|
+
label: 'Nested Label',
|
|
139
|
+
},
|
|
140
|
+
path: '/nested',
|
|
141
|
+
};
|
|
142
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Nested Label');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle object with label containing array', () => {
|
|
146
|
+
const breadcrumb = {
|
|
147
|
+
label: ['First', 'Second', 'Third'],
|
|
148
|
+
path: '/array-label',
|
|
149
|
+
};
|
|
150
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Third');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('React element handling', () => {
|
|
155
|
+
it('should extract text from simple React element', () => {
|
|
156
|
+
const element = React.createElement('div', {}, 'Simple Text');
|
|
157
|
+
expect(normalizeBreadcrumb(element)).toBe('Simple Text');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle nested React elements', () => {
|
|
161
|
+
const innerElement = React.createElement('span', {}, 'Nested Text');
|
|
162
|
+
const element = React.createElement('div', {}, innerElement);
|
|
163
|
+
expect(normalizeBreadcrumb(element)).toBe('Nested Text');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle React element with array of children', () => {
|
|
167
|
+
const element = React.createElement('div', {}, ['Part 1', ' ', 'Part 2']);
|
|
168
|
+
expect(normalizeBreadcrumb(element)).toBe('Part 1 Part 2');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle React element with mixed children types', () => {
|
|
172
|
+
const nestedElement = React.createElement('span', {}, ' nested');
|
|
173
|
+
const element = React.createElement('div', {}, ['Text', 42, nestedElement]);
|
|
174
|
+
expect(normalizeBreadcrumb(element)).toBe('Text42 nested');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('complex nested scenarios', () => {
|
|
179
|
+
it('should handle array with object containing function returning React element', () => {
|
|
180
|
+
const mockReactElement = React.createElement('div', {}, 'Complex Content');
|
|
181
|
+
const breadcrumb = [
|
|
182
|
+
'Home',
|
|
183
|
+
{
|
|
184
|
+
label: () => mockReactElement,
|
|
185
|
+
path: '/complex',
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Complex Content');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle function returning array with object containing React element', () => {
|
|
192
|
+
const mockReactElement = React.createElement('div', {}, 'Deep Nested');
|
|
193
|
+
const fn = () => ['Start', { label: mockReactElement, path: '/deep' }];
|
|
194
|
+
expect(normalizeBreadcrumb(fn)).toBe('Deep Nested');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle array with multiple levels of nesting', () => {
|
|
198
|
+
const breadcrumb = [
|
|
199
|
+
'Level1',
|
|
200
|
+
[
|
|
201
|
+
'Level2-1',
|
|
202
|
+
{
|
|
203
|
+
label: () => ['Level3-1', 'Level3-2'],
|
|
204
|
+
path: '/multi-level',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
];
|
|
208
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Level3-2');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should handle Trans-like component structure', () => {
|
|
212
|
+
// Simulating a <Trans>Global Settings</Trans> component
|
|
213
|
+
const transElement = React.createElement(
|
|
214
|
+
'Trans',
|
|
215
|
+
{
|
|
216
|
+
i18nKey: 'global.settings',
|
|
217
|
+
},
|
|
218
|
+
'Global Settings',
|
|
219
|
+
);
|
|
220
|
+
const breadcrumb = [
|
|
221
|
+
{
|
|
222
|
+
path: '/global-settings',
|
|
223
|
+
label: transElement,
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
expect(normalizeBreadcrumb(breadcrumb)).toBe('Global Settings');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('edge cases', () => {
|
|
231
|
+
it('should handle boolean values', () => {
|
|
232
|
+
expect(normalizeBreadcrumb(true)).toBe('');
|
|
233
|
+
expect(normalizeBreadcrumb(false)).toBe('');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle objects without label property', () => {
|
|
237
|
+
const obj = { path: '/test', name: 'Test' };
|
|
238
|
+
expect(normalizeBreadcrumb(obj)).toBe('');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should handle symbols', () => {
|
|
242
|
+
const sym = Symbol('test');
|
|
243
|
+
expect(normalizeBreadcrumb(sym)).toBe('');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle circular references gracefully', () => {
|
|
247
|
+
const circular: any = { label: null };
|
|
248
|
+
circular.label = circular;
|
|
249
|
+
// This should not cause infinite recursion
|
|
250
|
+
// The function should detect the circular reference and return empty string
|
|
251
|
+
expect(() => normalizeBreadcrumb(circular)).not.toThrow();
|
|
252
|
+
expect(normalizeBreadcrumb(circular)).toBe('');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle very deeply nested structures', () => {
|
|
256
|
+
let deep: any = 'Final Value';
|
|
257
|
+
for (let i = 0; i < 100; i++) {
|
|
258
|
+
deep = { label: deep };
|
|
259
|
+
}
|
|
260
|
+
expect(normalizeBreadcrumb(deep)).toBe('Final Value');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useMatches } from '@tanstack/react-router';
|
|
2
|
+
import React, { isValidElement, ReactElement, useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TITLE = 'Vendure';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @description
|
|
8
|
+
* Derives the meta title of the page based on the current route's breadcrumb
|
|
9
|
+
* data from the route loader.
|
|
10
|
+
*/
|
|
11
|
+
export function usePageTitle() {
|
|
12
|
+
const matches = useMatches();
|
|
13
|
+
const [pageTitle, setPageTitle] = useState<string>(DEFAULT_TITLE);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const lastMatch = matches.at(-1);
|
|
17
|
+
const breadcrumb = (lastMatch?.loaderData as any)?.breadcrumb;
|
|
18
|
+
|
|
19
|
+
const breadcrumbTitle = normalizeBreadcrumb(breadcrumb);
|
|
20
|
+
setPageTitle([breadcrumbTitle, DEFAULT_TITLE].filter(x => !!x).join(' • '));
|
|
21
|
+
}, [matches]);
|
|
22
|
+
|
|
23
|
+
return pageTitle;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const renderNodeAsString = function (reactNode: React.ReactNode): string {
|
|
27
|
+
let string = '';
|
|
28
|
+
if (typeof reactNode === 'string') {
|
|
29
|
+
string = reactNode;
|
|
30
|
+
} else if (typeof reactNode === 'number') {
|
|
31
|
+
string = reactNode.toString();
|
|
32
|
+
} else if (Array.isArray(reactNode)) {
|
|
33
|
+
reactNode.forEach(function (child) {
|
|
34
|
+
string += renderNodeAsString(child);
|
|
35
|
+
});
|
|
36
|
+
} else if (isValidElement(reactNode)) {
|
|
37
|
+
string += renderNodeAsString((reactNode as ReactElement<any>).props.children);
|
|
38
|
+
}
|
|
39
|
+
return string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Recursively normalizes a breadcrumb value to a string.
|
|
44
|
+
* Handles functions, arrays, objects with labels, and React nodes.
|
|
45
|
+
*/
|
|
46
|
+
export const normalizeBreadcrumb = (value: any, visited = new WeakSet()): string => {
|
|
47
|
+
// Handle null/undefined
|
|
48
|
+
if (value == null) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If it's a function, call it and normalize the result
|
|
53
|
+
if (typeof value === 'function') {
|
|
54
|
+
return normalizeBreadcrumb(value(), visited);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If it's already a string, return it
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If it's an array, normalize the last element
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
if (value.length === 0) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
return normalizeBreadcrumb(value.at(-1), visited);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// For objects, check for circular references
|
|
71
|
+
if (typeof value === 'object') {
|
|
72
|
+
// Prevent circular reference infinite loops
|
|
73
|
+
if (visited.has(value)) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
visited.add(value);
|
|
77
|
+
|
|
78
|
+
// If it's an object with a label property, normalize the label
|
|
79
|
+
if ('label' in value) {
|
|
80
|
+
return normalizeBreadcrumb(value.label, visited);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// For everything else (React nodes, numbers, etc.), use renderNodeAsString
|
|
85
|
+
return renderNodeAsString(value);
|
|
86
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AuthContext } from '@/vdb/providers/auth.js';
|
|
2
2
|
import { QueryClient } from '@tanstack/react-query';
|
|
3
|
-
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
|
3
|
+
import { createRootRouteWithContext, HeadContent, Outlet } from '@tanstack/react-router';
|
|
4
|
+
import { usePageTitle } from '../common/use-page-title.js';
|
|
4
5
|
|
|
5
6
|
export interface MyRouterContext {
|
|
6
7
|
auth: AuthContext;
|
|
@@ -9,14 +10,13 @@ export interface MyRouterContext {
|
|
|
9
10
|
|
|
10
11
|
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|
11
12
|
component: RootComponent,
|
|
12
|
-
search: {
|
|
13
|
-
// middlewares: [retainSearchParams(['page', 'perPage', 'sort'] as any)],
|
|
14
|
-
},
|
|
15
13
|
});
|
|
16
14
|
|
|
17
15
|
function RootComponent() {
|
|
16
|
+
document.title = usePageTitle();
|
|
18
17
|
return (
|
|
19
18
|
<>
|
|
19
|
+
<HeadContent />
|
|
20
20
|
<Outlet />
|
|
21
21
|
</>
|
|
22
22
|
);
|
|
@@ -63,13 +63,13 @@ export function CustomerHistoryContainer({ customerId }: Readonly<CustomerHistor
|
|
|
63
63
|
<>
|
|
64
64
|
<CustomerHistory
|
|
65
65
|
customer={customer}
|
|
66
|
-
historyEntries={historyEntries
|
|
66
|
+
historyEntries={historyEntries}
|
|
67
67
|
onAddNote={addNote}
|
|
68
68
|
onUpdateNote={updateNote}
|
|
69
69
|
onDeleteNote={deleteNote}
|
|
70
70
|
/>
|
|
71
71
|
{hasNextPage && (
|
|
72
|
-
<Button type="button" variant="outline" onClick={() => fetchNextPage()}>
|
|
72
|
+
<Button type="button" variant="outline" onClick={() => fetchNextPage?.()}>
|
|
73
73
|
<Trans>Load more</Trans>
|
|
74
74
|
</Button>
|
|
75
75
|
)}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { HistoryEntryItem } from '@/vdb/framework/extension-api/types/index.js';
|
|
2
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
3
|
+
import { CheckIcon, Edit3, KeyIcon, Mail, MapPin, SquarePen, User, UserCheck, Users } from 'lucide-react';
|
|
4
|
+
import { CustomerHistoryCustomerDetail } from './customer-history-types.js';
|
|
5
|
+
|
|
6
|
+
export function customerHistoryUtils(customer: CustomerHistoryCustomerDetail) {
|
|
7
|
+
const getTimelineIcon = (entry: HistoryEntryItem) => {
|
|
8
|
+
switch (entry.type) {
|
|
9
|
+
case 'CUSTOMER_REGISTERED':
|
|
10
|
+
return <User className="h-4 w-4" />;
|
|
11
|
+
case 'CUSTOMER_VERIFIED':
|
|
12
|
+
return <UserCheck className="h-4 w-4" />;
|
|
13
|
+
case 'CUSTOMER_NOTE':
|
|
14
|
+
return <SquarePen className="h-4 w-4" />;
|
|
15
|
+
case 'CUSTOMER_ADDED_TO_GROUP':
|
|
16
|
+
case 'CUSTOMER_REMOVED_FROM_GROUP':
|
|
17
|
+
return <Users className="h-4 w-4" />;
|
|
18
|
+
case 'CUSTOMER_DETAIL_UPDATED':
|
|
19
|
+
return <Edit3 className="h-4 w-4" />;
|
|
20
|
+
case 'CUSTOMER_ADDRESS_CREATED':
|
|
21
|
+
case 'CUSTOMER_ADDRESS_UPDATED':
|
|
22
|
+
case 'CUSTOMER_ADDRESS_DELETED':
|
|
23
|
+
return <MapPin className="h-4 w-4" />;
|
|
24
|
+
case 'CUSTOMER_PASSWORD_UPDATED':
|
|
25
|
+
case 'CUSTOMER_PASSWORD_RESET_REQUESTED':
|
|
26
|
+
case 'CUSTOMER_PASSWORD_RESET_VERIFIED':
|
|
27
|
+
return <KeyIcon className="h-4 w-4" />;
|
|
28
|
+
case 'CUSTOMER_EMAIL_UPDATE_REQUESTED':
|
|
29
|
+
case 'CUSTOMER_EMAIL_UPDATE_VERIFIED':
|
|
30
|
+
return <Mail className="h-4 w-4" />;
|
|
31
|
+
default:
|
|
32
|
+
return <CheckIcon className="h-4 w-4" />;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getTitle = (entry: HistoryEntryItem) => {
|
|
37
|
+
switch (entry.type) {
|
|
38
|
+
case 'CUSTOMER_REGISTERED':
|
|
39
|
+
return <Trans>Customer registered</Trans>;
|
|
40
|
+
case 'CUSTOMER_VERIFIED':
|
|
41
|
+
return <Trans>Customer verified</Trans>;
|
|
42
|
+
case 'CUSTOMER_NOTE':
|
|
43
|
+
return <Trans>Note added</Trans>;
|
|
44
|
+
case 'CUSTOMER_DETAIL_UPDATED':
|
|
45
|
+
return <Trans>Customer details updated</Trans>;
|
|
46
|
+
case 'CUSTOMER_ADDED_TO_GROUP':
|
|
47
|
+
return <Trans>Added to group</Trans>;
|
|
48
|
+
case 'CUSTOMER_REMOVED_FROM_GROUP':
|
|
49
|
+
return <Trans>Removed from group</Trans>;
|
|
50
|
+
case 'CUSTOMER_ADDRESS_CREATED':
|
|
51
|
+
return <Trans>Address created</Trans>;
|
|
52
|
+
case 'CUSTOMER_ADDRESS_UPDATED':
|
|
53
|
+
return <Trans>Address updated</Trans>;
|
|
54
|
+
case 'CUSTOMER_ADDRESS_DELETED':
|
|
55
|
+
return <Trans>Address deleted</Trans>;
|
|
56
|
+
case 'CUSTOMER_PASSWORD_UPDATED':
|
|
57
|
+
return <Trans>Password updated</Trans>;
|
|
58
|
+
case 'CUSTOMER_PASSWORD_RESET_REQUESTED':
|
|
59
|
+
return <Trans>Password reset requested</Trans>;
|
|
60
|
+
case 'CUSTOMER_PASSWORD_RESET_VERIFIED':
|
|
61
|
+
return <Trans>Password reset verified</Trans>;
|
|
62
|
+
case 'CUSTOMER_EMAIL_UPDATE_REQUESTED':
|
|
63
|
+
return <Trans>Email update requested</Trans>;
|
|
64
|
+
case 'CUSTOMER_EMAIL_UPDATE_VERIFIED':
|
|
65
|
+
return <Trans>Email update verified</Trans>;
|
|
66
|
+
default:
|
|
67
|
+
return <Trans>{entry.type.replace(/_/g, ' ').toLowerCase()}</Trans>;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getIconColor = (entry: HistoryEntryItem) => {
|
|
72
|
+
// Check for success states
|
|
73
|
+
if (
|
|
74
|
+
entry.type === 'CUSTOMER_VERIFIED' ||
|
|
75
|
+
entry.type === 'CUSTOMER_EMAIL_UPDATE_VERIFIED' ||
|
|
76
|
+
entry.type === 'CUSTOMER_PASSWORD_RESET_VERIFIED'
|
|
77
|
+
) {
|
|
78
|
+
return 'bg-success text-success-foreground';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check for destructive states
|
|
82
|
+
if (entry.type === 'CUSTOMER_REMOVED_FROM_GROUP' || entry.type === 'CUSTOMER_ADDRESS_DELETED') {
|
|
83
|
+
return 'bg-destructive text-destructive-foreground';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Registration gets muted style
|
|
87
|
+
if (entry.type === 'CUSTOMER_REGISTERED') {
|
|
88
|
+
return 'bg-muted text-muted-foreground';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// All other entries use neutral colors
|
|
92
|
+
return 'bg-muted text-muted-foreground';
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getActorName = (entry: HistoryEntryItem) => {
|
|
96
|
+
if (entry.administrator) {
|
|
97
|
+
return `${entry.administrator.firstName} ${entry.administrator.lastName}`;
|
|
98
|
+
} else if (customer) {
|
|
99
|
+
return `${customer.firstName} ${customer.lastName}`;
|
|
100
|
+
}
|
|
101
|
+
return '';
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const isPrimaryEvent = (entry: HistoryEntryItem) => {
|
|
105
|
+
switch (entry.type) {
|
|
106
|
+
case 'CUSTOMER_REGISTERED':
|
|
107
|
+
case 'CUSTOMER_VERIFIED':
|
|
108
|
+
case 'CUSTOMER_NOTE':
|
|
109
|
+
case 'CUSTOMER_EMAIL_UPDATE_VERIFIED':
|
|
110
|
+
case 'CUSTOMER_PASSWORD_RESET_VERIFIED':
|
|
111
|
+
return true;
|
|
112
|
+
default:
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
getTimelineIcon,
|
|
119
|
+
getTitle,
|
|
120
|
+
getIconColor,
|
|
121
|
+
getActorName,
|
|
122
|
+
isPrimaryEvent,
|
|
123
|
+
};
|
|
124
|
+
}
|