@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.
Files changed (66) hide show
  1. package/components/app/App.js +4 -2
  2. package/components/app/providers/GridColumnsProvider.d.ts +52 -0
  3. package/components/app/providers/GridColumnsProvider.js +177 -0
  4. package/components/app/providers/GridColumnsProvider.test.js +255 -0
  5. package/components/elements/addons/buttons/CustomIconButton.test.d.ts +1 -0
  6. package/components/elements/addons/buttons/CustomIconButton.test.js +81 -0
  7. package/components/elements/addons/layout/FlexVertical.test.d.ts +1 -0
  8. package/components/elements/addons/layout/FlexVertical.test.js +32 -0
  9. package/components/elements/addons/layout/vsbox/VSBoxContent.test.d.ts +1 -0
  10. package/components/elements/addons/layout/vsbox/VSBoxContent.test.js +26 -0
  11. package/components/elements/addons/search/phrase/WordLexer.test.d.ts +1 -0
  12. package/components/elements/addons/search/phrase/WordLexer.test.js +55 -0
  13. package/components/elements/hit/HitContextMenu.test.d.ts +1 -0
  14. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.d.ts +2 -1
  15. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.js +3 -3
  16. package/components/{routes/hits/search → elements/hit}/grid/HitRow.d.ts +1 -1
  17. package/components/elements/hit/grid/HitTable.d.ts +12 -0
  18. package/components/elements/hit/grid/HitTable.js +82 -0
  19. package/components/elements/view/LayoutToggle.d.ts +8 -0
  20. package/components/elements/view/LayoutToggle.js +12 -0
  21. package/components/elements/view/ViewTitle.d.ts +3 -0
  22. package/components/elements/view/ViewTitle.js +6 -3
  23. package/components/hooks/useMyTheme.test.d.ts +1 -0
  24. package/components/hooks/useMyTheme.test.js +35 -0
  25. package/components/routes/advanced/eqlTokenProvider.test.d.ts +1 -0
  26. package/components/routes/advanced/eqlTokenProvider.test.js +34 -0
  27. package/components/routes/advanced/luceneTokenProvider.test.d.ts +1 -0
  28. package/components/routes/advanced/luceneTokenProvider.test.js +34 -0
  29. package/components/routes/hits/search/HitBrowser.js +2 -1
  30. package/components/routes/hits/search/SearchPane.js +6 -8
  31. package/components/routes/hits/search/ViewLink.js +3 -2
  32. package/components/routes/hits/search/grid/HitGrid.js +17 -85
  33. package/components/routes/hits/search/shared/LayoutSettings.js +25 -0
  34. package/components/routes/hits/search/shared/SearchActionMenu.d.ts +4 -0
  35. package/components/routes/hits/search/shared/SearchActionMenu.js +17 -0
  36. package/components/routes/home/HomeSettings.test.d.ts +1 -0
  37. package/components/routes/home/HomeSettings.test.js +44 -0
  38. package/components/routes/home/ViewCard.js +1 -1
  39. package/components/routes/overviews/markdownExtendedTokenProvider.test.d.ts +1 -0
  40. package/components/routes/overviews/markdownExtendedTokenProvider.test.js +50 -0
  41. package/components/routes/views/ViewComposer.js +32 -6
  42. package/locales/en/translation.json +6 -3
  43. package/locales/fr/translation.json +6 -3
  44. package/models/entities/generated/Column.d.ts +7 -0
  45. package/models/entities/generated/Host.d.ts +1 -0
  46. package/models/entities/generated/Settings.d.ts +4 -0
  47. package/package.json +2 -1
  48. package/rest/AxiosClient.test.d.ts +1 -0
  49. package/rest/AxiosClient.test.js +184 -0
  50. package/utils/stringUtils.d.ts +1 -0
  51. package/utils/stringUtils.js +7 -0
  52. package/utils/stringUtils.test.js +21 -1
  53. package/utils/utils.test.js +119 -2
  54. package/components/routes/hits/search/LayoutSettings.js +0 -24
  55. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/GridColumnsProvider.test.d.ts} +0 -0
  56. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.d.ts +0 -0
  57. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.js +0 -0
  58. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.test.js +0 -0
  59. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.d.ts +0 -0
  60. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.js +0 -0
  61. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.d.ts +0 -0
  62. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.js +0 -0
  63. /package/components/{routes/hits/search → elements/hit}/grid/HitRow.js +0 -0
  64. /package/components/routes/hits/search/{BundleParentMenu.d.ts → shared/BundleParentMenu.d.ts} +0 -0
  65. /package/components/routes/hits/search/{BundleParentMenu.js → shared/BundleParentMenu.js} +0 -0
  66. /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
+ });
@@ -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;
@@ -45,3 +45,10 @@ export const validateRegex = (regex) => {
45
45
  return false;
46
46
  }
47
47
  };
48
+ export const parsePixelSizeStringToInt = (size) => {
49
+ const match = size.match(/^(\d+(\.\d+)?)(px)?$/);
50
+ if (match) {
51
+ return parseInt(match[1]);
52
+ }
53
+ return null;
54
+ };
@@ -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
+ });
@@ -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;