@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
@@ -51,6 +51,7 @@ import ViewComposer from '@cccsaurora/howler-ui/components/routes/views/ViewComp
51
51
  import Views from '@cccsaurora/howler-ui/components/routes/views/Views';
52
52
  import dayjs from 'dayjs';
53
53
  import duration from 'dayjs/plugin/duration';
54
+ import localizedFormat from 'dayjs/plugin/localizedFormat';
54
55
  import relativeTime from 'dayjs/plugin/relativeTime';
55
56
  import utc from 'dayjs/plugin/utc';
56
57
  import i18n from '@cccsaurora/howler-ui/i18n';
@@ -80,6 +81,7 @@ import ViewProvider from './providers/ViewProvider';
80
81
  dayjs.extend(utc);
81
82
  dayjs.extend(duration);
82
83
  dayjs.extend(relativeTime);
84
+ dayjs.extend(localizedFormat);
83
85
  loader.config({ monaco });
84
86
  const RoleRoute = ({ role }) => {
85
87
  const appUser = useAppUser();
@@ -18,7 +18,6 @@ const useMatchers = () => {
18
18
  return (await getHit(hit.howler.id, true)).__template;
19
19
  }
20
20
  catch (e) {
21
- console.warn(e);
22
21
  return null;
23
22
  }
24
23
  }, [getHit]);
@@ -35,7 +34,6 @@ const useMatchers = () => {
35
34
  return (await getHit(hit.howler.id, true)).__overview;
36
35
  }
37
36
  catch (e) {
38
- console.warn(e);
39
37
  return null;
40
38
  }
41
39
  }, [getHit]);
@@ -52,7 +50,6 @@ const useMatchers = () => {
52
50
  return (await getHit(hit.howler.id, true)).__dossiers ?? [];
53
51
  }
54
52
  catch (e) {
55
- console.warn(e);
56
53
  return [];
57
54
  }
58
55
  }, [getHit]);
@@ -68,7 +65,6 @@ const useMatchers = () => {
68
65
  return (await getHit(hit.howler.id, true)).__analytic;
69
66
  }
70
67
  catch (e) {
71
- console.warn(e);
72
68
  return null;
73
69
  }
74
70
  }, [getHit]);
@@ -5,6 +5,7 @@ import { sortBy, uniq } from 'lodash-es';
5
5
  import { createContext, useCallback, useContext, useEffect } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { useContextSelector } from 'use-context-selector';
8
+ import { buildViewUrl } from '@cccsaurora/howler-ui/utils/viewUtils';
8
9
  import { AnalyticContext } from './AnalyticProvider';
9
10
  import { ViewContext } from './ViewProvider';
10
11
  export const FavouriteContext = createContext(null);
@@ -35,7 +36,7 @@ const FavouriteProvider = ({ children }) => {
35
36
  .map(view => ({
36
37
  id: view.view_id,
37
38
  text: t(view.title),
38
- route: `/views/${view.view_id}`,
39
+ route: buildViewUrl(view),
39
40
  nested: true
40
41
  }));
41
42
  if (viewElement) {
@@ -1,9 +1,9 @@
1
1
  import type { SearchField } from '@cccsaurora/howler-ui/api/search/fields';
2
2
  import type { FC, PropsWithChildren } from 'react';
3
- interface FieldProviderType {
3
+ interface FieldContextType {
4
4
  hitFields: SearchField[];
5
5
  getHitFields: () => Promise<SearchField[]>;
6
6
  }
7
- export declare const FieldContext: import("react").Context<FieldProviderType>;
7
+ export declare const FieldContext: import("react").Context<FieldContextType>;
8
8
  declare const FieldProvider: FC<PropsWithChildren>;
9
9
  export default FieldProvider;
@@ -1,7 +1,7 @@
1
1
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
2
  import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
3
3
  import type { FC, PropsWithChildren } from 'react';
4
- interface HitProviderType {
4
+ export interface HitContextType {
5
5
  hits: {
6
6
  [index: string]: Hit;
7
7
  };
@@ -13,10 +13,10 @@ interface HitProviderType {
13
13
  updateHit: (newHit: Hit) => void;
14
14
  getHit: (id: string, force?: boolean) => Promise<WithMetadata<Hit>>;
15
15
  }
16
- export declare const HitContext: import("use-context-selector").Context<HitProviderType>;
16
+ export declare const HitContext: import("use-context-selector").Context<HitContextType>;
17
17
  /**
18
18
  * Central repository for storing individual hit data across the application. Allows efficient retrieval of hits across componenents.
19
19
  */
20
20
  declare const HitProvider: FC<PropsWithChildren>;
21
- export declare const useHitContextSelector: <Selected>(selector: (value: HitProviderType) => Selected) => Selected;
21
+ export declare const useHitContextSelector: <Selected>(selector: (value: HitContextType) => Selected) => Selected;
22
22
  export default HitProvider;
@@ -1,27 +1,26 @@
1
1
  import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
2
- import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
2
+ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
3
3
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
4
4
  import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
5
5
  import { type Dispatch, type FC, type PropsWithChildren, type SetStateAction } from 'react';
6
6
  export interface QueryEntry {
7
7
  [query: string]: string;
8
8
  }
9
- interface HitSearchProviderType {
10
- layout: HitLayout;
9
+ export interface HitSearchContextType {
11
10
  displayType: 'list' | 'grid';
12
11
  searching: boolean;
13
12
  error: string | null;
14
13
  response: HowlerSearchResponse<WithMetadata<Hit>> | null;
15
- viewId: string | null;
16
14
  bundleId: string | null;
17
- queryHistory: QueryEntry;
18
15
  fzfSearch: boolean;
19
16
  setDisplayType: (type: 'list' | 'grid') => void;
20
- setQueryHistory: Dispatch<SetStateAction<QueryEntry>>;
21
17
  setFzfSearch: Dispatch<SetStateAction<boolean>>;
22
18
  search: (query: string, appendResults?: boolean) => void;
19
+ getFilters: () => Promise<string[]>;
20
+ queryHistory: QueryEntry;
21
+ setQueryHistory: ReturnType<typeof useMyLocalStorageItem>[1];
23
22
  }
24
- export declare const HitSearchContext: import("use-context-selector").Context<HitSearchProviderType>;
23
+ export declare const HitSearchContext: import("use-context-selector").Context<HitSearchContextType>;
25
24
  declare const HitSearchProvider: FC<PropsWithChildren>;
26
- export declare const useHitSearchContextSelector: <Selected>(selector: (value: HitSearchProviderType) => Selected) => Selected;
25
+ export declare const useHitSearchContextSelector: <Selected>(selector: (value: HitSearchContextType) => Selected) => Selected;
27
26
  export default HitSearchProvider;
@@ -1,17 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import api from '@cccsaurora/howler-ui/api';
3
- import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
4
3
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
4
  import useMyLocalStorage, { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
5
+ import dayjs from 'dayjs';
6
6
  import i18n from '@cccsaurora/howler-ui/i18n';
7
+ import { cloneDeep } from 'lodash-es';
7
8
  import isNull from 'lodash-es/isNull';
8
9
  import isUndefined from 'lodash-es/isUndefined';
9
10
  import { useCallback, useEffect, useMemo, useState } from 'react';
10
- import { isMobile } from 'react-device-detect';
11
11
  import { useLocation, useParams } from 'react-router-dom';
12
12
  import { createContext, useContextSelector } from 'use-context-selector';
13
13
  import { DEFAULT_QUERY, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
14
- import { getStored } from '@cccsaurora/howler-ui/utils/localStorage';
15
14
  import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
16
15
  import { convertCustomDateRangeToLucene, convertDateToLucene } from '@cccsaurora/howler-ui/utils/utils';
17
16
  import { HitContext } from './HitProvider';
@@ -25,7 +24,7 @@ const HitSearchProvider = ({ children }) => {
25
24
  const location = useLocation();
26
25
  const { dispatchApi } = useMyApi();
27
26
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
28
- const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
27
+ const getCurrentViews = useContextSelector(ViewContext, ctx => ctx.getCurrentViews);
29
28
  const defaultView = useContextSelector(ViewContext, ctx => ctx.defaultView);
30
29
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
31
30
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
@@ -34,19 +33,60 @@ const HitSearchProvider = ({ children }) => {
34
33
  const trackTotalHits = useContextSelector(ParameterContext, ctx => ctx.trackTotalHits);
35
34
  const sort = useContextSelector(ParameterContext, ctx => ctx.sort);
36
35
  const span = useContextSelector(ParameterContext, ctx => ctx.span);
37
- const filter = useContextSelector(ParameterContext, ctx => ctx.filter);
36
+ const allFilters = useContextSelector(ParameterContext, ctx => ctx.filters);
38
37
  const startDate = useContextSelector(ParameterContext, ctx => ctx.startDate);
39
38
  const endDate = useContextSelector(ParameterContext, ctx => ctx.endDate);
39
+ const views = useContextSelector(ParameterContext, ctx => ctx.views);
40
+ const addView = useContextSelector(ParameterContext, ctx => ctx.addView);
40
41
  const loadHits = useContextSelector(HitContext, ctx => ctx.loadHits);
41
- const [displayType, setDisplayType] = useState(getStored(StorageKey.DISPLAY_TYPE) ?? 'list');
42
+ const [displayType, setDisplayType] = useState(get(StorageKey.DISPLAY_TYPE) ?? 'list');
42
43
  const [searching, setSearching] = useState(false);
43
44
  const [error, setError] = useState(null);
44
- const [response, setResponse] = useState();
45
- const [queryHistory, setQueryHistory] = useState(JSON.parse(get(StorageKey.QUERY_HISTORY)) || { 'howler.id: *': new Date().toISOString() });
45
+ const [response, setResponse] = useState(null);
46
+ const [queryHistory, setQueryHistory] = useMyLocalStorageItem(StorageKey.QUERY_HISTORY, {
47
+ 'howler.id: *': new Date().toISOString()
48
+ });
46
49
  const [fzfSearch, setFzfSearch] = useState(false);
47
- const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : defaultView) ?? null, [defaultView, location.pathname, routeParams.id]);
48
50
  const bundleId = useMemo(() => (location.pathname.startsWith('/bundles') ? routeParams.id : null), [location.pathname, routeParams.id]);
49
- const layout = useMemo(() => (isMobile ? HitLayout.COMFY : (get(StorageKey.HIT_LAYOUT) ?? HitLayout.NORMAL)), [get]);
51
+ const filters = useMemo(() => allFilters.filter(filter => !filter.endsWith('*')), [allFilters]);
52
+ // On load check to filter out any queries older than one month
53
+ useEffect(() => {
54
+ const filterQueryTime = dayjs().subtract(1, 'month').toISOString();
55
+ setQueryHistory(Object.fromEntries(Object.entries(queryHistory).filter(([_, value]) => value > filterQueryTime)));
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, []);
58
+ // Inject default view into URL when no views present
59
+ useEffect(() => {
60
+ if (views.length === 0 && defaultView) {
61
+ addView(defaultView);
62
+ }
63
+ }, [views.length, defaultView, addView]);
64
+ const getFilters = useCallback(async () => {
65
+ const _filters = cloneDeep(filters);
66
+ // Add span filter
67
+ if (span && !span.endsWith('custom')) {
68
+ _filters.push(`event.created:${convertDateToLucene(span)}`);
69
+ }
70
+ else if (startDate && endDate) {
71
+ _filters.push(`event.created:${convertCustomDateRangeToLucene(startDate, endDate)}`);
72
+ }
73
+ // Add bundle filter
74
+ const bundle = location.pathname.startsWith('/bundles') && routeParams.id;
75
+ if (bundle) {
76
+ _filters.push(`howler.bundles:${bundle}`);
77
+ }
78
+ // Fetch all view queries
79
+ if (views.length > 0) {
80
+ const viewObjects = await getCurrentViews();
81
+ // Filter out null/undefined views and extract queries
82
+ viewObjects
83
+ .filter(view => view?.query)
84
+ .map(view => view.query)
85
+ .forEach(viewQuery => _filters.push(viewQuery));
86
+ }
87
+ return _filters;
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ }, [endDate, filters, getCurrentViews, location.pathname, routeParams.id, span, startDate, views]);
50
90
  const search = useCallback(async (_query, appendResults) => {
51
91
  THROTTLER.debounce(async () => {
52
92
  if (_query === 'woof!') {
@@ -58,34 +98,20 @@ const HitSearchProvider = ({ children }) => {
58
98
  }
59
99
  if (!isNull(_query) && !isUndefined(_query) && _query !== query) {
60
100
  setQuery(_query);
101
+ setQueryHistory({
102
+ ...queryHistory,
103
+ [_query]: new Date().toISOString()
104
+ });
61
105
  }
62
106
  setSearching(true);
63
107
  setError(null);
64
- const filters = [];
65
- if (span && !span.endsWith('custom')) {
66
- filters.push(`event.created:${convertDateToLucene(span)}`);
67
- }
68
- else if (startDate && endDate) {
69
- filters.push(`event.created:${convertCustomDateRangeToLucene(startDate, endDate)}`);
70
- }
71
- if (filter) {
72
- filters.push(filter);
73
- }
74
108
  try {
75
- const bundle = location.pathname.startsWith('/bundles') && routeParams.id;
76
- let fullQuery = _query || DEFAULT_QUERY;
77
- if (bundle) {
78
- fullQuery = `(howler.bundles:${bundle}) AND (${fullQuery})`;
79
- }
80
- else if (viewId) {
81
- fullQuery = `(${(await getCurrentView({ viewId }))?.query || DEFAULT_QUERY}) AND (${fullQuery})`;
82
- }
83
109
  const _response = await dispatchApi(api.search.hit.post({
84
- offset: appendResults ? response.rows : offset,
110
+ offset: appendResults && response ? response.rows : offset,
85
111
  rows: pageCount,
86
- query: fullQuery,
112
+ query: _query || DEFAULT_QUERY,
87
113
  sort,
88
- filters,
114
+ filters: await getFilters(),
89
115
  track_total_hits: trackTotalHits,
90
116
  metadata: ['template', 'overview', 'analytic']
91
117
  }), { showError: false, throwError: true });
@@ -121,41 +147,40 @@ const HitSearchProvider = ({ children }) => {
121
147
  query,
122
148
  startDate,
123
149
  endDate,
124
- filter,
150
+ filters,
125
151
  setQuery,
126
152
  location.pathname,
127
153
  routeParams.id,
128
- viewId,
154
+ views,
129
155
  dispatchApi,
130
156
  offset,
131
157
  pageCount,
132
158
  trackTotalHits,
133
159
  loadHits,
134
- getCurrentView,
160
+ getCurrentViews,
135
161
  setOffset
136
162
  ]);
137
163
  // We only run this when ancillary properties (i.e. filters, sorting) change
138
164
  useEffect(() => {
139
- if (span.endsWith('custom') && (!startDate || !endDate)) {
165
+ if (span?.endsWith('custom') && (!startDate || !endDate)) {
140
166
  return;
141
167
  }
142
- if (viewId || bundleId || (query && query !== DEFAULT_QUERY) || offset > 0) {
168
+ if (views.length > 0 || bundleId || (query && query !== DEFAULT_QUERY) || offset > 0 || filters.length > 0) {
143
169
  search(query);
144
170
  }
145
171
  else {
146
172
  setResponse(null);
147
173
  }
148
174
  // eslint-disable-next-line react-hooks/exhaustive-deps
149
- }, [filter, offset, pageCount, sort, span, bundleId, location.pathname, startDate, endDate, viewId]);
175
+ }, [offset, pageCount, sort, span, bundleId, location.pathname, startDate, endDate, filters, query]);
150
176
  return (_jsx(HitSearchContext.Provider, { value: {
151
- layout,
152
177
  displayType,
153
178
  setDisplayType,
154
179
  search,
155
180
  searching,
181
+ getFilters,
156
182
  error,
157
183
  response,
158
- viewId,
159
184
  bundleId,
160
185
  setQueryHistory,
161
186
  queryHistory,