@cccsaurora/howler-ui 2.16.0-dev.376 → 2.16.0-dev.380

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 (58) hide show
  1. package/commons/components/app/hooks/useAppConfigs.d.ts +1 -1
  2. package/components/app/App.js +2 -0
  3. package/components/app/hooks/useMatchers.js +0 -4
  4. package/components/app/providers/FavouritesProvider.js +2 -1
  5. package/components/app/providers/FieldProvider.d.ts +2 -2
  6. package/components/app/providers/HitProvider.d.ts +3 -3
  7. package/components/app/providers/HitSearchProvider.d.ts +7 -8
  8. package/components/app/providers/HitSearchProvider.js +64 -39
  9. package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
  10. package/components/app/providers/HitSearchProvider.test.js +505 -0
  11. package/components/app/providers/ParameterProvider.d.ts +13 -5
  12. package/components/app/providers/ParameterProvider.js +240 -84
  13. package/components/app/providers/ParameterProvider.test.d.ts +1 -0
  14. package/components/app/providers/ParameterProvider.test.js +1041 -0
  15. package/components/app/providers/ViewProvider.d.ts +3 -2
  16. package/components/app/providers/ViewProvider.js +21 -14
  17. package/components/app/providers/ViewProvider.test.js +19 -29
  18. package/components/elements/display/ChipPopper.d.ts +21 -0
  19. package/components/elements/display/ChipPopper.js +36 -0
  20. package/components/elements/display/ChipPopper.test.d.ts +1 -0
  21. package/components/elements/display/ChipPopper.test.js +309 -0
  22. package/components/elements/hit/HitActions.js +3 -3
  23. package/components/elements/hit/HitSummary.d.ts +0 -1
  24. package/components/elements/hit/HitSummary.js +11 -21
  25. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
  26. package/components/elements/hit/aggregate/HitGraph.js +9 -15
  27. package/components/routes/dossiers/DossierCard.test.js +0 -2
  28. package/components/routes/dossiers/DossierEditor.test.js +27 -33
  29. package/components/routes/hits/search/HitBrowser.js +7 -48
  30. package/components/routes/hits/search/HitContextMenu.test.js +11 -29
  31. package/components/routes/hits/search/InformationPane.js +1 -1
  32. package/components/routes/hits/search/QuerySettings.js +30 -0
  33. package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
  34. package/components/routes/hits/search/QuerySettings.test.js +553 -0
  35. package/components/routes/hits/search/SearchPane.js +8 -10
  36. package/components/routes/hits/search/ViewLink.d.ts +4 -1
  37. package/components/routes/hits/search/ViewLink.js +37 -19
  38. package/components/routes/hits/search/ViewLink.test.js +349 -303
  39. package/components/routes/hits/search/grid/HitGrid.js +2 -6
  40. package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
  41. package/components/routes/hits/search/shared/HitFilter.js +31 -23
  42. package/components/routes/hits/search/shared/HitSort.js +16 -8
  43. package/components/routes/hits/search/shared/SearchSpan.js +19 -10
  44. package/components/routes/views/ViewComposer.js +7 -6
  45. package/components/routes/views/Views.js +2 -1
  46. package/locales/en/translation.json +6 -0
  47. package/locales/fr/translation.json +6 -0
  48. package/package.json +2 -2
  49. package/setupTests.js +4 -1
  50. package/tests/mocks.d.ts +18 -0
  51. package/tests/mocks.js +65 -0
  52. package/tests/server-handlers.js +10 -28
  53. package/utils/viewUtils.d.ts +2 -0
  54. package/utils/viewUtils.js +11 -0
  55. package/components/routes/hits/search/shared/QuerySettings.js +0 -22
  56. /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
  57. /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
  58. /package/components/routes/hits/search/{CustomSort.js → shared/CustomSort.js} +0 -0
@@ -14,23 +14,22 @@ import { memo, useCallback, useContext, useEffect, useState } from 'react';
14
14
  import { useTranslation } from 'react-i18next';
15
15
  import { useContextSelector } from 'use-context-selector';
16
16
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
17
- import { convertCustomDateRangeToLucene, convertDateToLucene, getTimeRange } from '@cccsaurora/howler-ui/utils/utils';
17
+ import { getTimeRange } from '@cccsaurora/howler-ui/utils/utils';
18
18
  import PluginChip from '../PluginChip';
19
19
  import HitGraph from './aggregate/HitGraph';
20
- const HitSummary = ({ query, response, onStart, onComplete }) => {
20
+ const HitSummary = ({ response, onStart, onComplete }) => {
21
21
  const { t } = useTranslation();
22
22
  const { dispatchApi } = useMyApi();
23
23
  const { hitFields } = useContext(FieldContext);
24
24
  const { showErrorMessage } = useMySnackbar();
25
25
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
26
26
  const { getMatchingTemplate } = useMatchers();
27
- const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
28
- const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
29
27
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
30
28
  const error = useContextSelector(HitSearchContext, ctx => ctx.error);
31
- const span = useContextSelector(ParameterContext, ctx => ctx.span);
32
- const startDate = useContextSelector(ParameterContext, ctx => ctx.startDate);
33
- const endDate = useContextSelector(ParameterContext, ctx => ctx.endDate);
29
+ const getFilters = useContextSelector(HitSearchContext, ctx => ctx.getFilters);
30
+ const query = useContextSelector(ParameterContext, ctx => ctx.query);
31
+ const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
32
+ const views = useContextSelector(ParameterContext, ctx => ctx.views);
34
33
  const [loading, setLoading] = useState(false);
35
34
  const [customKeys, setCustomKeys] = useState([]);
36
35
  const [keyCounts, setKeyCounts] = useState({});
@@ -39,13 +38,6 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
39
38
  if (onStart) {
40
39
  onStart();
41
40
  }
42
- const filters = [];
43
- if (span && !span.endsWith('custom')) {
44
- filters.push(`event.created:${convertDateToLucene(span)}`);
45
- }
46
- else if (startDate && endDate) {
47
- filters.push(`event.created:${convertCustomDateRangeToLucene(startDate, endDate)}`);
48
- }
49
41
  try {
50
42
  // Get a list of every key in every template of the hits we're searching
51
43
  const rawCounts = await Promise.all((response?.items ?? []).map(async (h) => {
@@ -91,7 +83,7 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
91
83
  fields: sortedKeys,
92
84
  query,
93
85
  rows: pageCount,
94
- filters
86
+ filters: await getFilters()
95
87
  }), {
96
88
  throwError: false,
97
89
  logError: true,
@@ -118,7 +110,7 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
118
110
  }, [
119
111
  customKeys,
120
112
  dispatchApi,
121
- endDate,
113
+ getFilters,
122
114
  getMatchingTemplate,
123
115
  onComplete,
124
116
  onStart,
@@ -126,21 +118,19 @@ const HitSummary = ({ query, response, onStart, onComplete }) => {
126
118
  query,
127
119
  response?.items,
128
120
  showErrorMessage,
129
- span,
130
- startDate,
131
121
  t
132
122
  ]);
133
123
  const setSearch = useCallback((key, value) => {
134
124
  setQuery(`${key}:${value}`);
135
125
  }, [setQuery]);
136
126
  useEffect(() => {
137
- if ((!query && !viewId) || searching || error) {
127
+ if ((!query && views?.length < 1) || searching || error) {
138
128
  return;
139
129
  }
140
130
  performAggregation();
141
131
  // eslint-disable-next-line react-hooks/exhaustive-deps
142
- }, [query, viewId, searching, error]);
143
- return (_jsxs(Stack, { sx: { mx: 2, height: '100%' }, spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('hit.summary.aggregate.title') }), _jsx(Divider, { flexItem: true }), _jsx(HitGraph, { query: query }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { sx: { overflow: 'auto', marginTop: '0 !important' }, pt: 1, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(Autocomplete, { fullWidth: true, multiple: true, sx: { minWidth: '175px' }, size: "small", value: customKeys, options: hitFields.map(_field => _field.key), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.summary.adhoc') }), onChange: (_, value) => setCustomKeys(value) }), _jsx(Button, { variant: "outlined", startIcon: loading ? _jsx(CircularProgress, { size: 20, sx: { ml: 1 } }) : _jsx(Analytics, { sx: { ml: 1 } }), disabled: loading, onClick: () => performAggregation(), children: t('button.aggregate') })] }), isEmpty(aggregateResults) && (_jsxs(Alert, { severity: "info", variant: "outlined", children: [_jsx(AlertTitle, { children: t('hit.summary.aggregate.nokeys.title') }), t('hit.summary.aggregate.nokeys.description')] })), loading && _jsx(LinearProgress, { sx: { minHeight: '4px' } }), Object.keys(aggregateResults)
132
+ }, [query, views, searching, error]);
133
+ return (_jsxs(Stack, { sx: { mx: 2, height: '100%' }, spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('hit.summary.aggregate.title') }), _jsx(Divider, { flexItem: true }), _jsx(HitGraph, {}), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { sx: { overflow: 'auto', marginTop: '0 !important' }, pt: 1, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(Autocomplete, { fullWidth: true, multiple: true, sx: { minWidth: '175px' }, size: "small", value: customKeys, options: hitFields.map(_field => _field.key), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.summary.adhoc') }), onChange: (_, value) => setCustomKeys(value) }), _jsx(Button, { variant: "outlined", startIcon: loading ? _jsx(CircularProgress, { size: 20, sx: { ml: 1 } }) : _jsx(Analytics, { sx: { ml: 1 } }), disabled: loading, onClick: () => performAggregation(), children: t('button.aggregate') })] }), isEmpty(aggregateResults) && (_jsxs(Alert, { severity: "info", variant: "outlined", children: [_jsx(AlertTitle, { children: t('hit.summary.aggregate.nokeys.title') }), t('hit.summary.aggregate.nokeys.description')] })), loading && _jsx(LinearProgress, { sx: { minHeight: '4px' } }), Object.keys(aggregateResults)
144
134
  .filter(key => !isEmpty(aggregateResults[key]))
145
135
  .flatMap(key => [
146
136
  _jsx(Fade, { in: true, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "body1", children: key }, key + '-title'), keyCounts[key]?.count < 0 ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", t('hit.summary.adhoc.custom'), ")"] })) : (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", keyCounts[key]?.count ?? '?', " ", t('references'), ")"] })), _jsx(Tooltip, { title: _jsxs(Stack, { children: [_jsx(Typography, { variant: "caption", children: t('hit.summary.aggregate.sources') }), keyCounts[key]?.sources.map(source => (_jsx(Typography, { variant: "caption", children: source }, source))) ?? '?'] }), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }) }, key + '-refs'),
@@ -1,6 +1,4 @@
1
1
  import 'chartjs-adapter-dayjs-4';
2
2
  import type { FC } from 'react';
3
- declare const HitGraph: FC<{
4
- query: string;
5
- }>;
3
+ declare const HitGraph: FC;
6
4
  export default HitGraph;
@@ -16,7 +16,7 @@ import { Scatter } from 'react-chartjs-2';
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { useContextSelector } from 'use-context-selector';
18
18
  import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
19
- import { convertCustomDateRangeToLucene, convertDateToLucene, stringToColor } from '@cccsaurora/howler-ui/utils/utils';
19
+ import { stringToColor } from '@cccsaurora/howler-ui/utils/utils';
20
20
  const MAX_ROWS = 2500;
21
21
  const OVERRIDE_ROWS = 10000;
22
22
  const MAX_QUERY_SIZE = 50000;
@@ -27,23 +27,23 @@ const FILTER_FIELDS = [
27
27
  'howler.assessment',
28
28
  'howler.detection'
29
29
  ];
30
- const HitGraph = ({ query }) => {
30
+ const HitGraph = () => {
31
31
  const { t } = useTranslation();
32
32
  const theme = useTheme();
33
33
  const { dispatchApi } = useMyApi();
34
34
  const { scatter } = useMyChart();
35
35
  const { config } = useContext(ApiConfigContext);
36
36
  const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
37
+ const query = useContextSelector(ParameterContext, ctx => ctx.query);
37
38
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
38
39
  const span = useContextSelector(ParameterContext, ctx => ctx.span);
39
- const startDate = useContextSelector(ParameterContext, ctx => ctx.startDate);
40
- const endDate = useContextSelector(ParameterContext, ctx => ctx.endDate);
40
+ const views = useContextSelector(ParameterContext, ctx => ctx.views);
41
41
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
42
42
  const addHitToSelection = useContextSelector(HitContext, ctx => ctx.addHitToSelection);
43
43
  const removeHitFromSelection = useContextSelector(HitContext, ctx => ctx.removeHitFromSelection);
44
- const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
45
44
  const error = useContextSelector(HitSearchContext, ctx => ctx.error);
46
45
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
46
+ const getFilters = useContextSelector(HitSearchContext, ctx => ctx.getFilters);
47
47
  const chartRef = useRef();
48
48
  const [loading, setLoading] = useState(false);
49
49
  const [filterField, setFilterField] = useState(FILTER_FIELDS[0]);
@@ -57,13 +57,7 @@ const HitGraph = ({ query }) => {
57
57
  setLoading(true);
58
58
  setSearchTotal(0);
59
59
  try {
60
- const filters = [];
61
- if (span && !span.endsWith('custom')) {
62
- filters.push(`event.created:${convertDateToLucene(span)}`);
63
- }
64
- else if (startDate && endDate) {
65
- filters.push(`event.created:${convertCustomDateRangeToLucene(startDate, endDate)}`);
66
- }
60
+ const filters = await getFilters();
67
61
  if (escalationFilter) {
68
62
  filters.push(`howler.escalation:${escalationFilter}`);
69
63
  }
@@ -113,14 +107,14 @@ const HitGraph = ({ query }) => {
113
107
  finally {
114
108
  setLoading(false);
115
109
  }
116
- }, [dispatchApi, endDate, escalationFilter, filterField, override, query, span, startDate]);
110
+ }, [dispatchApi, escalationFilter, filterField, getFilters, override, query]);
117
111
  useEffect(() => {
118
- if ((!query && !viewId) || error || !response) {
112
+ if ((!query && views?.length < 1) || error || !response) {
119
113
  return;
120
114
  }
121
115
  performQuery();
122
116
  // eslint-disable-next-line react-hooks/exhaustive-deps
123
- }, [query, viewId, error, span, response]);
117
+ }, [query, views, error, span, response, escalationFilter, filterField]);
124
118
  const options = useMemo(() => {
125
119
  const parentOptions = scatter('hit.summary.title', 'hit.summary.subtitle');
126
120
  return {
@@ -1,6 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- /* eslint-disable react/jsx-no-literals */
3
- /* eslint-disable import/imports-first */
4
2
  /// <reference types="vitest" />
5
3
  import { render, screen, waitFor } from '@testing-library/react';
6
4
  import userEvent, {} from '@testing-library/user-event';
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /* eslint-disable import/imports-first */
3
2
  import { render, screen, waitFor } from '@testing-library/react';
4
3
  import userEvent from '@testing-library/user-event';
5
4
  import omit from 'lodash-es/omit';
5
+ import { act } from 'react';
6
+ import { setupContextSelectorMock, setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
6
7
  import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
7
8
  // Mock the API
8
9
  const mockApiSearchHitPost = vi.fn();
@@ -23,28 +24,8 @@ vi.mock('api', () => ({
23
24
  }
24
25
  }
25
26
  }));
26
- // Mock react-router-dom
27
- vi.mock('react-router-dom', async () => {
28
- const actual = await vi.importActual('react-router-dom');
29
- return {
30
- ...actual,
31
- useParams: vi.fn(),
32
- useSearchParams: vi.fn(() => [new URLSearchParams(), () => { }]),
33
- useNavigate: () => vi.fn()
34
- };
35
- });
36
- // Mock ParameterContext
37
- const mockSetQuery = vi.fn();
38
- vi.mock('use-context-selector', async () => {
39
- const actual = await vi.importActual('use-context-selector');
40
- return {
41
- ...actual,
42
- useContextSelector: (_context, selector) => {
43
- const mockContext = { setQuery: mockSetQuery };
44
- return selector(mockContext);
45
- }
46
- };
47
- });
27
+ setupReactRouterMock();
28
+ setupContextSelectorMock();
48
29
  vi.mock('components/elements/ThemedEditor', () => ({
49
30
  default: ({ value, onChange, id }) => {
50
31
  return (_jsx("input", { id: id || 'themed-editor', value: value, onChange: e => {
@@ -92,6 +73,7 @@ vi.mock('../hits/search/HitQuery', () => ({
92
73
  }
93
74
  }));
94
75
  import ApiConfigProvider from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
76
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
95
77
  import i18n from '@cccsaurora/howler-ui/i18n';
96
78
  import { I18nextProvider } from 'react-i18next';
97
79
  import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
@@ -100,6 +82,11 @@ const mockUseParams = vi.mocked(useParams);
100
82
  const mockUseSearchParams = vi.mocked(useSearchParams);
101
83
  // eslint-disable-next-line react-hooks/rules-of-hooks
102
84
  const mockNavigate = vi.mocked(useNavigate());
85
+ // Mock ParameterContext
86
+ const mockSetQuery = vi.fn();
87
+ let mockParameterContext = {
88
+ setQuery: mockSetQuery
89
+ };
103
90
  // Mock data
104
91
  const mockDossier = {
105
92
  dossier_id: 'test-dossier-1',
@@ -132,7 +119,7 @@ const Wrapper = ({ children }) => {
132
119
  configuration: {},
133
120
  c12nDef: {},
134
121
  mapping: {}
135
- }, children: children }) }));
122
+ }, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }));
136
123
  };
137
124
  describe('DossierEditor', () => {
138
125
  beforeEach(() => {
@@ -142,6 +129,8 @@ describe('DossierEditor', () => {
142
129
  mockApiDossierPut.mockClear();
143
130
  mockNavigate.mockClear();
144
131
  mockSetQuery.mockClear();
132
+ // Reset mock context
133
+ mockParameterContext.setQuery = mockSetQuery;
145
134
  // Default mock implementations
146
135
  mockApiSearchHitPost.mockResolvedValue({ total: 42, items: [] });
147
136
  });
@@ -294,17 +283,22 @@ describe('DossierEditor', () => {
294
283
  expect(saveButton).toBeDisabled();
295
284
  });
296
285
  it('should enable save button when all required fields are filled', async () => {
297
- const user = userEvent.setup();
298
286
  mockUseParams.mockReturnValue({ id: null });
287
+ const searchParams = new URLSearchParams('tab=leads');
288
+ const mockSetSearchParams = vi.fn();
289
+ mockUseSearchParams.mockReturnValue([searchParams, mockSetSearchParams]);
290
+ const user = userEvent.setup();
299
291
  render(_jsx(Wrapper, { children: _jsx(DossierEditor, {}) }));
300
- // Fill title
301
- const titleInput = screen.getByTestId('dossier-title');
302
- await user.type(titleInput, 'Test Title');
303
- // Add query and trigger search
304
- const queryInput = screen.getByTestId('query-input');
305
- await user.click(queryInput);
306
- await user.keyboard('test query');
307
- await user.keyboard('{Enter}');
292
+ await act(async () => {
293
+ // Fill title
294
+ const titleInput = screen.getByTestId('dossier-title');
295
+ await user.type(titleInput, 'Test Title');
296
+ // Add query and trigger search
297
+ const queryInput = screen.getByTestId('query-input');
298
+ await user.click(queryInput);
299
+ await user.keyboard('test query');
300
+ await user.keyboard('{Enter}');
301
+ });
308
302
  await waitFor(() => {
309
303
  expect(screen.getByTestId('query-result-text')).toBeInTheDocument();
310
304
  expect(screen.getByTestId('create-lead-alert')).toBeInTheDocument();
@@ -10,13 +10,12 @@ import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/Fl
10
10
  import HitSummary from '@cccsaurora/howler-ui/components/elements/hit/HitSummary';
11
11
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
12
12
  import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
13
- import dayjs from 'dayjs';
14
- import { has, isNull } from 'lodash-es';
13
+ import { isNull } from 'lodash-es';
15
14
  import { memo, useCallback, useEffect, useMemo, useState } from 'react';
16
15
  import { Trans, useTranslation } from 'react-i18next';
17
- import { useLocation, useParams, useSearchParams } from 'react-router-dom';
16
+ import { useLocation, useParams } from 'react-router-dom';
18
17
  import { useContextSelector } from 'use-context-selector';
19
- import { DEFAULT_QUERY, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
18
+ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
20
19
  import InformationPane from './InformationPane';
21
20
  import SearchPane from './SearchPane';
22
21
  import HitGrid from './grid/HitGrid';
@@ -27,13 +26,12 @@ const Wrapper = memo(({ show, showDrawer, children, onClose }) => {
27
26
  const HitBrowser = () => {
28
27
  const { t } = useTranslation();
29
28
  const theme = useTheme();
30
- const views = useContextSelector(ViewContext, ctx => ctx.views);
31
29
  const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
32
30
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
33
31
  const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
34
- const query = useContextSelector(ParameterContext, ctx => ctx.query);
35
32
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
36
33
  const setOffset = useContextSelector(ParameterContext, ctx => ctx.setOffset);
34
+ const selectedViews = useContextSelector(ParameterContext, ctx => ctx.views);
37
35
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
38
36
  const addHitToSelection = useContextSelector(HitContext, ctx => ctx.addHitToSelection);
39
37
  const removeHitFromSelection = useContextSelector(HitContext, ctx => ctx.removeHitFromSelection);
@@ -41,29 +39,12 @@ const HitBrowser = () => {
41
39
  const searchPaneWidth = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null)[0];
42
40
  const forceDrawer = useMyLocalStorageItem(StorageKey.FORCE_DRAWER, false)[0];
43
41
  const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
44
- const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
45
42
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
46
- const queryHistory = useContextSelector(HitSearchContext, ctx => ctx?.queryHistory ?? {});
47
- const setQueryHistory = useContextSelector(HitSearchContext, ctx => ctx?.setQueryHistory);
48
- const setQueryList = useMyLocalStorageItem(StorageKey.QUERY_HISTORY, '')[1];
49
43
  const location = useLocation();
50
44
  const routeParams = useParams();
51
- const [searchParams] = useSearchParams();
52
45
  const [show, setShow] = useState(!!selected);
53
46
  useEffect(() => setShow(!!selected), [selected]);
54
47
  const showDrawer = useMediaQuery(theme.breakpoints.down(1600)) || forceDrawer || displayType === 'grid';
55
- // State that makes up the request
56
- const summaryQuery = useMemo(() => {
57
- const bundle = location.pathname.startsWith('/bundles') && routeParams.id;
58
- let _fullQuery = query;
59
- if (bundle) {
60
- _fullQuery = `(howler.bundles:${bundle}) AND (${_fullQuery})`;
61
- }
62
- else if (viewId) {
63
- _fullQuery = `(${views[viewId]?.query || DEFAULT_QUERY}) AND (${_fullQuery})`;
64
- }
65
- return _fullQuery;
66
- }, [location.pathname, query, routeParams.id, views, viewId]);
67
48
  const showSelectBar = useMemo(() => {
68
49
  if (selectedHits.length > 1) {
69
50
  return true;
@@ -74,31 +55,9 @@ const HitBrowser = () => {
74
55
  return false;
75
56
  }, [selected, selectedHits]);
76
57
  useEffect(() => {
77
- const newQuery = searchParams.get('query');
78
- if (newQuery) {
79
- setQueryHistory(_queryHistory => ({
80
- ..._queryHistory,
81
- [newQuery]: new Date().toISOString()
82
- }));
83
- }
84
- }, [searchParams, setQueryHistory]);
85
- useEffect(() => {
86
- setQueryList(JSON.stringify(queryHistory));
87
- }, [queryHistory, setQueryList]);
88
- useEffect(() => {
89
- // On load check to filter out any queries older than one month
90
- setQueryHistory(_queryHistory => {
91
- const filterQueryTime = dayjs().subtract(1, 'month').toISOString();
92
- return Object.fromEntries(Object.entries(_queryHistory).filter(([_, value]) => value > filterQueryTime));
93
- });
94
- }, [setQueryHistory]);
95
- useEffect(() => {
96
- if (!location.pathname.startsWith('/views') || !viewId || has(views, viewId)) {
97
- return;
98
- }
99
- fetchViews([viewId]);
58
+ fetchViews(selectedViews);
100
59
  // eslint-disable-next-line react-hooks/exhaustive-deps
101
- }, [location.pathname, viewId]);
60
+ }, [location.pathname, location.search]);
102
61
  const onClose = useCallback(() => {
103
62
  setSelected(null);
104
63
  }, [setSelected]);
@@ -132,7 +91,7 @@ const HitBrowser = () => {
132
91
  }, children: _jsx(Close, {}) }) }), _jsx(Tooltip, { title: t('hit.search.select.view'), children: _jsx(IconButton, { size: "small", onClick: () => {
133
92
  setOffset(0);
134
93
  setQuery(`howler.id:(${selectedHits.map(hit => hit.howler.id).join(' OR ')})`);
135
- }, children: _jsx(ManageSearch, {}) }) })] }) })] }), _jsxs(Wrapper, { show: show, showDrawer: showDrawer, onClose: () => setShow(false), children: [_jsx(HitSummary, { query: summaryQuery, response: response }), _jsxs(Card, { variant: "outlined", sx: [
94
+ }, children: _jsx(ManageSearch, {}) }) })] }) })] }), _jsxs(Wrapper, { show: show, showDrawer: showDrawer, onClose: () => setShow(false), children: [_jsx(HitSummary, { response: response }), _jsxs(Card, { variant: "outlined", sx: [
136
95
  {
137
96
  zIndex: 100,
138
97
  overflow: 'visible',
@@ -1,23 +1,13 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- /* eslint-disable react/jsx-no-literals */
3
- /* eslint-disable import/imports-first */
4
- /// <reference types="vitest" />
5
2
  import { fireEvent, render, screen, waitFor } from '@testing-library/react';
6
3
  import userEvent, {} from '@testing-library/user-event';
7
4
  import { omit } from 'lodash-es';
8
- import { act, createContext, useContext } from 'react';
5
+ import { act } from 'react';
6
+ import { setupContextSelectorMock } from '@cccsaurora/howler-ui/tests/mocks';
9
7
  import { vi } from 'vitest';
10
- globalThis.IS_REACT_ACT_ENVIRONMENT = true;
11
8
  // Mock API
12
9
  vi.mock('api', { spy: true });
13
- vi.mock('use-context-selector', async () => {
14
- return {
15
- createContext,
16
- useContextSelector: (context, selector) => {
17
- return selector(useContext(context));
18
- }
19
- };
20
- });
10
+ setupContextSelectorMock();
21
11
  // Mock react-router-dom
22
12
  const mockNavigate = vi.fn();
23
13
  vi.mock('react-router-dom', async () => {
@@ -140,10 +130,8 @@ describe('HitContextMenu', () => {
140
130
  });
141
131
  describe('Context Menu Initialization', () => {
142
132
  it('should open menu on right-click', async () => {
143
- act(() => {
144
- const contextMenuWrapper = screen.getByText('Test Content').parentElement;
145
- fireEvent.contextMenu(contextMenuWrapper);
146
- });
133
+ const contextMenuWrapper = screen.getByText('Test Content').parentElement;
134
+ await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
147
135
  await waitFor(() => {
148
136
  expect(screen.getByRole('menu')).toBeInTheDocument();
149
137
  });
@@ -644,12 +632,10 @@ describe('HitContextMenu', () => {
644
632
  mockGetSelectedId.mockReturnValue('hit-1');
645
633
  });
646
634
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
647
- fireEvent.contextMenu(contextMenuWrapper);
648
- await waitFor(() => {
649
- expect(screen.getByRole('menu')).toBeInTheDocument();
650
- });
635
+ await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
651
636
  // The component should use selectedHits for actions
652
637
  // We can verify this indirectly through the useHitActions hook receiving the right data
638
+ expect(screen.getByRole('menu')).toBeInTheDocument();
653
639
  expect(mockGetSelectedId).toHaveBeenCalled();
654
640
  });
655
641
  it('should use only current hit when not in selectedHits', async () => {
@@ -659,7 +645,7 @@ describe('HitContextMenu', () => {
659
645
  mockGetSelectedId.mockReturnValue('test-hit-1');
660
646
  });
661
647
  const contextMenuWrapper = screen.getByText('Test Content').parentElement;
662
- fireEvent.contextMenu(contextMenuWrapper);
648
+ await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
663
649
  await waitFor(() => {
664
650
  expect(screen.getByRole('menu')).toBeInTheDocument();
665
651
  });
@@ -668,9 +654,9 @@ describe('HitContextMenu', () => {
668
654
  });
669
655
  describe('Dynamic Data Loading', () => {
670
656
  let contextMenuWrapper;
671
- beforeEach(() => {
657
+ beforeEach(async () => {
672
658
  contextMenuWrapper = screen.getByText('Test Content').parentElement;
673
- fireEvent.contextMenu(contextMenuWrapper);
659
+ await user.pointer({ keys: '[MouseRight]', target: contextMenuWrapper });
674
660
  });
675
661
  it('should call getMatchingAnalytic when hit has analytic', async () => {
676
662
  await waitFor(() => {
@@ -694,11 +680,7 @@ describe('HitContextMenu', () => {
694
680
  await waitFor(() => {
695
681
  expect(screen.getByTestId('assessment-submenu')).toBeInTheDocument();
696
682
  });
697
- await act(async () => {
698
- // Close menu
699
- const menu = screen.getByRole('menu');
700
- await user.click(menu);
701
- });
683
+ await user.click(screen.getByRole('menu'));
702
684
  await waitFor(() => {
703
685
  expect(screen.queryByRole('menu')).not.toBeInTheDocument();
704
686
  });
@@ -151,7 +151,7 @@ const InformationPane = ({ onClose }) => {
151
151
  hit_raw: () => _jsx(JSONViewer, { data: !loading && hit, hideSearch: true, filter: filter }),
152
152
  hit_data: () => (_jsx(JSONViewer, { data: !loading && hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false, hideSearch: true, filter: filter })),
153
153
  hit_worklog: () => _jsx(HitWorklog, { hit: !loading && hit, users: users }),
154
- hit_aggregate: () => _jsx(HitSummary, { query: `howler.bundles:(${hit?.howler?.id})` }),
154
+ hit_aggregate: () => _jsx(HitSummary, {}),
155
155
  hit_related: () => _jsx(HitRelated, { hit: hit }),
156
156
  ...Object.fromEntries((hit?.howler.dossier ?? []).map((lead, index) => [
157
157
  'lead:' + index,
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Add, ArrowDropDown } from '@mui/icons-material';
3
+ import { Box, Button, Grid, Stack } from '@mui/material';
4
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
5
+ import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
6
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
7
+ import { memo, useMemo } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { useContextSelector } from 'use-context-selector';
10
+ import HitFilter from './shared/HitFilter';
11
+ import HitSort from './shared/HitSort';
12
+ import SearchSpan from './shared/SearchSpan';
13
+ import ViewLink from './ViewLink';
14
+ const QuerySettings = ({ boxSx }) => {
15
+ const { t } = useTranslation();
16
+ const views = useContextSelector(ViewContext, ctx => ctx.views);
17
+ const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
18
+ const currentViews = useContextSelector(ParameterContext, ctx => ctx.views);
19
+ const filters = useContextSelector(ParameterContext, ctx => ctx.filters);
20
+ const addFilter = useContextSelector(ParameterContext, ctx => ctx.addFilter);
21
+ const addView = useContextSelector(ParameterContext, ctx => ctx.addView);
22
+ const allowAddViews = useMemo(() => Object.values(views).filter(_view => !!_view && !currentViews?.includes(_view.view_id))?.length > 0 &&
23
+ !currentViews?.includes(''), [views, currentViews]);
24
+ const onAddView = async () => {
25
+ await fetchViews();
26
+ addView('');
27
+ };
28
+ return (_jsx(Box, { sx: boxSx ?? { position: 'relative', maxWidth: '1200px' }, children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Grid, { container: true, spacing: 1, sx: theme => ({ ml: `${theme.spacing(-1)} !important`, mt: `${theme.spacing(-1)} !important` }), children: [_jsx(Grid, { item: true, children: _jsx(HitSort, {}) }), _jsx(Grid, { item: true, children: _jsx(SearchSpan, {}) }), currentViews?.map((view, id) => (_jsx(Grid, { item: true, children: _jsx(ViewLink, { id: id, viewId: view }) }, view))), filters?.map((filter, id) => (_jsx(Grid, { item: true, children: _jsx(HitFilter, { id: id, value: filter }) }, filter)))] }), _jsx(ChipPopper, { icon: _jsx(Add, {}), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, closeOnClick: true, slotProps: { chip: { size: 'small', color: 'primary' }, paper: { sx: { p: 1 } } }, children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Button, { id: "add-filter", "aria-label": t('hit.search.filter.add'), variant: "outlined", onClick: () => addFilter('howler.assessment:*'), disabled: filters?.some(filter => filter.endsWith('*')), sx: { display: 'flex', pl: 1 }, children: [_jsx(Add, { fontSize: "small", sx: { mr: 1 } }), _jsx("div", { style: { flex: 1 } }), _jsx("span", { children: t('hit.search.filter.add') })] }), _jsxs(Button, { id: "add-view", "aria-label": t('hit.search.view.add'), variant: "outlined", onClick: onAddView, disabled: !allowAddViews, sx: { display: 'flex', pl: 1 }, children: [_jsx(Add, { fontSize: "small", sx: { mr: 1 } }), _jsx("div", { style: { flex: 1 } }), _jsx("span", { children: t('hit.search.view.add') })] })] }) })] }) }));
29
+ };
30
+ export default memo(QuerySettings);