@cccsaurora/howler-ui 2.19.0-dev.1099 → 2.19.0-dev.1148
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/components/app/App.js +4 -2
- package/components/app/providers/GridColumnsProvider.d.ts +52 -0
- package/components/app/providers/GridColumnsProvider.js +177 -0
- package/components/app/providers/GridColumnsProvider.test.js +255 -0
- package/components/elements/addons/buttons/CustomIconButton.test.d.ts +1 -0
- package/components/elements/addons/buttons/CustomIconButton.test.js +81 -0
- package/components/elements/addons/layout/FlexVertical.test.d.ts +1 -0
- package/components/elements/addons/layout/FlexVertical.test.js +32 -0
- package/components/elements/addons/layout/vsbox/VSBoxContent.test.d.ts +1 -0
- package/components/elements/addons/layout/vsbox/VSBoxContent.test.js +26 -0
- package/components/elements/addons/search/phrase/WordLexer.test.d.ts +1 -0
- package/components/elements/addons/search/phrase/WordLexer.test.js +55 -0
- package/components/elements/hit/HitContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.d.ts +2 -1
- package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.js +3 -3
- package/components/{routes/hits/search → elements/hit}/grid/HitRow.d.ts +1 -1
- package/components/elements/hit/grid/HitTable.d.ts +12 -0
- package/components/elements/hit/grid/HitTable.js +82 -0
- package/components/elements/view/LayoutToggle.d.ts +8 -0
- package/components/elements/view/LayoutToggle.js +12 -0
- package/components/elements/view/ViewTitle.d.ts +3 -0
- package/components/elements/view/ViewTitle.js +6 -3
- package/components/hooks/useMyTheme.test.d.ts +1 -0
- package/components/hooks/useMyTheme.test.js +35 -0
- package/components/routes/advanced/eqlTokenProvider.test.d.ts +1 -0
- package/components/routes/advanced/eqlTokenProvider.test.js +34 -0
- package/components/routes/advanced/luceneTokenProvider.test.d.ts +1 -0
- package/components/routes/advanced/luceneTokenProvider.test.js +34 -0
- package/components/routes/hits/search/HitBrowser.js +2 -1
- package/components/routes/hits/search/SearchPane.js +6 -8
- package/components/routes/hits/search/ViewLink.js +3 -2
- package/components/routes/hits/search/grid/HitGrid.js +17 -85
- package/components/routes/hits/search/shared/LayoutSettings.js +25 -0
- package/components/routes/hits/search/shared/SearchActionMenu.d.ts +4 -0
- package/components/routes/hits/search/shared/SearchActionMenu.js +17 -0
- package/components/routes/home/HomeSettings.test.d.ts +1 -0
- package/components/routes/home/HomeSettings.test.js +44 -0
- package/components/routes/home/ViewCard.js +1 -1
- package/components/routes/overviews/markdownExtendedTokenProvider.test.d.ts +1 -0
- package/components/routes/overviews/markdownExtendedTokenProvider.test.js +50 -0
- package/components/routes/views/ViewComposer.js +32 -6
- package/locales/en/translation.json +6 -3
- package/locales/fr/translation.json +6 -3
- package/models/entities/generated/Column.d.ts +7 -0
- package/models/entities/generated/Host.d.ts +1 -0
- package/models/entities/generated/Settings.d.ts +4 -0
- package/package.json +2 -1
- package/rest/AxiosClient.test.d.ts +1 -0
- package/rest/AxiosClient.test.js +184 -0
- package/utils/stringUtils.d.ts +1 -0
- package/utils/stringUtils.js +7 -0
- package/utils/stringUtils.test.js +21 -1
- package/utils/utils.test.js +119 -2
- package/components/routes/hits/search/LayoutSettings.js +0 -24
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/GridColumnsProvider.test.d.ts} +0 -0
- /package/components/{routes/hits/search → elements/hit}/HitContextMenu.d.ts +0 -0
- /package/components/{routes/hits/search → elements/hit}/HitContextMenu.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/HitContextMenu.test.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.d.ts +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.d.ts +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.js +0 -0
- /package/components/{routes/hits/search → elements/hit}/grid/HitRow.js +0 -0
- /package/components/routes/hits/search/{BundleParentMenu.d.ts → shared/BundleParentMenu.d.ts} +0 -0
- /package/components/routes/hits/search/{BundleParentMenu.js → shared/BundleParentMenu.js} +0 -0
- /package/components/routes/hits/search/{LayoutSettings.d.ts → shared/LayoutSettings.d.ts} +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import AxiosClient from './AxiosClient';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// vi.hoisted() ensures these values are available when the vi.mock factories
|
|
6
|
+
// run (which happens before any module code executes).
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const { mockAxiosInstance, mockAxiosError, mockInterceptorResponseUse, interceptorRef } = vi.hoisted(() => {
|
|
9
|
+
// Ref object used to capture the response interceptor across test resets
|
|
10
|
+
const _interceptorRef = { fn: null };
|
|
11
|
+
const _mockInterceptorResponseUse = vi.fn().mockImplementation((onFulfilled) => {
|
|
12
|
+
_interceptorRef.fn = onFulfilled;
|
|
13
|
+
});
|
|
14
|
+
const _mockAxiosInstance = vi.fn();
|
|
15
|
+
_mockAxiosInstance.interceptors = {
|
|
16
|
+
response: { use: _mockInterceptorResponseUse }
|
|
17
|
+
};
|
|
18
|
+
class MockAxiosError extends Error {
|
|
19
|
+
response;
|
|
20
|
+
constructor(message, response) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'AxiosError';
|
|
23
|
+
this.response = response;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
mockAxiosInstance: _mockAxiosInstance,
|
|
28
|
+
mockAxiosError: MockAxiosError,
|
|
29
|
+
mockInterceptorResponseUse: _mockInterceptorResponseUse,
|
|
30
|
+
interceptorRef: _interceptorRef
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
vi.mock('axios', () => ({
|
|
34
|
+
default: { create: vi.fn(() => mockAxiosInstance) },
|
|
35
|
+
AxiosError: mockAxiosError
|
|
36
|
+
}));
|
|
37
|
+
vi.mock('axios-retry', () => ({
|
|
38
|
+
default: vi.fn(),
|
|
39
|
+
exponentialDelay: vi.fn(),
|
|
40
|
+
isNetworkError: vi.fn()
|
|
41
|
+
}));
|
|
42
|
+
vi.mock('utils/sessionStorage', () => ({
|
|
43
|
+
getAxiosCache: vi.fn(() => ({})),
|
|
44
|
+
setAxiosCache: vi.fn()
|
|
45
|
+
}));
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Tests
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
describe('AxiosClient', () => {
|
|
50
|
+
let client;
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
interceptorRef.fn = null;
|
|
53
|
+
client = new AxiosClient();
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
// fetch() – happy paths
|
|
60
|
+
// -------------------------------------------------------------------------
|
|
61
|
+
describe('fetch() – successful responses', () => {
|
|
62
|
+
it('returns [data, status, headers] on a 200 response', async () => {
|
|
63
|
+
const payload = { api_response: { items: [] } };
|
|
64
|
+
mockAxiosInstance.mockResolvedValueOnce({
|
|
65
|
+
data: payload,
|
|
66
|
+
status: 200,
|
|
67
|
+
headers: { 'content-type': 'application/json' }
|
|
68
|
+
});
|
|
69
|
+
const result = await client.fetch('/api/v1/hit');
|
|
70
|
+
expect(result[0]).toEqual(payload);
|
|
71
|
+
expect(result[1]).toBe(200);
|
|
72
|
+
});
|
|
73
|
+
it('uses GET as the default HTTP method', async () => {
|
|
74
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
75
|
+
await client.fetch('/api/v1/hit');
|
|
76
|
+
const config = mockAxiosInstance.mock.calls[0][0];
|
|
77
|
+
expect(config.method).toBe('get');
|
|
78
|
+
});
|
|
79
|
+
it('passes POST as the HTTP method when specified', async () => {
|
|
80
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 201, headers: {} });
|
|
81
|
+
await client.fetch('/api/v1/hit', 'post', { key: 'val' });
|
|
82
|
+
const config = mockAxiosInstance.mock.calls[0][0];
|
|
83
|
+
expect(config.method).toBe('post');
|
|
84
|
+
});
|
|
85
|
+
it('serialises the body as a JSON string', async () => {
|
|
86
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
87
|
+
await client.fetch('/api/v1/hit', 'post', { status: 'open' });
|
|
88
|
+
const config = mockAxiosInstance.mock.calls[0][0];
|
|
89
|
+
expect(config.data).toBe(JSON.stringify({ status: 'open' }));
|
|
90
|
+
});
|
|
91
|
+
it('passes URLSearchParams through as params', async () => {
|
|
92
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
93
|
+
const params = new URLSearchParams({ q: 'test' });
|
|
94
|
+
await client.fetch('/api/v1/hit', 'get', undefined, params);
|
|
95
|
+
const config = mockAxiosInstance.mock.calls[0][0];
|
|
96
|
+
expect(config.params).toBe(params);
|
|
97
|
+
});
|
|
98
|
+
it('forwards custom headers', async () => {
|
|
99
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
100
|
+
const headers = { Authorization: '******' };
|
|
101
|
+
await client.fetch('/api/v1/hit', 'get', undefined, undefined, headers);
|
|
102
|
+
const config = mockAxiosInstance.mock.calls[0][0];
|
|
103
|
+
expect(config.headers).toEqual(headers);
|
|
104
|
+
});
|
|
105
|
+
it('sets withCredentials to true', async () => {
|
|
106
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
107
|
+
await client.fetch('/api/v1/hit');
|
|
108
|
+
const config = mockAxiosInstance.mock.calls[0][0];
|
|
109
|
+
expect(config.withCredentials).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it('passes the URL through to the axios config', async () => {
|
|
112
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
113
|
+
await client.fetch('/api/v1/test-url');
|
|
114
|
+
const config = mockAxiosInstance.mock.calls[0][0];
|
|
115
|
+
expect(config.url).toBe('/api/v1/test-url');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// -------------------------------------------------------------------------
|
|
119
|
+
// fetch() – error handling
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
describe('fetch() – error handling', () => {
|
|
122
|
+
it('returns [data, status, headers] when the error is an AxiosError with a response', async () => {
|
|
123
|
+
const errorResponse = { data: { error: 'not found' }, status: 404, headers: {} };
|
|
124
|
+
const axiosErr = new mockAxiosError('Not Found', errorResponse);
|
|
125
|
+
mockAxiosInstance.mockRejectedValueOnce(axiosErr);
|
|
126
|
+
const result = await client.fetch('/api/v1/missing');
|
|
127
|
+
expect(result[0]).toEqual({ error: 'not found' });
|
|
128
|
+
expect(result[1]).toBe(404);
|
|
129
|
+
});
|
|
130
|
+
it('rethrows non-AxiosError exceptions', async () => {
|
|
131
|
+
mockAxiosInstance.mockRejectedValueOnce(new TypeError('network failure'));
|
|
132
|
+
await expect(client.fetch('/api/v1/hit')).rejects.toThrow('network failure');
|
|
133
|
+
});
|
|
134
|
+
it('rethrows AxiosError that has no response body', async () => {
|
|
135
|
+
// AxiosError without a .response property → condition `e.response?.data` is falsy → rethrow
|
|
136
|
+
const axiosErr = new mockAxiosError('timeout');
|
|
137
|
+
mockAxiosInstance.mockRejectedValueOnce(axiosErr);
|
|
138
|
+
await expect(client.fetch('/api/v1/hit')).rejects.toThrow('timeout');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// AxiosCache – response interceptor
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
describe('AxiosCache response interceptor', () => {
|
|
145
|
+
it('is registered on the axios instance during construction', () => {
|
|
146
|
+
expect(mockInterceptorResponseUse).toHaveBeenCalled();
|
|
147
|
+
expect(interceptorRef.fn).toBeTypeOf('function');
|
|
148
|
+
});
|
|
149
|
+
it('stores a new etag cache entry on a 2xx response with an etag header', async () => {
|
|
150
|
+
const { setAxiosCache } = await import('utils/sessionStorage');
|
|
151
|
+
const res = {
|
|
152
|
+
status: 200,
|
|
153
|
+
headers: { etag: '"abc123"' },
|
|
154
|
+
data: { value: 1 },
|
|
155
|
+
config: { headers: {} }
|
|
156
|
+
};
|
|
157
|
+
await interceptorRef.fn(res);
|
|
158
|
+
expect(setAxiosCache).toHaveBeenCalledWith('"abc123"', { value: 1 });
|
|
159
|
+
});
|
|
160
|
+
it('replaces data from cache on a 304 response', async () => {
|
|
161
|
+
const { getAxiosCache } = await import('utils/sessionStorage');
|
|
162
|
+
getAxiosCache.mockReturnValueOnce({ '"etag-v1"': { cached: true } });
|
|
163
|
+
const res = {
|
|
164
|
+
status: 304,
|
|
165
|
+
headers: {},
|
|
166
|
+
data: null,
|
|
167
|
+
config: { headers: { 'If-Match': '"etag-v1"' } }
|
|
168
|
+
};
|
|
169
|
+
const result = await interceptorRef.fn(res);
|
|
170
|
+
expect(result.data).toEqual({ cached: true });
|
|
171
|
+
});
|
|
172
|
+
it('passes through a 2xx response without an etag without caching', async () => {
|
|
173
|
+
const { setAxiosCache } = await import('utils/sessionStorage');
|
|
174
|
+
const res = {
|
|
175
|
+
status: 200,
|
|
176
|
+
headers: {},
|
|
177
|
+
data: { ok: true },
|
|
178
|
+
config: { headers: {} }
|
|
179
|
+
};
|
|
180
|
+
await interceptorRef.fn(res);
|
|
181
|
+
expect(setAxiosCache).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
package/utils/stringUtils.d.ts
CHANGED
|
@@ -6,3 +6,4 @@ export declare const sanitizeLuceneQuery: (query: string) => string;
|
|
|
6
6
|
export declare const safeStringPropertyCompare: (propertyPath: string) => (a: unknown, b: unknown) => any;
|
|
7
7
|
export declare const sanitizeMultilineLucene: (query: string) => string;
|
|
8
8
|
export declare const validateRegex: (regex: string) => boolean;
|
|
9
|
+
export declare const parsePixelSizeStringToInt: (size: string) => number;
|
package/utils/stringUtils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="vitest" />
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { maxLenStr, nameToInitials, safeFieldValue, safeFieldValueURI, safeStringPropertyCompare, sanitizeLuceneQuery, sanitizeMultilineLucene, validateRegex } from './stringUtils';
|
|
3
|
+
import { maxLenStr, nameToInitials, parsePixelSizeStringToInt, safeFieldValue, safeFieldValueURI, safeStringPropertyCompare, sanitizeLuceneQuery, sanitizeMultilineLucene, validateRegex } from './stringUtils';
|
|
4
4
|
describe('nameToInitials', () => {
|
|
5
5
|
it('returns initials from a standard first-last name', () => {
|
|
6
6
|
expect(nameToInitials('John Doe')).toEqual(['J', 'D']);
|
|
@@ -157,3 +157,23 @@ describe('validateRegex', () => {
|
|
|
157
157
|
expect(validateRegex('a{2,5}')).toBe(true);
|
|
158
158
|
});
|
|
159
159
|
});
|
|
160
|
+
describe('parsePixelSizeStringToInt', () => {
|
|
161
|
+
it('parses a pixel size string with "px" suffix', () => {
|
|
162
|
+
expect(parsePixelSizeStringToInt('100px')).toBe(100);
|
|
163
|
+
});
|
|
164
|
+
it('parses a pixel size string without "px" suffix', () => {
|
|
165
|
+
expect(parsePixelSizeStringToInt('200')).toBe(200);
|
|
166
|
+
});
|
|
167
|
+
it('parses a pixel size string with decimal value', () => {
|
|
168
|
+
expect(parsePixelSizeStringToInt('150.5px')).toBe(150);
|
|
169
|
+
});
|
|
170
|
+
it('returns null for a non-numeric string', () => {
|
|
171
|
+
expect(parsePixelSizeStringToInt('abc')).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
it('returns null for an empty string', () => {
|
|
174
|
+
expect(parsePixelSizeStringToInt('')).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
it('returns null for a string with an invalid unit', () => {
|
|
177
|
+
expect(parsePixelSizeStringToInt('50rem')).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|
package/utils/utils.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="vitest" />
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { bytesToSize, compareTimestamp, convertCustomDateRangeToLucene, convertDateToLucene, convertLuceneToDate, flattenDeep, formatDate, getTimeRange, hashCode, humanReadableNumber, removeEmpty, searchObject, sortByTimestamp, tryParse, twitterShort } from './utils';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { bytesToSize, compareTimestamp, convertCustomDateRangeToLucene, convertDateToLucene, convertLuceneToDate, delay, flattenDeep, formatDate, getProvider, getTimeRange, hashCode, humanReadableNumber, removeEmpty, searchObject, searchResultsDisplay, sortByTimestamp, stringToColor, tryParse, twitterShort } from './utils';
|
|
4
4
|
describe('bytesToSize', () => {
|
|
5
5
|
it('returns "0 B" for 0', () => {
|
|
6
6
|
expect(bytesToSize(0)).toBe('0 B');
|
|
@@ -290,3 +290,120 @@ describe('twitterShort', () => {
|
|
|
290
290
|
expect(result).toMatch(/year/);
|
|
291
291
|
});
|
|
292
292
|
});
|
|
293
|
+
describe('stringToColor', () => {
|
|
294
|
+
it('returns a non-empty string for any input', () => {
|
|
295
|
+
const result = stringToColor('hello');
|
|
296
|
+
expect(typeof result).toBe('string');
|
|
297
|
+
expect(result.length).toBeGreaterThan(0);
|
|
298
|
+
});
|
|
299
|
+
it('returns the same color for the same input', () => {
|
|
300
|
+
expect(stringToColor('alice')).toBe(stringToColor('alice'));
|
|
301
|
+
});
|
|
302
|
+
it('returns different colors for different inputs', () => {
|
|
303
|
+
// Not guaranteed for all pairs, but statistically very likely for distinct words
|
|
304
|
+
const colors = ['alice', 'bob', 'carol', 'dave'].map(stringToColor);
|
|
305
|
+
const unique = new Set(colors);
|
|
306
|
+
expect(unique.size).toBeGreaterThan(1);
|
|
307
|
+
});
|
|
308
|
+
it('handles an empty string without throwing', () => {
|
|
309
|
+
expect(() => stringToColor('')).not.toThrow();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
describe('delay', () => {
|
|
313
|
+
it('resolves after the specified time', async () => {
|
|
314
|
+
vi.useFakeTimers();
|
|
315
|
+
const promise = delay(100);
|
|
316
|
+
vi.advanceTimersByTime(100);
|
|
317
|
+
await expect(promise).resolves.toBeUndefined();
|
|
318
|
+
vi.useRealTimers();
|
|
319
|
+
});
|
|
320
|
+
it('does not resolve before the specified time', async () => {
|
|
321
|
+
vi.useFakeTimers();
|
|
322
|
+
let resolved = false;
|
|
323
|
+
delay(200).then(() => {
|
|
324
|
+
resolved = true;
|
|
325
|
+
});
|
|
326
|
+
vi.advanceTimersByTime(100);
|
|
327
|
+
expect(resolved).toBe(false);
|
|
328
|
+
vi.advanceTimersByTime(100);
|
|
329
|
+
await Promise.resolve(); // flush microtasks
|
|
330
|
+
expect(resolved).toBe(true);
|
|
331
|
+
vi.useRealTimers();
|
|
332
|
+
});
|
|
333
|
+
it('can be cancelled via the .cancel() method without rejecting by default', () => {
|
|
334
|
+
vi.useFakeTimers();
|
|
335
|
+
const d = delay(100);
|
|
336
|
+
expect(() => d.cancel()).not.toThrow();
|
|
337
|
+
vi.useRealTimers();
|
|
338
|
+
});
|
|
339
|
+
it('rejects on cancel when rejectOnCancel=true', async () => {
|
|
340
|
+
vi.useFakeTimers();
|
|
341
|
+
const d = delay(100, true);
|
|
342
|
+
const rejection = expect(d).rejects.toBeUndefined();
|
|
343
|
+
d.cancel();
|
|
344
|
+
await rejection;
|
|
345
|
+
vi.useRealTimers();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
describe('getProvider', () => {
|
|
349
|
+
const originalLocation = window.location;
|
|
350
|
+
afterEach(() => {
|
|
351
|
+
Object.defineProperty(window, 'location', { value: originalLocation, writable: true });
|
|
352
|
+
});
|
|
353
|
+
it('returns the provider from the search params when not in oauth path', () => {
|
|
354
|
+
Object.defineProperty(window, 'location', {
|
|
355
|
+
writable: true,
|
|
356
|
+
value: { pathname: '/login', search: '?provider=azure', href: 'http://localhost/login?provider=azure' }
|
|
357
|
+
});
|
|
358
|
+
expect(getProvider()).toBe('azure');
|
|
359
|
+
});
|
|
360
|
+
it('returns null when no provider param is present and path is not oauth', () => {
|
|
361
|
+
Object.defineProperty(window, 'location', {
|
|
362
|
+
writable: true,
|
|
363
|
+
value: { pathname: '/hits', search: '', href: 'http://localhost/hits' }
|
|
364
|
+
});
|
|
365
|
+
expect(getProvider()).toBeNull();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
describe('searchResultsDisplay', () => {
|
|
369
|
+
const originalLocation = window.location;
|
|
370
|
+
afterEach(() => {
|
|
371
|
+
Object.defineProperty(window, 'location', { value: originalLocation, writable: true });
|
|
372
|
+
});
|
|
373
|
+
it('returns count as string when below the max', () => {
|
|
374
|
+
Object.defineProperty(window, 'location', {
|
|
375
|
+
writable: true,
|
|
376
|
+
value: { pathname: '/', search: '', href: 'http://localhost/' }
|
|
377
|
+
});
|
|
378
|
+
expect(searchResultsDisplay(500)).toBe('500');
|
|
379
|
+
});
|
|
380
|
+
it('appends "+" when count equals the default max (10000) and no track_total_hits param', () => {
|
|
381
|
+
Object.defineProperty(window, 'location', {
|
|
382
|
+
writable: true,
|
|
383
|
+
value: { pathname: '/', search: '', href: 'http://localhost/' }
|
|
384
|
+
});
|
|
385
|
+
expect(searchResultsDisplay(10000)).toBe('10000+');
|
|
386
|
+
});
|
|
387
|
+
it('appends "+" when count matches the explicit track_total_hits param', () => {
|
|
388
|
+
Object.defineProperty(window, 'location', {
|
|
389
|
+
writable: true,
|
|
390
|
+
value: { pathname: '/', search: '?track_total_hits=500', href: 'http://localhost/?track_total_hits=500' }
|
|
391
|
+
});
|
|
392
|
+
expect(searchResultsDisplay(500)).toBe('500+');
|
|
393
|
+
});
|
|
394
|
+
it('does not append "+" when count does not equal track_total_hits', () => {
|
|
395
|
+
Object.defineProperty(window, 'location', {
|
|
396
|
+
writable: true,
|
|
397
|
+
value: { pathname: '/', search: '?track_total_hits=1000', href: 'http://localhost/?track_total_hits=1000' }
|
|
398
|
+
});
|
|
399
|
+
expect(searchResultsDisplay(500)).toBe('500');
|
|
400
|
+
});
|
|
401
|
+
it('uses a custom max when provided', () => {
|
|
402
|
+
Object.defineProperty(window, 'location', {
|
|
403
|
+
writable: true,
|
|
404
|
+
value: { pathname: '/', search: '', href: 'http://localhost/' }
|
|
405
|
+
});
|
|
406
|
+
expect(searchResultsDisplay(500, 500)).toBe('500+');
|
|
407
|
+
expect(searchResultsDisplay(499, 500)).toBe('499');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowDropDown, InfoOutlined, List, Settings, TableChart, ViewComfy, ViewCompact, ViewModule } from '@mui/icons-material';
|
|
3
|
-
import { Checkbox, Divider, FormLabel, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material';
|
|
4
|
-
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
5
|
-
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
6
|
-
import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
|
|
7
|
-
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
8
|
-
import { useTranslation } from 'react-i18next';
|
|
9
|
-
import { useContextSelector } from 'use-context-selector';
|
|
10
|
-
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
11
|
-
const LayoutSettings = () => {
|
|
12
|
-
const { t } = useTranslation();
|
|
13
|
-
const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
|
|
14
|
-
const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
|
|
15
|
-
const [hitLayout, setHitLayout] = useMyLocalStorageItem(StorageKey.HIT_LAYOUT, false);
|
|
16
|
-
const [templateFieldCount, setTemplateFieldCount] = useMyLocalStorageItem(StorageKey.TEMPLATE_FIELD_COUNT, null);
|
|
17
|
-
return (_jsx(ChipPopper, { icon: _jsx(Tooltip, { title: t('search.layout.settings'), children: _jsx(Settings, {}) }), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, disablePortal: false, slotProps: { chip: { size: 'medium', 'aria-label': t('search.layout.settings') } }, placement: "bottom-end", children: _jsxs(Stack, { spacing: 1, alignItems: "start", children: [_jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "display_type", children: t('page.settings.local.hits.display_type') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.display_type.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", "aria-labelledby": "display_type", children: [_jsx(ToggleButton, { value: "list", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(List, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.list') })] }) }), _jsx(ToggleButton, { value: "grid", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(TableChart, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.grid') })] }) })] }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "layout", children: t('page.settings.local.hits.layout') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.layout.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(ToggleButtonGroup, { exclusive: true, size: "small", value: hitLayout, onChange: (_, value) => setHitLayout(value), "aria-labelledby": "layout", children: [_jsx(ToggleButton, { value: HitLayout.DENSE, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewCompact, {}), _jsx("span", { children: t('page.settings.local.hits.layout.dense') })] }) }), _jsx(ToggleButton, { value: HitLayout.NORMAL, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewModule, {}), _jsx("span", { children: t('page.settings.local.hits.layout.normal') })] }) }), _jsx(ToggleButton, { value: HitLayout.COMFY, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewComfy, {}), _jsx("span", { children: t('page.settings.local.hits.layout.comfy') })] }) })] }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "field_count", children: t('page.settings.local.hits.field_count') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.field_count.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignSelf: "stretch", children: [_jsx(Checkbox, { checked: templateFieldCount !== null, onChange: (_, checked) => setTemplateFieldCount(checked ? 3 : null), size: "small" }), _jsx(TextField, { type: "number", size: "small", disabled: templateFieldCount === null, value: templateFieldCount ?? 3, fullWidth: true, onChange: e => {
|
|
18
|
-
const val = parseInt(e.target.value);
|
|
19
|
-
if (!isNaN(val)) {
|
|
20
|
-
setTemplateFieldCount(Math.min(15, Math.max(0, val)));
|
|
21
|
-
}
|
|
22
|
-
}, inputProps: { min: 0, max: 15, 'aria-labelledby': 'field_count' } })] })] }) }));
|
|
23
|
-
};
|
|
24
|
-
export default LayoutSettings;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/components/routes/hits/search/{BundleParentMenu.d.ts → shared/BundleParentMenu.d.ts}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|