@cccsaurora/howler-ui 2.13.0-dev.75 → 2.13.0-dev.77

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.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { QueryStats, SavedSearch } from '@mui/icons-material';
3
3
  import { useAppLeftNav, useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
4
- import { uniqBy } from 'lodash-es';
4
+ import { 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';
@@ -13,11 +13,10 @@ const FavouriteProvider = ({ children }) => {
13
13
  const leftNav = useAppLeftNav();
14
14
  const appUser = useAppUser();
15
15
  const analytics = useContext(AnalyticContext);
16
- const views = useContextSelector(ViewContext, ctx => ctx.views);
17
- const viewsReady = useContextSelector(ViewContext, ctx => ctx.ready);
18
- const processViewElement = useCallback(() => {
16
+ const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
17
+ const processViewElement = useCallback(async () => {
19
18
  const viewElement = leftNav.elements.find(el => el.element?.id === 'views');
20
- const favourites = appUser.user?.favourite_views || [];
19
+ const favourites = uniq(appUser.user?.favourite_views || []);
21
20
  // There are no favourites and no nav elements - return
22
21
  if (favourites.length < 1 && !viewElement) {
23
22
  return null;
@@ -30,19 +29,15 @@ const FavouriteProvider = ({ children }) => {
30
29
  if (favourites.length === viewElement?.element?.items?.length) {
31
30
  return viewElement;
32
31
  }
33
- const items = uniqBy(favourites
34
- .map(view_id => {
35
- const view = views?.find(v => v.view_id === view_id);
36
- return view
37
- ? {
38
- id: view.view_id,
39
- text: t(view.title),
40
- route: `/views/${view.view_id}`,
41
- nested: true
42
- }
43
- : null;
44
- })
45
- .filter(v => !!v), val => val.id);
32
+ const savedViews = await fetchViews(favourites);
33
+ const items = savedViews
34
+ .filter(view => !!view)
35
+ .map(view => ({
36
+ id: view.view_id,
37
+ text: t(view.title),
38
+ route: `/views/${view.view_id}`,
39
+ nested: true
40
+ }));
46
41
  if (viewElement) {
47
42
  const newViewElement = {
48
43
  ...viewElement,
@@ -64,7 +59,7 @@ const FavouriteProvider = ({ children }) => {
64
59
  }
65
60
  };
66
61
  }
67
- }, [appUser.user?.favourite_views, leftNav, t, views]);
62
+ }, [appUser.user?.favourite_views, fetchViews, leftNav.elements, t]);
68
63
  const processAnalyticElement = useCallback(() => {
69
64
  const analyticElement = leftNav.elements.find(el => el.element?.id === 'analytics');
70
65
  const favourites = appUser.user?.favourite_analytics;
@@ -115,23 +110,25 @@ const FavouriteProvider = ({ children }) => {
115
110
  }
116
111
  }, [analytics.analytics, appUser.user?.favourite_analytics, leftNav, t]);
117
112
  useEffect(() => {
118
- if (!appUser.isReady() || !viewsReady || !analytics.ready) {
113
+ if (!appUser.isReady() || !analytics.ready) {
119
114
  return;
120
115
  }
121
116
  const newElements = leftNav.elements
122
117
  .filter(el => !['views', 'analytics'].includes(el.element?.id))
123
118
  .filter(el => !!el);
124
- const analyticElement = processAnalyticElement();
125
- if (analyticElement) {
126
- newElements.splice(1, 0, analyticElement);
127
- }
128
- const viewElement = processViewElement();
129
- if (viewElement) {
130
- newElements.splice(1, 0, viewElement);
131
- }
132
- leftNav.setElements(newElements);
119
+ (async () => {
120
+ const analyticElement = processAnalyticElement();
121
+ if (analyticElement) {
122
+ newElements.splice(1, 0, analyticElement);
123
+ }
124
+ const viewElement = await processViewElement();
125
+ if (viewElement) {
126
+ newElements.splice(1, 0, viewElement);
127
+ }
128
+ leftNav.setElements(newElements);
129
+ })();
133
130
  // eslint-disable-next-line react-hooks/exhaustive-deps
134
- }, [analytics.ready, appUser, viewsReady]);
131
+ }, [analytics.ready, appUser]);
135
132
  return _jsx(FavouriteContext.Provider, { value: {}, children: children });
136
133
  };
137
134
  export default FavouriteProvider;
@@ -25,8 +25,8 @@ const HitSearchProvider = ({ children }) => {
25
25
  const location = useLocation();
26
26
  const { dispatchApi } = useMyApi();
27
27
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
28
- const viewsReady = useContextSelector(ViewContext, ctx => ctx.ready);
29
- const views = useContextSelector(ViewContext, ctx => ctx.views);
28
+ const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
29
+ const defaultView = useContextSelector(ViewContext, ctx => ctx.defaultView);
30
30
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
31
31
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
32
32
  const offset = useContextSelector(ParameterContext, ctx => ctx.offset);
@@ -44,7 +44,7 @@ const HitSearchProvider = ({ children }) => {
44
44
  const [response, setResponse] = useState();
45
45
  const [queryHistory, setQueryHistory] = useState(JSON.parse(get(StorageKey.QUERY_HISTORY)) || { 'howler.id: *': new Date().toISOString() });
46
46
  const [fzfSearch, setFzfSearch] = useState(false);
47
- const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
47
+ const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : defaultView) ?? null, [defaultView, location.pathname, routeParams.id]);
48
48
  const bundleId = useMemo(() => (location.pathname.startsWith('/bundles') ? routeParams.id : null), [location.pathname, routeParams.id]);
49
49
  const layout = useMemo(() => (isMobile ? HitLayout.COMFY : (get(StorageKey.HIT_LAYOUT) ?? HitLayout.NORMAL)), [get]);
50
50
  const search = useCallback(async (_query, appendResults) => {
@@ -78,7 +78,7 @@ const HitSearchProvider = ({ children }) => {
78
78
  fullQuery = `(howler.bundles:${bundle}) AND (${fullQuery})`;
79
79
  }
80
80
  else if (viewId) {
81
- fullQuery = `(${views.find(_view => _view.view_id === viewId)?.query || 'howler.id:*'}) AND (${fullQuery})`;
81
+ fullQuery = `(${(await getCurrentView())?.query || 'howler.id:*'}) AND (${fullQuery})`;
82
82
  }
83
83
  const _response = await dispatchApi(api.search.hit.post({
84
84
  offset: appendResults ? response.rows : offset,
@@ -130,15 +130,12 @@ const HitSearchProvider = ({ children }) => {
130
130
  pageCount,
131
131
  trackTotalHits,
132
132
  loadHits,
133
- views,
133
+ getCurrentView,
134
+ defaultView,
134
135
  setOffset
135
136
  ]);
136
137
  // We only run this when ancillary properties (i.e. filters, sorting) change
137
138
  useEffect(() => {
138
- // We're being asked to present a view, but we don't currently have the views loaded
139
- if (viewId && !viewsReady) {
140
- return;
141
- }
142
139
  if (span.endsWith('custom') && (!startDate || !endDate)) {
143
140
  return;
144
141
  }
@@ -149,7 +146,7 @@ const HitSearchProvider = ({ children }) => {
149
146
  setResponse(null);
150
147
  }
151
148
  // eslint-disable-next-line react-hooks/exhaustive-deps
152
- }, [filter, offset, pageCount, sort, span, bundleId, location.pathname, viewsReady, startDate, endDate]);
149
+ }, [filter, offset, pageCount, sort, span, bundleId, location.pathname, startDate, endDate]);
153
150
  return (_jsx(HitSearchContext.Provider, { value: {
154
151
  layout,
155
152
  displayType,
@@ -1,17 +1,18 @@
1
1
  import type { View } from '@cccsaurora/howler-ui/models/entities/generated/View';
2
2
  import { type FC, type PropsWithChildren } from 'react';
3
3
  export interface ViewContextType {
4
- ready: boolean;
5
4
  defaultView: string;
6
5
  setDefaultView: (viewId: string) => void;
7
- views: View[];
6
+ views: {
7
+ [viewId: string]: View;
8
+ };
8
9
  addFavourite: (id: string) => Promise<void>;
9
10
  removeFavourite: (id: string) => Promise<void>;
10
- fetchViews: (force?: boolean) => Promise<void>;
11
+ fetchViews: (ids?: string[]) => Promise<View[]>;
11
12
  addView: (v: View) => Promise<View>;
12
13
  editView: (id: string, title: string, query: string, sort: string, span: string, advanceOnTriage: boolean) => Promise<View>;
13
14
  removeView: (id: string) => Promise<void>;
14
- getCurrentView: () => View;
15
+ getCurrentView: (lazy?: boolean) => Promise<View>;
15
16
  }
16
17
  export declare const ViewContext: import("use-context-selector").Context<ViewContextType>;
17
18
  declare const ViewProvider: FC<PropsWithChildren>;
@@ -3,6 +3,7 @@ import api from '@cccsaurora/howler-ui/api';
3
3
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
4
4
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
5
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
6
+ import { has, omit } from 'lodash-es';
6
7
  import { useCallback, useEffect, useState } from 'react';
7
8
  import { useLocation, useParams } from 'react-router-dom';
8
9
  import { createContext, useContextSelector } from 'use-context-selector';
@@ -14,77 +15,98 @@ const ViewProvider = ({ children }) => {
14
15
  const [defaultView, setDefaultView] = useMyLocalStorageItem(StorageKey.DEFAULT_VIEW);
15
16
  const location = useLocation();
16
17
  const routeParams = useParams();
17
- const [loading, setLoading] = useState(false);
18
- const [views, setViews] = useState({ ready: false, views: [] });
19
- const fetchViews = useCallback(async (force = false) => {
20
- if (views.ready && !force) {
21
- return;
18
+ const [views, setViews] = useState({});
19
+ const fetchViews = useCallback(async (ids) => {
20
+ if (!ids) {
21
+ const newViews = (await dispatchApi(api.view.get(), { throwError: false })) ?? [];
22
+ setViews(_views => ({
23
+ ..._views,
24
+ ...Object.fromEntries(newViews.map(_view => [_view.view_id, _view]))
25
+ }));
26
+ return newViews;
22
27
  }
23
- if (!appUser.isReady()) {
24
- return;
28
+ const missingIds = ids.filter(_id => !views[_id]);
29
+ if (missingIds.length < 1) {
30
+ return ids.map(id => views[id]);
25
31
  }
26
- setLoading(true);
27
32
  try {
28
- setViews({ ready: true, views: await api.view.get() });
29
- }
30
- finally {
31
- setLoading(false);
33
+ const response = await dispatchApi(api.search.view.post({
34
+ query: `view_id:(${missingIds.join(' OR ')})`,
35
+ rows: missingIds.length
36
+ }));
37
+ const newViews = Object.fromEntries(response.items.map(_view => [_view.view_id, _view]));
38
+ setViews(_views => ({
39
+ ..._views,
40
+ ...Object.fromEntries(missingIds.map(_view_id => [_view_id, null])),
41
+ ...newViews
42
+ }));
43
+ return ids.map(id => views[id] ?? newViews[id]);
32
44
  }
33
- }, [appUser, views.ready]);
34
- useEffect(() => {
35
- if (!views.ready && !loading) {
36
- fetchViews();
45
+ catch (e) {
46
+ // eslint-disable-next-line no-console
47
+ console.warn(e);
48
+ return [];
37
49
  }
38
- }, [fetchViews, views.ready, appUser, loading]);
50
+ }, [dispatchApi, views]);
39
51
  useEffect(() => {
40
- if (defaultView && views.views?.length > 0 && !views.views?.find(v => v.view_id === defaultView)) {
41
- setDefaultView(undefined);
52
+ if (!defaultView || has(views, defaultView)) {
53
+ return;
42
54
  }
43
- });
44
- const getCurrentView = useCallback(() => {
45
- if (!location.pathname.startsWith('/views')) {
55
+ (async () => {
56
+ const result = await fetchViews([defaultView]);
57
+ if (!result.length) {
58
+ setDefaultView(undefined);
59
+ }
60
+ })();
61
+ }, [defaultView, fetchViews, setDefaultView, views]);
62
+ const getCurrentView = useCallback(async (lazy = false) => {
63
+ const id = location.pathname.startsWith('/views') ? routeParams.id : defaultView;
64
+ if (!id) {
46
65
  return null;
47
66
  }
48
- return views.views.find(_view => _view.view_id === routeParams.id);
49
- }, [location.pathname, routeParams.id, views.views]);
67
+ if (!has(views, id) && !lazy) {
68
+ return (await fetchViews([id]))[0];
69
+ }
70
+ return views[id];
71
+ }, [defaultView, fetchViews, location.pathname, routeParams.id, views]);
50
72
  const editView = useCallback(async (id, title, query, sort, span, advanceOnTriage) => {
51
- const result = await api.view.put(id, title, query, sort, span, advanceOnTriage);
73
+ const result = await dispatchApi(api.view.put(id, title, query, sort, span, advanceOnTriage));
52
74
  setViews(_views => ({
53
75
  ..._views,
54
- views: _views.views.map(v => v.view_id === id ? { ...v, title, query, sort, span, settings: { advance_on_triage: advanceOnTriage } } : v)
76
+ [id]: { ...(_views[id] ?? {}), title, query, sort, span, settings: { advance_on_triage: advanceOnTriage } }
55
77
  }));
56
78
  return result;
57
- }, []);
79
+ }, [dispatchApi]);
58
80
  const addFavourite = useCallback(async (id) => {
59
- await api.view.favourite.post(id);
81
+ await dispatchApi(api.view.favourite.post(id));
60
82
  appUser.setUser({
61
83
  ...appUser.user,
62
84
  favourite_views: [...appUser.user.favourite_views, id]
63
85
  });
64
- }, [appUser]);
86
+ }, [appUser, dispatchApi]);
65
87
  const addView = useCallback(async (view) => {
66
88
  const newView = await dispatchApi(api.view.post(view));
67
- setViews(_views => ({ ..._views, views: [..._views.views, newView] }));
89
+ setViews(_views => ({ ..._views, [newView.view_id]: newView }));
68
90
  addFavourite(newView.view_id);
69
91
  return newView;
70
92
  }, [addFavourite, dispatchApi]);
71
93
  const removeFavourite = useCallback(async (id) => {
72
- await api.view.favourite.del(id);
94
+ await dispatchApi(api.view.favourite.del(id));
73
95
  appUser.setUser({
74
96
  ...appUser.user,
75
97
  favourite_views: appUser.user.favourite_views.filter(v => v !== id)
76
98
  });
77
- }, [appUser]);
99
+ }, [appUser, dispatchApi]);
78
100
  const removeView = useCallback(async (id) => {
79
- const result = await api.view.del(id);
80
- setViews(_views => ({ ..._views, views: _views.views.filter(v => v.view_id !== id) }));
101
+ const result = await dispatchApi(api.view.del(id));
102
+ setViews(_views => omit(_views, id));
81
103
  if (appUser.user?.favourite_views.includes(id)) {
82
104
  removeFavourite(id);
83
105
  }
84
106
  return result;
85
- }, [appUser.user?.favourite_views, removeFavourite]);
107
+ }, [appUser.user?.favourite_views, dispatchApi, removeFavourite]);
86
108
  return (_jsx(ViewContext.Provider, { value: {
87
- ...views,
109
+ views,
88
110
  addFavourite,
89
111
  removeFavourite,
90
112
  fetchViews,
@@ -68,7 +68,7 @@ const HitActions = ({ hit, orientation = 'horizontal' }) => {
68
68
  actionFunction: async () => {
69
69
  if (!loading) {
70
70
  await assess(assessment, analytic?.triage_settings?.skip_rationale);
71
- if (getCurrentView()?.settings?.advance_on_triage && nextHit) {
71
+ if ((await getCurrentView())?.settings?.advance_on_triage && nextHit) {
72
72
  clearSelectedHits(nextHit.howler.id);
73
73
  setSelected?.(nextHit.howler.id);
74
74
  }
@@ -10,7 +10,7 @@ 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 { isNull } from 'lodash-es';
13
+ import { has, isNull } from 'lodash-es';
14
14
  import moment from 'moment';
15
15
  import { memo, useCallback, useEffect, useMemo, useState } from 'react';
16
16
  import { Trans, useTranslation } from 'react-i18next';
@@ -28,7 +28,6 @@ const HitBrowser = () => {
28
28
  const { t } = useTranslation();
29
29
  const theme = useTheme();
30
30
  const views = useContextSelector(ViewContext, ctx => ctx.views);
31
- const viewsReady = useContextSelector(ViewContext, ctx => ctx.ready);
32
31
  const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
33
32
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
34
33
  const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
@@ -61,7 +60,7 @@ const HitBrowser = () => {
61
60
  _fullQuery = `(howler.bundles:${bundle}) AND (${_fullQuery})`;
62
61
  }
63
62
  else if (viewId) {
64
- _fullQuery = `(${views.find(_view => _view.view_id === viewId)?.query || 'howler.id:*'}) AND (${_fullQuery})`;
63
+ _fullQuery = `(${views[viewId]?.query || 'howler.id:*'}) AND (${_fullQuery})`;
65
64
  }
66
65
  return _fullQuery;
67
66
  }, [location.pathname, query, routeParams.id, views, viewId]);
@@ -69,11 +68,11 @@ const HitBrowser = () => {
69
68
  if (selectedHits.length > 1) {
70
69
  return true;
71
70
  }
72
- if (selectedHits.length === 1 && selectedHits[0]?.howler.id !== routeParams.id) {
71
+ if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
73
72
  return true;
74
73
  }
75
74
  return false;
76
- }, [routeParams.id, selectedHits]);
75
+ }, [selected, selectedHits]);
77
76
  useEffect(() => {
78
77
  const newQuery = searchParams.get('query');
79
78
  if (newQuery) {
@@ -94,11 +93,12 @@ const HitBrowser = () => {
94
93
  });
95
94
  }, [setQueryHistory]);
96
95
  useEffect(() => {
97
- if (location.pathname.startsWith('/views') && !viewsReady) {
98
- fetchViews(true);
96
+ if (!location.pathname.startsWith('/views') || has(views, viewId)) {
97
+ return;
99
98
  }
99
+ fetchViews([viewId]);
100
100
  // eslint-disable-next-line react-hooks/exhaustive-deps
101
- }, [location.pathname, viewsReady]);
101
+ }, [location.pathname, viewId]);
102
102
  const onClose = useCallback(() => {
103
103
  setSelected(null);
104
104
  }, [setSelected]);
@@ -100,7 +100,7 @@ const SearchPane = () => {
100
100
  const bundleHit = useContextSelector(HitContext, ctx => location.pathname.startsWith('/bundles') ? ctx.hits[routeParams.id] : null);
101
101
  const searchPaneWidth = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null)[0];
102
102
  const verticalSorters = useMediaQuery('(max-width: 1919px)') || (searchPaneWidth ?? Number.MAX_SAFE_INTEGER) < 900;
103
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(val => val.view_id === viewId));
103
+ const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
104
104
  const getSelectedId = useCallback((event) => {
105
105
  const target = event.target;
106
106
  const selectedElement = target.closest('[id]');
@@ -4,6 +4,7 @@ import { Alert, IconButton, Stack, Tooltip, Typography } from '@mui/material';
4
4
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
5
5
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
6
6
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
7
+ import { has } from 'lodash-es';
7
8
  import { memo, useMemo } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
9
10
  import { Link } from 'react-router-dom';
@@ -14,8 +15,8 @@ const ViewLink = () => {
14
15
  const sort = useContextSelector(ParameterContext, ctx => ctx.sort);
15
16
  const span = useContextSelector(ParameterContext, ctx => ctx.span);
16
17
  const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
17
- const viewsReady = useContextSelector(ViewContext, ctx => ctx.ready);
18
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(val => val.view_id === viewId));
18
+ const viewsReady = useContextSelector(ViewContext, ctx => has(ctx.views, viewId));
19
+ const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
19
20
  const viewUrl = useMemo(() => {
20
21
  if (viewId) {
21
22
  return `/views/${viewId}/edit`;
@@ -16,7 +16,6 @@ import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelect
16
16
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
17
17
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
18
18
  import { useTranslation } from 'react-i18next';
19
- import { useLocation, useParams } from 'react-router-dom';
20
19
  import { useContextSelector } from 'use-context-selector';
21
20
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
22
21
  import HitContextMenu from '../HitContextMenu';
@@ -29,8 +28,6 @@ import HitRow from './HitRow';
29
28
  const HitGrid = () => {
30
29
  const { t } = useTranslation();
31
30
  const { getIdFromName } = useContext(AnalyticContext);
32
- const routeParams = useParams();
33
- const location = useLocation();
34
31
  const theme = useTheme();
35
32
  const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
36
33
  const { onClick } = useHitSelection();
@@ -39,10 +36,11 @@ const HitGrid = () => {
39
36
  const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
40
37
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
41
38
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
39
+ const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
42
40
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
43
41
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
44
- const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
45
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(val => val.view_id === viewId));
42
+ const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
43
+ const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
46
44
  const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
47
45
  const [analyticIds, setAnalyticIds] = useState({});
48
46
  const columnModalRef = useRef();
@@ -59,11 +57,11 @@ const HitGrid = () => {
59
57
  if (selectedHits.length > 1) {
60
58
  return true;
61
59
  }
62
- if (selectedHits.length === 1 && selectedHits[0]?.howler.id !== routeParams.id) {
60
+ if (selectedHits.length === 1 && selected && selectedHits[0]?.howler.id !== selected) {
63
61
  return true;
64
62
  }
65
63
  return false;
66
- }, [routeParams.id, selectedHits]);
64
+ }, [selected, selectedHits]);
67
65
  useEffect(() => {
68
66
  response?.items.forEach(hit => {
69
67
  if (!analyticIds[hit.howler.analytic]) {
@@ -4,7 +4,7 @@ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers
4
4
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
5
5
  import { memo, useCallback, useEffect, useMemo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
- import { useLocation, useParams } from 'react-router-dom';
7
+ import { useLocation } from 'react-router-dom';
8
8
  import { useContextSelector } from 'use-context-selector';
9
9
  import CustomSort from '../CustomSort';
10
10
  const CUSTOM = '__custom__';
@@ -21,8 +21,7 @@ const ACCEPTED_SORTS = [
21
21
  const HitSort = ({ size = 'small' }) => {
22
22
  const { t } = useTranslation();
23
23
  const location = useLocation();
24
- const routeParams = useParams();
25
- const views = useContextSelector(ViewContext, ctx => ctx.views);
24
+ const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
26
25
  const savedSort = useContextSelector(ParameterContext, ctx => ctx.sort);
27
26
  const setSavedSort = useContextSelector(ParameterContext, ctx => ctx.setSort);
28
27
  const sortEntries = useMemo(() => savedSort.split(',').filter(part => !!part), [savedSort]);
@@ -38,7 +37,6 @@ const HitSort = ({ size = 'small' }) => {
38
37
  * Should the custom sorter be shown? Defaults to true if there's more than one sort field, or we're sorting on a field not supported by the default dropdown
39
38
  */
40
39
  const [showCustomSort, setShowCustomSort] = useState(sortEntries.length > 1 || (sortEntries.length > 0 && !ACCEPTED_SORTS.includes(sortEntries[0]?.split(' ')[0])));
41
- const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
42
40
  /**
43
41
  * This handles changing the sort if the basic sorter is used, OR enables the custom sorting.
44
42
  */
@@ -51,14 +49,17 @@ const HitSort = ({ size = 'small' }) => {
51
49
  }
52
50
  }, [setSavedSort, sort]);
53
51
  useEffect(() => {
54
- if (viewId) {
55
- const selectedView = views.find(_view => _view.view_id === viewId);
52
+ if (location.search.includes('sort')) {
53
+ return;
54
+ }
55
+ (async () => {
56
+ const selectedView = await getCurrentView(true);
56
57
  if (selectedView?.sort && !location.search.includes('sort')) {
57
58
  setSavedSort(selectedView.sort);
58
59
  }
59
- }
60
+ })();
60
61
  // eslint-disable-next-line react-hooks/exhaustive-deps
61
- }, [views, viewId]);
62
+ }, [getCurrentView]);
62
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, {}));
63
64
  };
64
65
  export default memo(HitSort);
@@ -10,7 +10,7 @@ import HitSort from './HitSort';
10
10
  import SearchSpan from './SearchSpan';
11
11
  const QuerySettings = ({ verticalSorters = false, boxSx }) => {
12
12
  const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
13
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(val => val.view_id === viewId));
13
+ const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
14
14
  return (_jsxs(Box, { sx: boxSx ?? { position: 'relative', maxWidth: '1200px' }, children: [_jsxs(Stack, { direction: verticalSorters ? 'column' : 'row', justifyContent: "space-between", spacing: 1, divider: !verticalSorters && _jsx(Divider, { flexItem: true, orientation: "vertical" }), sx: [
15
15
  viewId &&
16
16
  !selectedView && {
@@ -2,9 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Autocomplete, TextField } from '@mui/material';
3
3
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
4
4
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
5
- import { memo, useEffect, useMemo } from 'react';
5
+ import { memo, useEffect } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
- import { useLocation, useParams } from 'react-router-dom';
7
+ import { useLocation } from 'react-router-dom';
8
8
  import { useContextSelector } from 'use-context-selector';
9
9
  import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
10
10
  const DATE_RANGES = [
@@ -18,23 +18,27 @@ const DATE_RANGES = [
18
18
  const SearchSpan = ({ omitCustom = false, size }) => {
19
19
  const { t } = useTranslation();
20
20
  const location = useLocation();
21
- const routeParams = useParams();
22
21
  const span = useContextSelector(ParameterContext, ctx => ctx.span);
23
22
  const setSpan = useContextSelector(ParameterContext, ctx => ctx.setSpan);
24
- const viewId = useMemo(() => (location.pathname.startsWith('/views') ? routeParams.id : null), [location.pathname, routeParams.id]);
25
- const selectedView = useContextSelector(ViewContext, ctx => ctx.views?.find(_view => _view.view_id === viewId));
23
+ const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
26
24
  useEffect(() => {
27
- if (!selectedView?.span || location.search.includes('span')) {
25
+ if (location.search.includes('span')) {
28
26
  return;
29
27
  }
30
- if (selectedView.span.includes(':')) {
31
- setSpan(convertLuceneToDate(selectedView.span));
32
- }
33
- else {
34
- setSpan(selectedView.span);
35
- }
28
+ (async () => {
29
+ const viewSpan = (await getCurrentView(true))?.span;
30
+ if (!viewSpan) {
31
+ return;
32
+ }
33
+ if (viewSpan.includes(':')) {
34
+ setSpan(convertLuceneToDate(viewSpan));
35
+ }
36
+ else {
37
+ setSpan(viewSpan);
38
+ }
39
+ })();
36
40
  // eslint-disable-next-line react-hooks/exhaustive-deps
37
- }, [selectedView]);
41
+ }, [getCurrentView]);
38
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 }));
39
43
  };
40
44
  export default memo(SearchSpan);
@@ -16,9 +16,12 @@ const VISUALIZATIONS = ['assessment', 'created', 'escalation', 'status', 'detect
16
16
  const AddNewCard = ({ dashboard, addCard }) => {
17
17
  const { t } = useTranslation();
18
18
  const views = useContextSelector(ViewContext, ctx => ctx.views ?? []);
19
+ const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
19
20
  const [selectedType, setSelectedType] = useState('');
20
21
  const [analytics, setAnalytics] = useState([]);
21
22
  const [config, _setConfig] = useState({});
23
+ const [viewOpen, setViewOpen] = useState(false);
24
+ const [viewLoading, setViewLoading] = useState(false);
22
25
  const setConfig = useCallback((key, value) => _setConfig(_config => ({ ..._config, [key]: value })), []);
23
26
  const _addCard = useCallback(() => {
24
27
  if (!selectedType) {
@@ -43,9 +46,19 @@ const AddNewCard = ({ dashboard, addCard }) => {
43
46
  _setConfig({});
44
47
  }
45
48
  }, [selectedType]);
49
+ const onViewOpen = useCallback(async () => {
50
+ setViewOpen(true);
51
+ setViewLoading(true);
52
+ try {
53
+ await fetchViews();
54
+ }
55
+ finally {
56
+ setViewLoading(false);
57
+ }
58
+ }, [fetchViews]);
46
59
  return (_jsx(Grid, { item: true, xs: 12, md: 6, children: _jsxs(Card, { variant: "outlined", sx: { height: '100%' }, children: [_jsx(CardHeader, { title: t('route.home.add'), subheader: _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('route.home.add.description') }) }), _jsx(CardContent, { children: _jsxs(Stack, { spacing: 1, children: [_jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.type') }), _jsx(Select, { value: selectedType, onChange: event => setSelectedType(event.target.value), label: t('route.home.add.type'), children: Object.keys(TYPES).map(type => (_jsx(MenuItem, { value: type, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.type.${type}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.type.${type}.description`) })] }) }, type))) })] }), selectedType && _jsx(Divider, { flexItem: true }), selectedType === 'analytic' && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: t('route.home.add.analytic.title') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('route.home.add.analytic.description') }), _jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('analyticId', opt.analytic_id), options: analytics, filterOptions: (options, state) => options.filter(opt => opt.name.toLowerCase().includes(state.inputValue.toLowerCase()) ||
47
60
  opt.description?.split('\n')[0]?.toLowerCase().includes(state.inputValue.toLowerCase())), renderOption: (props, option) => (_createElement("li", { ...props, key: option.analytic_id },
48
- _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: option.name }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.description?.split('\n')[0] })] }))), getOptionLabel: option => option.name, renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.analytic') }) }), _jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.visualization') }), _jsx(Select, { value: config.type ?? '', onChange: event => setConfig('type', event.target.value), label: t('route.home.add.visualization'), children: VISUALIZATIONS.map(viz => (_jsx(MenuItem, { value: viz, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.visualization.${viz}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.visualization.${viz}.description`) })] }) }, viz))) })] })] })), selectedType === 'view' && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('viewId', opt.view_id), options: views, filterOptions: (options, state) => options.filter(opt => !dashboard?.find(entry => entry.type === 'view' && JSON.parse(entry.config).viewId === opt.view_id) &&
61
+ _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: option.name }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.description?.split('\n')[0] })] }))), getOptionLabel: option => option.name, renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.analytic') }) }), _jsxs(FormControl, { sx: theme => ({ mt: `${theme.spacing(2)} !important` }), children: [_jsx(InputLabel, { children: t('route.home.add.visualization') }), _jsx(Select, { value: config.type ?? '', onChange: event => setConfig('type', event.target.value), label: t('route.home.add.visualization'), children: VISUALIZATIONS.map(viz => (_jsx(MenuItem, { value: viz, children: _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(`route.home.add.visualization.${viz}`) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t(`route.home.add.visualization.${viz}.description`) })] }) }, viz))) })] })] })), selectedType === 'view' && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { sx: { pt: 1 }, onChange: (__, opt) => setConfig('viewId', opt.view_id), onOpen: onViewOpen, onClose: () => setViewOpen(false), open: viewOpen, loading: viewLoading, options: Object.values(views), filterOptions: (options, state) => options.filter(opt => !dashboard?.find(entry => entry.type === 'view' && JSON.parse(entry.config).viewId === opt.view_id) &&
49
62
  (opt.title.toLowerCase().includes(state.inputValue.toLowerCase()) ||
50
63
  opt.query.toLowerCase().includes(state.inputValue.toLowerCase()))), renderOption: (props, option) => (_createElement("li", { ...props, key: option.view_id },
51
64
  _jsxs(Stack, { children: [_jsx(Typography, { variant: "body1", children: t(option.title) }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: option.query })] }))), getOptionLabel: option => t(option.title), renderInput: params => _jsx(TextField, { ...params, label: t('route.home.add.view') }) }), _jsx(Typography, { variant: "body1", sx: { pt: 1 }, children: t('route.home.add.limit') }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('route.home.add.limit.description') }), _jsx(Box, { sx: { px: 0.5 }, children: _jsx(Slider, { value: config.limit ?? 3, valueLabelDisplay: "auto", onChange: (_, value) => setConfig('limit', value), min: 1, max: 10, step: 1, marks: true }) })] })), _jsx(Stack, { direction: "row", justifyContent: "end", children: _jsx(CustomButton, { variant: "outlined", size: "small", color: "primary", startIcon: _jsx(Check, {}), disabled: !selectedType || TYPES[selectedType]?.filter(field => !config[field])?.length > 0, onClick: _addCard, children: t('create') }) })] }) })] }) }));
@@ -17,7 +17,11 @@ const ViewCard = ({ viewId, limit }) => {
17
17
  const { dispatchApi } = useMyApi();
18
18
  const [hits, setHits] = useState([]);
19
19
  const [loading, setLoading] = useState(false);
20
- const view = useContextSelector(ViewContext, ctx => ctx.views?.find(_view => _view.view_id === viewId));
20
+ const view = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
21
+ const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
22
+ useEffect(() => {
23
+ fetchViews([viewId]);
24
+ }, [fetchViews, viewId]);
21
25
  useEffect(() => {
22
26
  if (!view?.query) {
23
27
  return;
@@ -77,7 +77,7 @@ const Home = () => {
77
77
  api.search.hit
78
78
  .post({
79
79
  query: updateQuery,
80
- rows: 5
80
+ rows: 0
81
81
  })
82
82
  .then(result => setUpdatedHitTotal(result.total));
83
83
  }, [updateQuery]);
@@ -36,7 +36,7 @@ const ViewComposer = () => {
36
36
  const navigate = useNavigate();
37
37
  const addView = useContextSelector(ViewContext, ctx => ctx.addView);
38
38
  const editView = useContextSelector(ViewContext, ctx => ctx.editView);
39
- const views = useContextSelector(ViewContext, ctx => ctx.views);
39
+ const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
40
40
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
41
41
  const loadHits = useContextSelector(HitContext, ctx => ctx.loadHits);
42
42
  // view state
@@ -130,29 +130,30 @@ const ViewComposer = () => {
130
130
  // eslint-disable-next-line react-hooks/exhaustive-deps
131
131
  }, [sort, span]);
132
132
  useEffect(() => {
133
- if (routeParams.id) {
134
- const viewToEdit = views?.find(_view => _view.view_id === routeParams.id);
135
- if (!viewToEdit && views?.length > 0) {
133
+ if (!routeParams.id) {
134
+ return;
135
+ }
136
+ (async () => {
137
+ const viewToEdit = await getCurrentView();
138
+ if (!viewToEdit) {
136
139
  setError('route.views.missing');
137
140
  return;
138
141
  }
139
142
  else {
140
143
  setError(null);
141
144
  }
142
- if (viewToEdit) {
143
- setTitle(viewToEdit.title);
144
- setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
145
- setQuery(viewToEdit.query);
146
- if (viewToEdit.sort) {
147
- setSort(viewToEdit.sort);
148
- }
149
- if (viewToEdit.span) {
150
- setSpan(viewToEdit.span);
151
- }
145
+ setTitle(viewToEdit.title);
146
+ setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
147
+ setQuery(viewToEdit.query);
148
+ if (viewToEdit.sort) {
149
+ setSort(viewToEdit.sort);
152
150
  }
153
- }
151
+ if (viewToEdit.span) {
152
+ setSpan(viewToEdit.span);
153
+ }
154
+ })();
154
155
  // eslint-disable-next-line react-hooks/exhaustive-deps
155
- }, [routeParams.id, views]);
156
+ }, [routeParams.id, getCurrentView]);
156
157
  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) => {
157
158
  if (_type) {
158
159
  setType(_type);
@@ -1,3 +1,4 @@
1
+ import { createElement as _createElement } from "react";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { Clear, Edit, SavedSearch, Star, StarBorder } from '@mui/icons-material';
3
4
  import { Autocomplete, Card, Checkbox, IconButton, Skeleton, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
@@ -12,6 +13,7 @@ import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemM
12
13
  import { ViewTitle } from '@cccsaurora/howler-ui/components/elements/view/ViewTitle';
13
14
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
14
15
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
16
+ import { isNull, omitBy, size } from 'lodash-es';
15
17
  import React, { useCallback, useContext, useEffect, useState } from 'react';
16
18
  import { useTranslation } from 'react-i18next';
17
19
  import { Link, useNavigate, useSearchParams } from 'react-router-dom';
@@ -24,8 +26,8 @@ const ViewsBase = () => {
24
26
  const { user } = useAppUser();
25
27
  const navigate = useNavigate();
26
28
  const { dispatchApi } = useMyApi();
27
- const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
28
29
  const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
30
+ const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
29
31
  const removeFavourite = useContextSelector(ViewContext, ctx => ctx.removeFavourite);
30
32
  const removeView = useContextSelector(ViewContext, ctx => ctx.removeView);
31
33
  const views = useContextSelector(ViewContext, ctx => ctx.views);
@@ -41,6 +43,8 @@ const ViewsBase = () => {
41
43
  const [hasError, setHasError] = useState(false);
42
44
  const [searching, setSearching] = useState(false);
43
45
  const [favouritesOnly, setFavouritesOnly] = useState(false);
46
+ const [defaultViewOpen, setDefaultViewOpen] = useState(false);
47
+ const [defaultViewLoading, setDefaultViewLoading] = useState(false);
44
48
  const onSearch = useCallback(async () => {
45
49
  try {
46
50
  setSearching(true);
@@ -52,7 +56,6 @@ const ViewsBase = () => {
52
56
  searchParams.delete('phrase');
53
57
  }
54
58
  setSearchParams(searchParams, { replace: true });
55
- fetchViews(true);
56
59
  const searchTerm = phrase ? `*${sanitizeLuceneQuery(phrase)}*` : '*';
57
60
  const phraseQuery = FIELDS_TO_SEARCH.map(_field => `${_field}:${searchTerm}`).join(' OR ');
58
61
  const typeQuery = `(type:global OR owner:(${user.username} OR none)) AND type:(${types.join(' OR ') || '*'}${types.includes('personal') ? ' OR readonly' : ''})`;
@@ -73,7 +76,6 @@ const ViewsBase = () => {
73
76
  phrase,
74
77
  setSearchParams,
75
78
  searchParams,
76
- fetchViews,
77
79
  user.username,
78
80
  user.favourite_views,
79
81
  types,
@@ -105,29 +107,37 @@ const ViewsBase = () => {
105
107
  const onDelete = useCallback(async (event, id) => {
106
108
  event.preventDefault();
107
109
  event.stopPropagation();
108
- await dispatchApi(removeView(id));
110
+ await removeView(id);
109
111
  onSearch();
110
- }, [dispatchApi, onSearch, removeView]);
112
+ }, [onSearch, removeView]);
111
113
  const onFavourite = useCallback(async (event, id) => {
112
114
  event.preventDefault();
113
115
  if (user.favourite_views?.includes(id)) {
114
- await dispatchApi(removeFavourite(id));
116
+ await removeFavourite(id);
115
117
  if (user.favourite_views?.length < 2) {
116
118
  setFavouritesOnly(false);
117
119
  }
118
120
  }
119
121
  else {
120
- await dispatchApi(addFavourite(id));
122
+ await addFavourite(id);
123
+ }
124
+ }, [addFavourite, removeFavourite, user.favourite_views]);
125
+ const onDefaultViewOpen = useCallback(async () => {
126
+ setDefaultViewOpen(true);
127
+ setDefaultViewLoading(true);
128
+ try {
129
+ await fetchViews();
121
130
  }
122
- }, [addFavourite, dispatchApi, removeFavourite, user.favourite_views]);
131
+ finally {
132
+ setDefaultViewLoading(false);
133
+ }
134
+ }, [fetchViews]);
123
135
  useEffect(() => {
124
- onSearch();
125
136
  if (!searchParams.has('offset')) {
126
137
  searchParams.set('offset', '0');
127
138
  setSearchParams(searchParams, { replace: true });
128
139
  }
129
- // eslint-disable-next-line react-hooks/exhaustive-deps
130
- }, [dispatchApi, types]);
140
+ }, [searchParams, setSearchParams]);
131
141
  useEffect(() => {
132
142
  if (response?.total <= offset) {
133
143
  setOffset(0);
@@ -140,13 +150,14 @@ const ViewsBase = () => {
140
150
  onSearch();
141
151
  }
142
152
  // eslint-disable-next-line react-hooks/exhaustive-deps
143
- }, [offset, favouritesOnly]);
153
+ }, [offset, favouritesOnly, types]);
144
154
  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: types, onChange: (__, _types) => {
145
155
  if (_types) {
146
156
  setTypes(_types.length < 2 ? _types : []);
147
157
  }
148
- }, 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: views?.length > 0 ? (_jsx(Autocomplete, { options: views, renderOption: (props, o) => (_jsx("li", { ...props, children: _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()) ||
149
- v.query.toLowerCase().includes(inputValue.toLowerCase())), getOptionLabel: (v) => t(v.title), isOptionEqualToValue: (view, value) => view.view_id === value.view_id, value: views?.find(v => v.view_id === 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') ||
158
+ }, 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 },
159
+ _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()) ||
160
+ 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') ||
150
161
  (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 } }) }));
151
162
  };
152
163
  const Views = () => {
package/package.json CHANGED
@@ -96,7 +96,7 @@
96
96
  "internal-slot": "1.0.7"
97
97
  },
98
98
  "type": "module",
99
- "version": "2.13.0-dev.75",
99
+ "version": "2.13.0-dev.77",
100
100
  "exports": {
101
101
  "./i18n": "./i18n.js",
102
102
  "./index.css": "./index.css",