@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.
Files changed (57) hide show
  1. package/components/app/App.js +2 -0
  2. package/components/app/hooks/useMatchers.js +0 -4
  3. package/components/app/providers/FavouritesProvider.js +2 -1
  4. package/components/app/providers/FieldProvider.d.ts +2 -2
  5. package/components/app/providers/HitProvider.d.ts +3 -3
  6. package/components/app/providers/HitSearchProvider.d.ts +7 -8
  7. package/components/app/providers/HitSearchProvider.js +64 -39
  8. package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
  9. package/components/app/providers/HitSearchProvider.test.js +505 -0
  10. package/components/app/providers/ParameterProvider.d.ts +13 -5
  11. package/components/app/providers/ParameterProvider.js +240 -84
  12. package/components/app/providers/ParameterProvider.test.d.ts +1 -0
  13. package/components/app/providers/ParameterProvider.test.js +1041 -0
  14. package/components/app/providers/ViewProvider.d.ts +3 -2
  15. package/components/app/providers/ViewProvider.js +21 -14
  16. package/components/app/providers/ViewProvider.test.js +19 -29
  17. package/components/elements/display/ChipPopper.d.ts +21 -0
  18. package/components/elements/display/ChipPopper.js +36 -0
  19. package/components/elements/display/ChipPopper.test.d.ts +1 -0
  20. package/components/elements/display/ChipPopper.test.js +309 -0
  21. package/components/elements/hit/HitActions.js +3 -3
  22. package/components/elements/hit/HitSummary.d.ts +0 -1
  23. package/components/elements/hit/HitSummary.js +11 -21
  24. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
  25. package/components/elements/hit/aggregate/HitGraph.js +9 -15
  26. package/components/routes/dossiers/DossierCard.test.js +0 -2
  27. package/components/routes/dossiers/DossierEditor.test.js +27 -33
  28. package/components/routes/hits/search/HitBrowser.js +7 -48
  29. package/components/routes/hits/search/HitContextMenu.test.js +11 -29
  30. package/components/routes/hits/search/InformationPane.js +1 -1
  31. package/components/routes/hits/search/QuerySettings.js +30 -0
  32. package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
  33. package/components/routes/hits/search/QuerySettings.test.js +553 -0
  34. package/components/routes/hits/search/SearchPane.js +8 -10
  35. package/components/routes/hits/search/ViewLink.d.ts +4 -1
  36. package/components/routes/hits/search/ViewLink.js +37 -19
  37. package/components/routes/hits/search/ViewLink.test.js +349 -303
  38. package/components/routes/hits/search/grid/HitGrid.js +2 -6
  39. package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
  40. package/components/routes/hits/search/shared/HitFilter.js +31 -23
  41. package/components/routes/hits/search/shared/HitSort.js +16 -8
  42. package/components/routes/hits/search/shared/SearchSpan.js +19 -10
  43. package/components/routes/views/ViewComposer.js +7 -6
  44. package/components/routes/views/Views.js +2 -1
  45. package/locales/en/translation.json +6 -0
  46. package/locales/fr/translation.json +6 -0
  47. package/package.json +2 -2
  48. package/setupTests.js +4 -1
  49. package/tests/mocks.d.ts +18 -0
  50. package/tests/mocks.js +65 -0
  51. package/tests/server-handlers.js +10 -28
  52. package/utils/viewUtils.d.ts +2 -0
  53. package/utils/viewUtils.js +11 -0
  54. package/components/routes/hits/search/shared/QuerySettings.js +0 -22
  55. /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
  56. /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
  57. /package/components/routes/hits/search/{CustomSort.js → shared/CustomSort.js} +0 -0
@@ -7,7 +7,6 @@ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers'
7
7
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
8
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
9
9
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
10
- import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
11
10
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
12
11
  import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
13
12
  import DevelopmentBanner from '@cccsaurora/howler-ui/components/elements/display/features/DevelopmentBanner';
@@ -20,8 +19,7 @@ import { useContextSelector } from 'use-context-selector';
20
19
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
21
20
  import HitContextMenu from '../HitContextMenu';
22
21
  import HitQuery from '../HitQuery';
23
- import QuerySettings from '../shared/QuerySettings';
24
- import ViewLink from '../ViewLink';
22
+ import QuerySettings from '../QuerySettings';
25
23
  import AddColumnModal from './AddColumnModal';
26
24
  import ColumnHeader from './ColumnHeader';
27
25
  import HitRow from './HitRow';
@@ -36,11 +34,9 @@ const HitGrid = () => {
36
34
  const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
37
35
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
38
36
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
39
- const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
40
37
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
41
38
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
42
39
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
43
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
44
40
  const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
45
41
  const [analyticIds, setAnalyticIds] = useState({});
46
42
  const columnModalRef = useRef();
@@ -124,7 +120,7 @@ const HitGrid = () => {
124
120
  }
125
121
  return selectedElement.id;
126
122
  }, []);
127
- return (_jsxs(Stack, { spacing: 1, p: 2, width: "100%", sx: { overflow: 'hidden', height: `calc(100vh - ${theme.spacing(showSelectBar ? 13 : 8)})` }, children: [_jsx(DevelopmentBanner, {}), _jsx(ViewLink, {}), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5, textAlign: 'left' }, variant: "body2", children: t('hit.search.prompt') }), _jsx(DevelopmentIcon, {})] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { position: "relative", flex: 1, children: [_jsx(HitQuery, { disabled: viewId && !selectedView, searching: searching, triggerSearch: search, compact: true }), searching && (_jsx(LinearProgress, { sx: {
123
+ return (_jsxs(Stack, { spacing: 1, p: 2, width: "100%", sx: { overflow: 'hidden', height: `calc(100vh - ${theme.spacing(showSelectBar ? 13 : 8)})` }, children: [_jsx(DevelopmentBanner, {}), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5, textAlign: 'left' }, variant: "body2", children: t('hit.search.prompt') }), _jsx(DevelopmentIcon, {})] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { position: "relative", flex: 1, children: [_jsx(HitQuery, { searching: searching, triggerSearch: search, compact: true }), searching && (_jsx(LinearProgress, { sx: {
128
124
  position: 'absolute',
129
125
  left: 0,
130
126
  right: 0,
@@ -1,4 +1,6 @@
1
1
  declare const _default: import("react").NamedExoticComponent<{
2
2
  size?: "small" | "medium";
3
+ id: number;
4
+ value: string;
3
5
  }>;
4
6
  export default _default;
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Autocomplete, Stack, TextField } from '@mui/material';
2
+ import { FilterList } from '@mui/icons-material';
3
+ import { Autocomplete, Stack, TextField, Typography } from '@mui/material';
3
4
  import api from '@cccsaurora/howler-ui/api';
4
5
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
6
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
7
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
8
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
6
9
  import { memo, useCallback, useContext, useEffect, useState } from 'react';
7
10
  import { useTranslation } from 'react-i18next';
8
11
  import { useContextSelector } from 'use-context-selector';
@@ -15,50 +18,55 @@ const ACCEPTED_LOOKUPS = [
15
18
  'event.provider',
16
19
  'organization.name'
17
20
  ];
18
- const HitFilter = ({ size }) => {
21
+ const HitFilter = ({ size, id, value }) => {
19
22
  const { t } = useTranslation();
20
23
  const { config } = useContext(ApiConfigContext);
21
- const savedFilter = useContextSelector(ParameterContext, ctx => ctx.filter);
24
+ const { dispatchApi } = useMyApi();
22
25
  const setSavedFilter = useContextSelector(ParameterContext, ctx => ctx.setFilter);
23
- const [category, setCategory] = useState(ACCEPTED_LOOKUPS[0]);
24
- const [filter, setFilter] = useState('');
26
+ const removeSavedFilter = useContextSelector(ParameterContext, ctx => ctx.removeFilter);
27
+ const [category, setCategory] = useState(value?.split(':')[0] ?? ACCEPTED_LOOKUPS[0]);
28
+ const [filter, setFilter] = useState(value?.split(':')[1] ?? null);
29
+ const [loading, setLoading] = useState(false);
25
30
  const [customLookups, setCustomLookups] = useState([]);
26
31
  useEffect(() => {
27
- if (savedFilter) {
28
- const [_category, _filter] = (savedFilter || ':').split(':');
32
+ if (value) {
33
+ const [_category, _filter] = (value || ':').split(':');
29
34
  if (_category) {
30
35
  setCategory(_category);
31
36
  }
32
- if (_filter) {
37
+ if (_filter && _filter !== '*') {
33
38
  setFilter(_filter);
34
39
  }
35
40
  if (_category && _filter) {
36
- setSavedFilter(`${_category}:${_filter}`);
41
+ setSavedFilter(id, `${_category}:${_filter}`);
37
42
  }
38
43
  }
39
- }, [setSavedFilter, savedFilter]);
44
+ }, [id, setSavedFilter, value]);
40
45
  const onCategoryChange = useCallback(async (_, _category) => {
41
46
  setCategory(_category);
42
- setFilter('');
43
- setSavedFilter(null);
47
+ setFilter(null);
44
48
  if (!config.lookups[_category]) {
45
- const facets = await api.search.facet.hit.post({ query: 'howler.id:*', fields: [_category] });
46
- setCustomLookups(Object.keys(facets[_category]));
49
+ setLoading(true);
50
+ const facets = await dispatchApi(api.search.facet.hit.post({ query: 'howler.id:*', fields: [_category], rows: 100 }), {
51
+ throwError: false
52
+ });
53
+ setCustomLookups(Object.keys((facets ?? {})[_category]));
54
+ setLoading(false);
47
55
  }
48
56
  else {
49
57
  setCustomLookups([]);
50
58
  }
51
- }, [config.lookups, setSavedFilter]);
52
- const onValueChange = useCallback((_, value) => {
53
- setFilter(value);
54
- if (value) {
55
- const newFilter = `${category}:"${sanitizeLuceneQuery(value)}"`;
56
- setSavedFilter(newFilter);
59
+ }, [config.lookups, dispatchApi]);
60
+ const onValueChange = useCallback((_, newValue) => {
61
+ setFilter(newValue);
62
+ if (newValue && newValue !== '*') {
63
+ setSavedFilter(id, `${category}:"${sanitizeLuceneQuery(newValue)}"`);
57
64
  }
58
65
  else {
59
- setSavedFilter(null);
66
+ setSavedFilter(id, `${category}:*`);
60
67
  }
61
- }, [category, setSavedFilter]);
62
- return (_jsxs(Stack, { direction: "row", spacing: 1, sx: { flex: 1.75 }, children: [_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '200px' }, size: size ?? 'small', value: category, options: ACCEPTED_LOOKUPS, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.filter.fields') }), onChange: onCategoryChange }), _jsx(Autocomplete, { fullWidth: true, freeSolo: true, sx: { minWidth: '150px' }, disabled: !category, size: size ?? 'small', value: filter?.replaceAll('"', '').replaceAll('\\-', '-') || '', options: config.lookups[category] ? config.lookups[category] : customLookups, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.filter.values') }), getOptionLabel: option => t(option), onChange: onValueChange })] }));
68
+ }, [category, id, setSavedFilter]);
69
+ const filterValue = filter?.replaceAll('"', '').replaceAll('\\-', '-') || '';
70
+ return (_jsx(ChipPopper, { icon: _jsx(FilterList, { fontSize: "small" }), label: category && _jsx(Typography, { variant: "body2", children: `${category}:${filterValue || '*'}` }), minWidth: "250px", onDelete: () => removeSavedFilter(value), slotProps: { chip: { size: 'small', color: value?.endsWith('*') ? 'warning' : 'default' } }, children: _jsxs(Stack, { spacing: 1, sx: { minWidth: '225px' }, children: [_jsx(Autocomplete, { fullWidth: true, size: size ?? 'small', value: category, options: ACCEPTED_LOOKUPS, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.filter.fields') }), onChange: onCategoryChange }), _jsx(Autocomplete, { fullWidth: true, freeSolo: true, disabled: !category, loading: loading, size: size ?? 'small', value: filter?.replaceAll('"', '').replaceAll('\\-', '-') || '', options: [...(config.lookups[category] ? config.lookups[category] : customLookups), '*'], renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.filter.values') }), getOptionLabel: option => t(option), onChange: onValueChange })] }) }));
63
71
  };
64
72
  export default memo(HitFilter);
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Autocomplete, MenuItem, Select, Stack, TextField } from '@mui/material';
2
+ import { Sort } from '@mui/icons-material';
3
+ import { Autocomplete, Stack, TextField } from '@mui/material';
3
4
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
4
5
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
6
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
5
7
  import { memo, useCallback, useEffect, useMemo, useState } from 'react';
6
8
  import { useTranslation } from 'react-i18next';
7
9
  import { useLocation } from 'react-router-dom';
8
10
  import { useContextSelector } from 'use-context-selector';
9
- import CustomSort from '../CustomSort';
11
+ import CustomSort from './CustomSort';
10
12
  const CUSTOM = '__custom__';
11
13
  const ACCEPTED_SORTS = [
12
14
  'event.created',
@@ -21,7 +23,8 @@ const ACCEPTED_SORTS = [
21
23
  const HitSort = ({ size = 'small' }) => {
22
24
  const { t } = useTranslation();
23
25
  const location = useLocation();
24
- const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
26
+ const getCurrentViews = useContextSelector(ViewContext, ctx => ctx.getCurrentViews);
27
+ const views = useContextSelector(ParameterContext, ctx => ctx.views);
25
28
  const savedSort = useContextSelector(ParameterContext, ctx => ctx.sort);
26
29
  const setSavedSort = useContextSelector(ParameterContext, ctx => ctx.setSort);
27
30
  const sortEntries = useMemo(() => savedSort.split(',').filter(part => !!part), [savedSort]);
@@ -53,13 +56,18 @@ const HitSort = ({ size = 'small' }) => {
53
56
  return;
54
57
  }
55
58
  (async () => {
56
- const selectedView = await getCurrentView({ lazy: true });
57
- if (selectedView?.sort && !location.search.includes('sort')) {
58
- setSavedSort(selectedView.sort);
59
+ const selectedViewSort = (await getCurrentViews({ lazy: true })).find(view => view?.sort)?.sort;
60
+ if (selectedViewSort && !location.search.includes('sort')) {
61
+ setSavedSort(selectedViewSort);
59
62
  }
60
63
  })();
61
64
  // eslint-disable-next-line react-hooks/exhaustive-deps
62
- }, [getCurrentView]);
63
- return !showCustomSort ? (_jsxs(Stack, { direction: "row", spacing: 1, sx: { flex: 1.5 }, children: [_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '175px' }, size: size, value: field, options: ACCEPTED_SORTS, getOptionLabel: option => (option === CUSTOM ? t('hit.search.custom') : option), isOptionEqualToValue: (option, value) => option === value || (!value && option === ACCEPTED_SORTS[0]), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.sort.fields') }), onChange: (_, value) => handleChange(value) }), _jsxs(Select, { size: size, sx: { minWidth: '150px' }, value: sort, onChange: e => setSavedSort(`${field} ${e.target.value}`), children: [_jsx(MenuItem, { value: "asc", children: t('asc') }), _jsx(MenuItem, { value: "desc", children: t('desc') })] })] })) : (_jsx(CustomSort, {}));
65
+ }, [getCurrentViews, views]);
66
+ return (_jsx(ChipPopper, { icon: _jsx(Sort, { fontSize: "small" }), label: savedSort, slotProps: { chip: { size: 'small' } }, children: !showCustomSort ? (_jsxs(Stack, { spacing: 1, onClick: e => {
67
+ e.stopPropagation();
68
+ e.preventDefault();
69
+ }, children: [_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '275px' }, size: size, value: field, options: ACCEPTED_SORTS, getOptionLabel: option => (option === CUSTOM ? t('hit.search.custom') : option), isOptionEqualToValue: (option, value) => option === value || (!value && option === ACCEPTED_SORTS[0]), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.sort.fields') }), onChange: (_, value) => handleChange(value) }), _jsx(Autocomplete, { fullWidth: true, size: size, sx: { minWidth: '150px' }, value: sort, onChange: (_e, value) => {
70
+ setSavedSort(`${field} ${value}`);
71
+ }, options: ['asc', 'desc'], getOptionLabel: option => t(option), renderInput: _params => _jsx(TextField, { ..._params }) })] })) : (_jsx(CustomSort, {})) }));
64
72
  };
65
73
  export default memo(HitSort);
@@ -1,12 +1,16 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Autocomplete, TextField } from '@mui/material';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AvTimer } from '@mui/icons-material';
3
+ import { Autocomplete, Stack, TextField, Typography } from '@mui/material';
3
4
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
4
5
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
6
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
7
+ import dayjs from 'dayjs';
5
8
  import { memo, useEffect } from 'react';
6
9
  import { useTranslation } from 'react-i18next';
7
10
  import { useLocation } from 'react-router-dom';
8
11
  import { useContextSelector } from 'use-context-selector';
9
12
  import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
13
+ import CustomSpan from './CustomSpan';
10
14
  const DATE_RANGES = [
11
15
  'date.range.1.day',
12
16
  'date.range.3.day',
@@ -18,27 +22,32 @@ const DATE_RANGES = [
18
22
  const SearchSpan = ({ omitCustom = false, size }) => {
19
23
  const { t } = useTranslation();
20
24
  const location = useLocation();
25
+ const views = useContextSelector(ParameterContext, ctx => ctx.views);
21
26
  const span = useContextSelector(ParameterContext, ctx => ctx.span);
22
27
  const setSpan = useContextSelector(ParameterContext, ctx => ctx.setSpan);
23
- const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
28
+ const startDate = useContextSelector(ParameterContext, ctx => (ctx.startDate ? dayjs(ctx.startDate) : null));
29
+ const endDate = useContextSelector(ParameterContext, ctx => (ctx.endDate ? dayjs(ctx.endDate) : null));
30
+ const getCurrentViews = useContextSelector(ViewContext, ctx => ctx.getCurrentViews);
24
31
  useEffect(() => {
25
32
  if (location.search.includes('span')) {
26
33
  return;
27
34
  }
28
35
  (async () => {
29
- const viewSpan = (await getCurrentView({ lazy: true }))?.span;
30
- if (!viewSpan) {
36
+ const selectedViewSpan = (await getCurrentViews({ lazy: true })).find(view => view?.span)?.span;
37
+ if (!selectedViewSpan) {
31
38
  return;
32
39
  }
33
- if (viewSpan.includes(':')) {
34
- setSpan(convertLuceneToDate(viewSpan));
40
+ if (selectedViewSpan.includes(':')) {
41
+ setSpan(convertLuceneToDate(selectedViewSpan));
35
42
  }
36
43
  else {
37
- setSpan(viewSpan);
44
+ setSpan(selectedViewSpan);
38
45
  }
39
46
  })();
40
47
  // eslint-disable-next-line react-hooks/exhaustive-deps
41
- }, [getCurrentView]);
42
- return (_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '200px', flex: 1 }, size: size ?? 'small', value: span, options: omitCustom ? DATE_RANGES.slice(0, DATE_RANGES.length - 1) : DATE_RANGES, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.span') }), getOptionLabel: option => t(option), onChange: (_, value) => setSpan(value), disableClearable: true }));
48
+ }, [getCurrentViews, views]);
49
+ return (_jsx(ChipPopper, { icon: _jsx(AvTimer, { fontSize: "small" }), label: _jsx(Typography, { variant: "body2", children: span !== 'date.range.custom'
50
+ ? t(span)
51
+ : `${startDate.format('YYYY-MM-DD HH:mm')} ${t('to')} ${endDate.format('YYYY-MM-DD HH:mm')}` }), minWidth: "225px", slotProps: { chip: { size: 'small' } }, children: _jsxs(Stack, { spacing: 1, children: [_jsx(Autocomplete, { fullWidth: true, sx: { minWidth: '200px', flex: 1 }, size: size ?? 'small', value: span, options: omitCustom ? DATE_RANGES.slice(0, DATE_RANGES.length - 1) : DATE_RANGES, renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.search.span') }), getOptionLabel: option => t(option), onChange: (_, value) => setSpan(value), disableClearable: true }), _jsx(CustomSpan, {})] }) }));
43
52
  };
44
53
  export default memo(SearchSpan);
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { HelpOutline, Save } from '@mui/icons-material';
5
- import { Alert, Checkbox, CircularProgress, Divider, LinearProgress, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
5
+ import { Alert, Checkbox, CircularProgress, LinearProgress, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
6
6
  import api from '@cccsaurora/howler-ui/api';
7
7
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
8
8
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
@@ -24,6 +24,7 @@ import { useNavigate, useParams } from 'react-router-dom';
24
24
  import { useContextSelector } from 'use-context-selector';
25
25
  import { DEFAULT_QUERY, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
26
26
  import { convertDateToLucene } from '@cccsaurora/howler-ui/utils/utils';
27
+ import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
27
28
  import ErrorBoundary from '../ErrorBoundary';
28
29
  import HitQuery from '../hits/search/HitQuery';
29
30
  import HitSort from '../hits/search/shared/HitSort';
@@ -36,7 +37,7 @@ const ViewComposer = () => {
36
37
  const navigate = useNavigate();
37
38
  const addView = useContextSelector(ViewContext, ctx => ctx.addView);
38
39
  const editView = useContextSelector(ViewContext, ctx => ctx.editView);
39
- const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
40
+ const getCurrentViews = useContextSelector(ViewContext, ctx => ctx.getCurrentViews);
40
41
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
41
42
  const loadHits = useContextSelector(HitContext, ctx => ctx.loadHits);
42
43
  // view state
@@ -69,7 +70,7 @@ const ViewComposer = () => {
69
70
  advance_on_triage: advanceOnTriage
70
71
  }
71
72
  });
72
- navigate(`/views/${newView.view_id}`);
73
+ navigate(buildViewUrl(newView));
73
74
  }
74
75
  else {
75
76
  await editView(routeParams.id, {
@@ -142,7 +143,7 @@ const ViewComposer = () => {
142
143
  return;
143
144
  }
144
145
  (async () => {
145
- const viewToEdit = await getCurrentView();
146
+ const viewToEdit = (await getCurrentViews({ viewId: routeParams.id }))[0];
146
147
  if (!viewToEdit) {
147
148
  setError('route.views.missing');
148
149
  return;
@@ -161,7 +162,7 @@ const ViewComposer = () => {
161
162
  }
162
163
  })();
163
164
  // eslint-disable-next-line react-hooks/exhaustive-deps
164
- }, [routeParams.id, getCurrentView]);
165
+ }, [routeParams.id, getCurrentViews]);
165
166
  return (_jsx(FlexPort, { children: _jsx(ErrorBoundary, { children: _jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(VSBox, { top: 0, children: [_jsx(VSBoxHeader, { pb: 1, children: _jsxs(Stack, { spacing: 1, children: [error && (_jsx(Alert, { variant: "outlined", severity: "error", children: t(error) })), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { label: t('route.views.name'), size: "small", value: title, onChange: e => setTitle(e.target.value), fullWidth: true }), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, onChange: (__, _type) => {
166
167
  if (_type) {
167
168
  setType(_type);
@@ -171,6 +172,6 @@ const ViewComposer = () => {
171
172
  fontSize: '0.9em',
172
173
  fontStyle: 'italic',
173
174
  mb: 0.5
174
- }), variant: "body2", children: t('hit.search.prompt') }), _jsx(HitQuery, { triggerSearch: search, searching: searching, onChange: (_query, isDirty) => setIsSearchDirty(isDirty) }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { '& > :not(.MuiDivider-root)': { flex: 1 } }, divider: _jsx(Divider, { flexItem: true, orientation: "vertical" }), children: [_jsx(HitSort, {}), _jsx(SearchSpan, { omitCustom: true }), _jsxs(Stack, { spacing: 1, direction: "row", alignItems: "center", sx: { flex: '0 !important', minWidth: '300px' }, children: [_jsx(Typography, { component: "span", children: t('view.settings.advance_on_triage') }), _jsx(Tooltip, { title: t('view.settings.advance_on_triage.description'), children: _jsx(HelpOutline, { sx: { fontSize: '16px' } }) }), _jsx(Checkbox, { size: "small", checked: advanceOnTriage, onChange: (_event, checked) => setAdvanceOnTriage(checked) })] })] }), response?.total ? (_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) })) : null, _jsx(LinearProgress, { sx: [!searching && { opacity: 0 }] })] }) }), _jsx(VSBoxContent, { children: _jsxs(Stack, { spacing: 1, children: [!response?.total && _jsx(AppListEmpty, {}), response?.items.map(hit => (_jsx(HitCard, { id: hit.howler.id, layout: HitLayout.DENSE }, hit.howler.id)))] }) })] }) }) }) }));
175
+ }), variant: "body2", children: t('hit.search.prompt') }), _jsx(HitQuery, { triggerSearch: search, searching: searching, onChange: (_query, isDirty) => setIsSearchDirty(isDirty) }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(HitSort, {}), _jsx(SearchSpan, { omitCustom: true }), _jsx("div", { style: { flex: 1 } }), _jsxs(Stack, { spacing: 1, direction: "row", alignItems: "center", sx: { flex: '0 !important', minWidth: '300px' }, children: [_jsx(Typography, { component: "span", children: t('view.settings.advance_on_triage') }), _jsx(Tooltip, { title: t('view.settings.advance_on_triage.description'), children: _jsx(HelpOutline, { sx: { fontSize: '16px' } }) }), _jsx(Checkbox, { size: "small", checked: advanceOnTriage, onChange: (_event, checked) => setAdvanceOnTriage(checked) })] })] }), response?.total ? (_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) })) : null, _jsx(LinearProgress, { sx: [!searching && { opacity: 0 }] })] }) }), _jsx(VSBoxContent, { children: _jsxs(Stack, { spacing: 1, children: [!response?.total && _jsx(AppListEmpty, {}), response?.items.map(hit => (_jsx(HitCard, { id: hit.howler.id, layout: HitLayout.DENSE }, hit.howler.id)))] }) })] }) }) }) }));
175
176
  };
176
177
  export default ViewComposer;
@@ -20,6 +20,7 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
20
20
  import { useContextSelector } from 'use-context-selector';
21
21
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
22
22
  import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
23
+ import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
23
24
  const FIELDS_TO_SEARCH = ['title', 'query', 'sort', 'type', 'owner'];
24
25
  const ViewsBase = () => {
25
26
  const { t } = useTranslation();
@@ -173,7 +174,7 @@ const ViewsBase = () => {
173
174
  }, [offset, favouritesOnly, type]);
174
175
  return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchFilters: _jsx(Stack, { direction: "row", spacing: 1, alignItems: "center", children: _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", value: type, exclusive: true, onChange: (__, _type) => onTypeChange(_type), children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.views.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.views.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.views.search.prompt') }), afterSearch: size(views) > 0 ? (_jsx(Autocomplete, { open: defaultViewOpen, loading: defaultViewLoading, onOpen: onDefaultViewOpen, onClose: () => setDefaultViewOpen(false), options: Object.values(omitBy(views, isNull)), renderOption: ({ key, ...props }, o) => (_createElement("li", { ...props, key: key },
175
176
  _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(o.title) }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: o.query }) })] }))), renderInput: params => (_jsx(TextField, { ...params, label: t('route.views.manager.default'), sx: { minWidth: '300px' } })), filterOptions: (_views, { inputValue }) => _views.filter(v => t(v.title).toLowerCase().includes(inputValue.toLowerCase()) ||
176
- v.query.toLowerCase().includes(inputValue.toLowerCase())), getOptionLabel: (v) => t(v.title), isOptionEqualToValue: (view, value) => view.view_id === value.view_id, value: views[defaultView] ?? null, onChange: (_, option) => setDefaultView(option?.view_id) })) : (_jsx(Skeleton, { variant: "rounded", width: "300px", height: "initial" })), belowSearch: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Checkbox, { size: "small", disabled: user.favourite_views?.length < 1, checked: favouritesOnly, onChange: (_, checked) => setFavouritesOnly(checked) }), _jsx(Typography, { variant: "body1", sx: theme => ({ color: theme.palette.text.disabled }), children: t('route.views.manager.favourites') })] }), renderer: ({ item }, classRenderer) => (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1, transitionProperty: 'border-color', '&:hover': { borderColor: 'primary.main' } }, className: classRenderer(), children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { color: 'inherit', textDecoration: 'none' }, component: Link, to: `/views/${item.item.view_id}`, children: [_jsx(ViewTitle, { ...item.item }), _jsx(FlexOne, {}), ((item.item.owner === user.username && item.item.type !== 'readonly') ||
177
+ v.query.toLowerCase().includes(inputValue.toLowerCase())), getOptionLabel: (v) => t(v.title), isOptionEqualToValue: (view, value) => view.view_id === value.view_id, value: views[defaultView] ?? null, onChange: (_, option) => setDefaultView(option?.view_id) })) : (_jsx(Skeleton, { variant: "rounded", width: "300px", height: "initial" })), belowSearch: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Checkbox, { size: "small", disabled: user.favourite_views?.length < 1, checked: favouritesOnly, onChange: (_, checked) => setFavouritesOnly(checked) }), _jsx(Typography, { variant: "body1", sx: theme => ({ color: theme.palette.text.disabled }), children: t('route.views.manager.favourites') })] }), renderer: ({ item }, classRenderer) => (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1, transitionProperty: 'border-color', '&:hover': { borderColor: 'primary.main' } }, className: classRenderer(), children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { color: 'inherit', textDecoration: 'none' }, component: Link, to: buildViewUrl(item.item), children: [_jsx(ViewTitle, { ...item.item }), _jsx(FlexOne, {}), ((item.item.owner === user.username && item.item.type !== 'readonly') ||
177
178
  (item.item.type === 'global' && user.is_admin)) && (_jsx(Tooltip, { title: t('button.edit'), children: _jsx(IconButton, { component: Link, to: `/views/${item.item.view_id}/edit?query=${item.item.query}`, children: _jsx(Edit, {}) }) })), item.item.owner === user.username && item.item.type !== 'readonly' && (_jsx(Tooltip, { title: t('button.delete'), children: _jsx(IconButton, { onClick: event => onDelete(event, item.item.view_id), children: _jsx(Clear, {}) }) })), item.item.type === 'global' && item.item.owner !== user.username && (_jsx(Tooltip, { title: item.item.owner, children: _jsx("div", { children: _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important', marginLeft: '8px !important' }, userId: item.item.owner }) }) })), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { onClick: e => onFavourite(e, item.item.view_id), children: user.favourite_views?.includes(item.item.view_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }, item.item.view_id)), response: response, searchPrompt: "route.views.manager.search", onCreate: () => navigate('/views/create'), createPrompt: "route.views.create", createIcon: _jsx(SavedSearch, { sx: { mr: 1 } }) }));
178
179
  };
179
180
  const Views = () => {
@@ -4,10 +4,12 @@
4
4
  "adminmenu.users": "Users",
5
5
  "add": "Add",
6
6
  "all": "All",
7
+ "*": "All values",
7
8
  "any": "Any",
8
9
  "asc": "Ascending",
9
10
  "close": "Close",
10
11
  "desc": "Descending",
12
+ "to": "to",
11
13
  "actions.running": "Action \"{{action}}\" is executing.",
12
14
  "actions.succeeded": "Action \"{{action}}\" completed successfully.",
13
15
  "api.user.apikey.updated": "New API key added successfully.",
@@ -246,6 +248,7 @@
246
248
  "hit.search.filter.values": "Values",
247
249
  "hit.search.filter.button": "Use lookup filters with predefined values",
248
250
  "hit.search.filter.label": "Lookup Filters",
251
+ "hit.search.filter.add": "Add filter",
249
252
  "hit.search.save.button": "Save query as view",
250
253
  "hit.search.save.view": "Save View",
251
254
  "hit.search.sort.button": "Sort Results",
@@ -254,8 +257,11 @@
254
257
  "hit.search.select.all": "Select all hits on page",
255
258
  "hit.search.select.clear": "Clear Selection",
256
259
  "hit.search.selected": "{{size}} selected",
260
+ "hit.search.view.add": "Add view",
257
261
  "hit.search.view.current": "Current View",
258
262
  "hit.search.view.default": "(Default)",
263
+ "hit.search.view.select": "Select View",
264
+ "hit.search.view.remove": "Remove View",
259
265
  "hit.viewer.aggregate": "Summary",
260
266
  "hit.viewer.comments": "Comments",
261
267
  "hit.viewer.data": "Raw Data",
@@ -14,10 +14,12 @@
14
14
  "apikey.write": "Écrire",
15
15
  "add": "Ajouter",
16
16
  "all": "Tous",
17
+ "*": "Toutes les valeurs",
17
18
  "any": "Tous",
18
19
  "asc": "Ascendant",
19
20
  "close": "Fermer",
20
21
  "desc": "Descendant",
22
+ "to": "à",
21
23
  "actions.running": "Action \"{{action}}\" s'exécute.",
22
24
  "actions.succeeded": "Action \"{{action}}\" achevé avec succès.",
23
25
  "app.drawer.hit.assignment.autocomplete.label": "Choisissez le destinataire",
@@ -246,6 +248,7 @@
246
248
  "hit.search.filter.values": "Valeurs",
247
249
  "hit.search.filter.button": "Utiliser des filtres de recherche avec des valeurs prédéfinies",
248
250
  "hit.search.filter.label": "Filtres de recherche",
251
+ "hit.search.filter.add": "Ajouter un filtre",
249
252
  "hit.search.save.button": "Sauvegarder la requête en tant que vue",
250
253
  "hit.search.save.view": "Sauvegarder la vue",
251
254
  "hit.search.custom": "Triage personnalisé",
@@ -255,8 +258,11 @@
255
258
  "hit.search.select.all": "Sélectionner tous les résultats de la page",
256
259
  "hit.search.select.clear": "Effacer la sélection",
257
260
  "hit.search.selected": "{{size}} sélectionné(s)",
261
+ "hit.search.view.add": "Ajouter une vue",
258
262
  "hit.search.view.current": "Vue courante",
259
263
  "hit.search.view.default": "(Défaut)",
264
+ "hit.search.view.select": "Sélectionner une vue",
265
+ "hit.search.view.remove": "Supprimer la vue",
260
266
  "hit.viewer.aggregate": "Sommaire",
261
267
  "hit.viewer.comments": "Commentaires",
262
268
  "hit.viewer.data": "Données brutes",
package/package.json CHANGED
@@ -51,7 +51,7 @@
51
51
  "react-markdown": "^10.1.0",
52
52
  "react-pluggable": "^0.4.3",
53
53
  "react-resize-detector": "^9.1.1",
54
- "react-router": "^6.30.2",
54
+ "react-router": "^6.30.3",
55
55
  "react-router-dom": "^6.30.1",
56
56
  "react-syntax-highlighter": "^15.6.1",
57
57
  "rehype-raw": "^7.0.0",
@@ -96,7 +96,7 @@
96
96
  "internal-slot": "1.0.7"
97
97
  },
98
98
  "type": "module",
99
- "version": "2.16.0-dev.378",
99
+ "version": "2.16.0-dev.381",
100
100
  "exports": {
101
101
  "./i18n": "./i18n.js",
102
102
  "./index.css": "./index.css",
package/setupTests.js CHANGED
@@ -9,5 +9,8 @@ expect.extend(matchers);
9
9
  // tell React Testing Library to look for id as the testId.
10
10
  configure({ testIdAttribute: 'id' });
11
11
  beforeAll(() => server.listen());
12
- afterEach(() => server.resetHandlers());
12
+ afterEach(() => {
13
+ server.resetHandlers();
14
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
15
+ });
13
16
  afterAll(() => server.close());
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Sets up a mock for use-context-selector that uses React's native context
3
+ * This allows tests to use context providers without the full use-context-selector implementation
4
+ */
5
+ export declare const setupContextSelectorMock: () => void;
6
+ /**
7
+ * Sets up a mock for react-router-dom with common defaults
8
+ * @param options - Override specific router behavior
9
+ */
10
+ export declare const setupReactRouterMock: () => void;
11
+ /**
12
+ * Sets up a mock localStorage instance
13
+ */
14
+ export declare const setupLocalStorageMock: () => Storage;
15
+ /**
16
+ * Sets up a mock sessionStorage instance
17
+ */
18
+ export declare const setupSessionStorageMock: () => Storage;
package/tests/mocks.js ADDED
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, forwardRef, useContext } from 'react';
3
+ import { vi } from 'vitest';
4
+ import MockLocalStorage from './MockLocalStorage';
5
+ /**
6
+ * Sets up a mock for use-context-selector that uses React's native context
7
+ * This allows tests to use context providers without the full use-context-selector implementation
8
+ */
9
+ export const setupContextSelectorMock = () => {
10
+ beforeAll(() => {
11
+ vi.mock('use-context-selector', async () => {
12
+ const actual = await vi.importActual('use-context-selector');
13
+ return {
14
+ ...actual,
15
+ createContext,
16
+ useContextSelector: (_context, selector) => {
17
+ return selector(useContext(_context));
18
+ }
19
+ };
20
+ });
21
+ });
22
+ afterAll(() => vi.resetModules());
23
+ };
24
+ /**
25
+ * Sets up a mock for react-router-dom with common defaults
26
+ * @param options - Override specific router behavior
27
+ */
28
+ export const setupReactRouterMock = () => {
29
+ const mockLocation = vi.hoisted(() => ({ pathname: '/hits', search: '' }));
30
+ const mockParams = vi.hoisted(() => ({ id: undefined }));
31
+ const mockSearchParams = vi.hoisted(() => new URLSearchParams());
32
+ const mockSetParams = vi.hoisted(() => vi.fn());
33
+ beforeAll(() => {
34
+ vi.mock('react-router-dom', () => ({
35
+ Link: forwardRef(({ to, children, ...props }, ref) => (_jsx("a", { ref: ref, href: to, ...props, children: children }))),
36
+ useLocation: vi.fn(() => mockLocation),
37
+ useParams: vi.fn(() => mockParams),
38
+ useSearchParams: vi.fn(() => [mockSearchParams, mockSetParams]),
39
+ useNavigate: () => vi.fn()
40
+ }));
41
+ });
42
+ afterAll(() => vi.resetModules());
43
+ };
44
+ /**
45
+ * Sets up a mock localStorage instance
46
+ */
47
+ export const setupLocalStorageMock = () => {
48
+ const mockLocalStorage = new MockLocalStorage();
49
+ Object.defineProperty(window, 'localStorage', {
50
+ value: mockLocalStorage,
51
+ writable: true
52
+ });
53
+ return mockLocalStorage;
54
+ };
55
+ /**
56
+ * Sets up a mock sessionStorage instance
57
+ */
58
+ export const setupSessionStorageMock = () => {
59
+ const mockSessionStorage = new MockLocalStorage();
60
+ Object.defineProperty(window, 'sessionStorage', {
61
+ value: mockSessionStorage,
62
+ writable: true
63
+ });
64
+ return mockSessionStorage;
65
+ };
@@ -1,5 +1,5 @@
1
1
  import { http, HttpResponse } from 'msw';
2
- import { createMockAction } from './utils';
2
+ import { createMockAction, createMockAnalytic, createMockHit, createMockView } from './utils';
3
3
  export const MOCK_RESPONSES = {
4
4
  '/api/v1/view/example_view_id': {
5
5
  owner: 'user',
@@ -13,42 +13,24 @@ export const MOCK_RESPONSES = {
13
13
  type: 'personal',
14
14
  span: 'date.range.1.month'
15
15
  },
16
- '/api/v1/search/view': {
17
- items: [
18
- {
19
- owner: 'user',
20
- settings: {
21
- advance_on_triage: false
22
- },
23
- view_id: 'searched_view_id',
24
- query: 'howler.id:searched',
25
- sort: 'event.created desc',
26
- title: 'Searched View',
27
- type: 'personal',
28
- span: 'date.range.1.month'
29
- }
30
- ],
16
+ '/api/v1/search/hit': {
17
+ items: [createMockHit({ howler: { id: 'howler.id' } })],
31
18
  total: 1,
32
19
  rows: 1
33
20
  },
34
- '/api/v1/view/new_view_id': {
35
- owner: 'user',
36
- settings: {
37
- advance_on_triage: false
38
- },
39
- view_id: 'new_view_id',
40
- query: 'howler.id:new',
41
- sort: 'event.created desc',
42
- title: 'New View',
43
- type: 'personal',
44
- span: 'date.range.1.month'
21
+ '/api/v1/search/view': {
22
+ items: [createMockView({ view_id: 'searched_view_id', title: 'Searched View' })],
23
+ total: 1,
24
+ rows: 1
45
25
  },
26
+ '/api/v1/view/new_view_id': createMockView({ view_id: 'new_view_id' }),
46
27
  '/api/v1/view/:view_id/favourite': { success: true },
47
28
  '/api/v1/search/action': {
48
29
  items: [createMockAction()],
49
30
  total: 1,
50
31
  rows: 1
51
- }
32
+ },
33
+ '/api/v1/analytic': [createMockAnalytic()]
52
34
  };
53
35
  const handlers = [
54
36
  ...Object.entries(MOCK_RESPONSES).map(([path, data]) => http.all(path, async () => HttpResponse.json({ api_response: data }))),
@@ -0,0 +1,2 @@
1
+ import type { View } from '@cccsaurora/howler-ui/models/entities/generated/View';
2
+ export declare const buildViewUrl: (view: View) => string;
@@ -0,0 +1,11 @@
1
+ export const buildViewUrl = (view) => {
2
+ const params = new URLSearchParams();
3
+ params.set('view', view.view_id);
4
+ if (view.span) {
5
+ params.set('span', view.span);
6
+ }
7
+ if (view.sort) {
8
+ params.set('sort', view.sort);
9
+ }
10
+ return `/search?${params.toString()}`;
11
+ };