@cccsaurora/howler-ui 2.16.0-dev.378 → 2.16.0-dev.381
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 +2 -0
- package/components/app/hooks/useMatchers.js +0 -4
- package/components/app/providers/FavouritesProvider.js +2 -1
- package/components/app/providers/FieldProvider.d.ts +2 -2
- package/components/app/providers/HitProvider.d.ts +3 -3
- package/components/app/providers/HitSearchProvider.d.ts +7 -8
- package/components/app/providers/HitSearchProvider.js +64 -39
- package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
- package/components/app/providers/HitSearchProvider.test.js +505 -0
- package/components/app/providers/ParameterProvider.d.ts +13 -5
- package/components/app/providers/ParameterProvider.js +240 -84
- package/components/app/providers/ParameterProvider.test.d.ts +1 -0
- package/components/app/providers/ParameterProvider.test.js +1041 -0
- package/components/app/providers/ViewProvider.d.ts +3 -2
- package/components/app/providers/ViewProvider.js +21 -14
- package/components/app/providers/ViewProvider.test.js +19 -29
- package/components/elements/display/ChipPopper.d.ts +21 -0
- package/components/elements/display/ChipPopper.js +36 -0
- package/components/elements/display/ChipPopper.test.d.ts +1 -0
- package/components/elements/display/ChipPopper.test.js +309 -0
- package/components/elements/hit/HitActions.js +3 -3
- package/components/elements/hit/HitSummary.d.ts +0 -1
- package/components/elements/hit/HitSummary.js +11 -21
- package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
- package/components/elements/hit/aggregate/HitGraph.js +9 -15
- package/components/routes/dossiers/DossierCard.test.js +0 -2
- package/components/routes/dossiers/DossierEditor.test.js +27 -33
- package/components/routes/hits/search/HitBrowser.js +7 -48
- package/components/routes/hits/search/HitContextMenu.test.js +11 -29
- package/components/routes/hits/search/InformationPane.js +1 -1
- package/components/routes/hits/search/QuerySettings.js +30 -0
- package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
- package/components/routes/hits/search/QuerySettings.test.js +553 -0
- package/components/routes/hits/search/SearchPane.js +8 -10
- package/components/routes/hits/search/ViewLink.d.ts +4 -1
- package/components/routes/hits/search/ViewLink.js +37 -19
- package/components/routes/hits/search/ViewLink.test.js +349 -303
- package/components/routes/hits/search/grid/HitGrid.js +2 -6
- package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
- package/components/routes/hits/search/shared/HitFilter.js +31 -23
- package/components/routes/hits/search/shared/HitSort.js +16 -8
- package/components/routes/hits/search/shared/SearchSpan.js +19 -10
- package/components/routes/views/ViewComposer.js +7 -6
- package/components/routes/views/Views.js +2 -1
- package/locales/en/translation.json +6 -0
- package/locales/fr/translation.json +6 -0
- package/package.json +2 -2
- package/setupTests.js +4 -1
- package/tests/mocks.d.ts +18 -0
- package/tests/mocks.js +65 -0
- package/tests/server-handlers.js +10 -28
- package/utils/viewUtils.d.ts +2 -0
- package/utils/viewUtils.js +11 -0
- package/components/routes/hits/search/shared/QuerySettings.js +0 -22
- /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
- /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
- /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 {
|
|
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 = ({
|
|
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
|
|
32
|
-
const
|
|
33
|
-
const
|
|
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
|
-
|
|
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 &&
|
|
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,
|
|
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, {
|
|
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'),
|
|
@@ -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 {
|
|
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 = (
|
|
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
|
|
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,
|
|
110
|
+
}, [dispatchApi, escalationFilter, filterField, getFilters, override, query]);
|
|
117
111
|
useEffect(() => {
|
|
118
|
-
if ((!query &&
|
|
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,
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
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
|
|
16
|
+
import { useLocation, useParams } from 'react-router-dom';
|
|
18
17
|
import { useContextSelector } from 'use-context-selector';
|
|
19
|
-
import {
|
|
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
|
-
|
|
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,
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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, {
|
|
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);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|