Commit 3a2c844a by Alex Khomenko Committed by GitHub

Search: folder view improvements (#24324)

* Search: Do not set items if no results returned

* Search: Simplify canSave logic

* Search: Add initialLoading state

* Search: Add itemsFetching state to folder

* Search: Fix props and tests

* Search: Fix strict null check
parent 078d08d0
...@@ -78,7 +78,7 @@ export class SearchSrv { ...@@ -78,7 +78,7 @@ export class SearchSrv {
if (query.layout === SearchLayout.List) { if (query.layout === SearchLayout.List) {
return backendSrv return backendSrv
.search({ ...query, type: DashboardSearchItemType.DashDB }) .search({ ...query, type: DashboardSearchItemType.DashDB })
.then(results => [{ items: results }]); .then(results => (results.length ? [{ items: results }] : []));
} }
if (!filters) { if (!filters) {
......
...@@ -4,7 +4,7 @@ import { HorizontalGroup, LinkButton } from '@grafana/ui'; ...@@ -4,7 +4,7 @@ import { HorizontalGroup, LinkButton } from '@grafana/ui';
export interface Props { export interface Props {
folderId?: number; folderId?: number;
isEditor: boolean; isEditor: boolean;
canEdit: boolean; canEdit?: boolean;
} }
export const DashboardActions: FC<Props> = ({ folderId, isEditor, canEdit }) => { export const DashboardActions: FC<Props> = ({ folderId, isEditor, canEdit }) => {
......
...@@ -28,14 +28,14 @@ export const DashboardListPage: FC<Props> = memo(({ navModel, uid, url }) => { ...@@ -28,14 +28,14 @@ export const DashboardListPage: FC<Props> = memo(({ navModel, uid, url }) => {
getLocationSrv().update({ path }); getLocationSrv().update({ path });
} }
return { id: folder.id, pageNavModel: { ...navModel, ...model } }; return { folder, pageNavModel: { ...navModel, ...model } };
}); });
}, [uid]); }, [uid]);
return ( return (
<Page navModel={value?.pageNavModel}> <Page navModel={value?.pageNavModel}>
<Page.Contents isLoading={loading}> <Page.Contents isLoading={loading}>
<ManageDashboards folderUid={uid} folderId={value?.id} /> <ManageDashboards folder={value?.folder} />
</Page.Contents> </Page.Contents>
</Page> </Page>
); );
......
import React, { FC, memo, useState } from 'react'; import React, { FC, memo, useState } from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import { HorizontalGroup, stylesFactory, useTheme } from '@grafana/ui'; import { HorizontalGroup, stylesFactory, useTheme, Spinner } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { FolderDTO } from 'app/types';
import { useManageDashboards } from '../hooks/useManageDashboards';
import { SearchLayout } from '../types';
import { ConfirmDeleteModal } from './ConfirmDeleteModal'; import { ConfirmDeleteModal } from './ConfirmDeleteModal';
import { MoveToFolderModal } from './MoveToFolderModal'; import { MoveToFolderModal } from './MoveToFolderModal';
import { useSearchQuery } from '../hooks/useSearchQuery'; import { useSearchQuery } from '../hooks/useSearchQuery';
import { useManageDashboards } from '../hooks/useManageDashboards';
import { SearchResultsFilter } from './SearchResultsFilter'; import { SearchResultsFilter } from './SearchResultsFilter';
import { SearchResults } from './SearchResults'; import { SearchResults } from './SearchResults';
import { DashboardActions } from './DashboardActions'; import { DashboardActions } from './DashboardActions';
import { SearchLayout } from '../types';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
export interface Props { export interface Props {
folderId?: number; folder?: FolderDTO;
folderUid?: string;
} }
const { isEditor } = contextSrv; const { isEditor } = contextSrv;
export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => { export const ManageDashboards: FC<Props> = memo(({ folder }) => {
const folderId = folder?.id;
const folderUid = folder?.uid;
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme); const styles = getStyles(theme);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
...@@ -47,6 +49,7 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => { ...@@ -47,6 +49,7 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
const { const {
results, results,
loading, loading,
initialLoading,
canSave, canSave,
allChecked, allChecked,
hasEditPermissionInFolders, hasEditPermissionInFolders,
...@@ -57,7 +60,8 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => { ...@@ -57,7 +60,8 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
onToggleAllChecked, onToggleAllChecked,
onDeleteItems, onDeleteItems,
onMoveItems, onMoveItems,
} = useManageDashboards(query, { hasEditPermissionInFolders: contextSrv.hasEditPermissionInFolders }, folderUid); noFolders,
} = useManageDashboards(query, {}, folder);
const onMoveTo = () => { const onMoveTo = () => {
setIsMoveModalOpen(true); setIsMoveModalOpen(true);
...@@ -67,7 +71,11 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => { ...@@ -67,7 +71,11 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}; };
if (canSave && folderId && !hasFilters && results.length === 0 && !loading) { if (initialLoading) {
return <Spinner className={styles.spinner} />;
}
if (noFolders && !hasFilters) {
return ( return (
<EmptyListCTA <EmptyListCTA
title="This folder doesn't have any dashboards yet" title="This folder doesn't have any dashboards yet"
...@@ -150,5 +158,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -150,5 +158,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
height: 100%; height: 100%;
margin-top: ${theme.spacing.xl}; margin-top: ${theme.spacing.xl};
`, `,
spinner: css`
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
`,
}; };
}); });
...@@ -4,7 +4,7 @@ import { FixedSizeList } from 'react-window'; ...@@ -4,7 +4,7 @@ import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme, Spinner } from '@grafana/ui'; import { stylesFactory, useTheme, Spinner } from '@grafana/ui';
import { DashboardSection, OnToggleChecked, SearchLayout, DashboardSearchHit } from '../types'; import { DashboardSection, OnToggleChecked, SearchLayout } from '../types';
import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants'; import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants';
import { SearchItem } from './SearchItem'; import { SearchItem } from './SearchItem';
import { SectionHeader } from './SectionHeader'; import { SectionHeader } from './SectionHeader';
...@@ -15,7 +15,7 @@ export interface Props { ...@@ -15,7 +15,7 @@ export interface Props {
onTagSelected: (name: string) => any; onTagSelected: (name: string) => any;
onToggleChecked?: OnToggleChecked; onToggleChecked?: OnToggleChecked;
onToggleSection: (section: DashboardSection) => void; onToggleSection: (section: DashboardSection) => void;
results: DashboardSearchHit[]; results: DashboardSection[];
layout?: string; layout?: string;
} }
......
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { Icon, IconButton, stylesFactory, useTheme } from '@grafana/ui'; import { Icon, IconButton, Spinner, stylesFactory, useTheme } from '@grafana/ui';
import { DashboardSection, OnToggleChecked } from '../types'; import { DashboardSection, OnToggleChecked } from '../types';
import { SearchCheckbox } from './SearchCheckbox'; import { SearchCheckbox } from './SearchCheckbox';
import { getSectionIcon } from '../utils'; import { getSectionIcon } from '../utils';
...@@ -51,7 +51,7 @@ export const SectionHeader: FC<SectionHeaderProps> = ({ ...@@ -51,7 +51,7 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
<IconButton name="cog" className={styles.button} /> <IconButton name="cog" className={styles.button} />
</a> </a>
)} )}
<Icon name={section.expanded ? 'angle-down' : 'angle-right'} /> {section.itemsFetching ? <Spinner /> : <Icon name={section.expanded ? 'angle-down' : 'angle-right'} />}
</div> </div>
); );
}; };
......
import { useMemo, useReducer } from 'react'; import { useMemo, useReducer } from 'react';
import { backendSrv } from 'app/core/services/backend_srv'; import { FolderDTO } from 'app/types';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardQuery, DashboardSection, OnDeleteItems, OnMoveItems, OnToggleChecked } from '../types'; import { DashboardQuery, DashboardSection, OnDeleteItems, OnMoveItems, OnToggleChecked } from '../types';
import { import { DELETE_ITEMS, MOVE_ITEMS, TOGGLE_ALL_CHECKED, TOGGLE_CHECKED } from '../reducers/actionTypes';
DELETE_ITEMS,
MOVE_ITEMS,
TOGGLE_ALL_CHECKED,
TOGGLE_CHECKED,
TOGGLE_CAN_SAVE,
TOGGLE_EDIT_PERMISSIONS,
} from '../reducers/actionTypes';
import { manageDashboardsReducer, manageDashboardsState, ManageDashboardsState } from '../reducers/manageDashboards'; import { manageDashboardsReducer, manageDashboardsState, ManageDashboardsState } from '../reducers/manageDashboards';
import { useSearch } from './useSearch'; import { useSearch } from './useSearch';
export const useManageDashboards = ( export const useManageDashboards = (
query: DashboardQuery, query: DashboardQuery,
state: Partial<ManageDashboardsState> = {}, state: Partial<ManageDashboardsState> = {},
folderUid?: string folder?: FolderDTO
) => { ) => {
const reducer = useReducer(manageDashboardsReducer, { const reducer = useReducer(manageDashboardsReducer, {
...manageDashboardsState, ...manageDashboardsState,
...state, ...state,
}); });
const searchCallback = (folderUid: string | undefined) => {
if (folderUid) {
backendSrv.getFolderByUid(folderUid).then(folder => {
dispatch({ type: TOGGLE_CAN_SAVE, payload: folder.canSave });
if (!folder.canSave) {
dispatch({ type: TOGGLE_EDIT_PERMISSIONS, payload: false });
}
});
}
};
const { const {
state: { results, loading, canSave, allChecked, hasEditPermissionInFolders }, state: { results, loading, initialLoading, allChecked },
onToggleSection, onToggleSection,
dispatch, dispatch,
} = useSearch<ManageDashboardsState>(query, reducer, { folderUid, searchCallback }); } = useSearch<ManageDashboardsState>(query, reducer, {});
const onToggleChecked: OnToggleChecked = item => { const onToggleChecked: OnToggleChecked = item => {
dispatch({ type: TOGGLE_CHECKED, payload: item }); dispatch({ type: TOGGLE_CHECKED, payload: item });
...@@ -64,9 +47,14 @@ export const useManageDashboards = ( ...@@ -64,9 +47,14 @@ export const useManageDashboards = (
results, results,
]); ]);
const canSave = folder?.canSave;
const hasEditPermissionInFolders = canSave ? contextSrv.hasEditPermissionInFolders : false;
const noFolders = canSave && folder?.id && results.length === 0 && !loading && !initialLoading;
return { return {
results, results,
loading, loading,
initialLoading,
canSave, canSave,
allChecked, allChecked,
hasEditPermissionInFolders, hasEditPermissionInFolders,
...@@ -77,5 +65,6 @@ export const useManageDashboards = ( ...@@ -77,5 +65,6 @@ export const useManageDashboards = (
onToggleAllChecked, onToggleAllChecked,
onDeleteItems, onDeleteItems,
onMoveItems, onMoveItems,
noFolders,
}; };
}; };
...@@ -2,7 +2,7 @@ import { useEffect } from 'react'; ...@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { SearchSrv } from 'app/core/services/search_srv'; import { SearchSrv } from 'app/core/services/search_srv';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { FETCH_RESULTS, FETCH_ITEMS, TOGGLE_SECTION, SEARCH_START } from '../reducers/actionTypes'; import { FETCH_RESULTS, FETCH_ITEMS, TOGGLE_SECTION, SEARCH_START, FETCH_ITEMS_START } from '../reducers/actionTypes';
import { DashboardSection, UseSearch } from '../types'; import { DashboardSection, UseSearch } from '../types';
import { hasId, getParsedQuery } from '../utils'; import { hasId, getParsedQuery } from '../utils';
...@@ -16,8 +16,8 @@ const searchSrv = new SearchSrv(); ...@@ -16,8 +16,8 @@ const searchSrv = new SearchSrv();
* @param reducer - return result of useReducer * @param reducer - return result of useReducer
* @param params - custom params * @param params - custom params
*/ */
export const useSearch: UseSearch = (query, reducer, params) => { export const useSearch: UseSearch = (query, reducer, params = {}) => {
const { queryParsing, folderUid, searchCallback } = params; const { queryParsing } = params;
const [state, dispatch] = reducer; const [state, dispatch] = reducer;
const search = () => { const search = () => {
...@@ -25,10 +25,6 @@ export const useSearch: UseSearch = (query, reducer, params) => { ...@@ -25,10 +25,6 @@ export const useSearch: UseSearch = (query, reducer, params) => {
const parsedQuery = getParsedQuery(query, queryParsing); const parsedQuery = getParsedQuery(query, queryParsing);
searchSrv.search(parsedQuery).then(results => { searchSrv.search(parsedQuery).then(results => {
dispatch({ type: FETCH_RESULTS, payload: results }); dispatch({ type: FETCH_RESULTS, payload: results });
if (searchCallback) {
searchCallback(folderUid);
}
}); });
}; };
...@@ -37,11 +33,11 @@ export const useSearch: UseSearch = (query, reducer, params) => { ...@@ -37,11 +33,11 @@ export const useSearch: UseSearch = (query, reducer, params) => {
dispatch({ type: SEARCH_START }); dispatch({ type: SEARCH_START });
}, [query.tag, query.sort, query.starred, query.layout]); }, [query.tag, query.sort, query.starred, query.layout]);
useDebounce(search, 300, [query, folderUid, queryParsing]); useDebounce(search, 300, [query, queryParsing]);
// TODO as possible improvement, show spinner after expanding section while items are fetching
const onToggleSection = (section: DashboardSection) => { const onToggleSection = (section: DashboardSection) => {
if (hasId(section.title) && !section.items.length) { if (hasId(section.title) && !section.items.length) {
dispatch({ type: FETCH_ITEMS_START, payload: section.id });
backendSrv.search({ folderIds: [section.id] }).then(items => { backendSrv.search({ folderIds: [section.id] }).then(items => {
dispatch({ type: FETCH_ITEMS, payload: { section, items } }); dispatch({ type: FETCH_ITEMS, payload: { section, items } });
dispatch({ type: TOGGLE_SECTION, payload: section }); dispatch({ type: TOGGLE_SECTION, payload: section });
......
export const FETCH_RESULTS = 'FETCH_RESULTS'; export const FETCH_RESULTS = 'FETCH_RESULTS';
export const TOGGLE_SECTION = 'TOGGLE_SECTION'; export const TOGGLE_SECTION = 'TOGGLE_SECTION';
export const FETCH_ITEMS = 'FETCH_ITEMS'; export const FETCH_ITEMS = 'FETCH_ITEMS';
export const FETCH_ITEMS_START = 'FETCH_ITEMS_START';
export const MOVE_SELECTION_UP = 'MOVE_SELECTION_UP'; export const MOVE_SELECTION_UP = 'MOVE_SELECTION_UP';
export const MOVE_SELECTION_DOWN = 'MOVE_SELECTION_DOWN'; export const MOVE_SELECTION_DOWN = 'MOVE_SELECTION_DOWN';
export const SEARCH_START = 'SEARCH_START'; export const SEARCH_START = 'SEARCH_START';
// Manage dashboards // Manage dashboards
export const TOGGLE_CAN_SAVE = 'TOGGLE_CAN_SAVE';
export const TOGGLE_EDIT_PERMISSIONS = 'TOGGLE_EDIT_PERMISSIONS';
export const TOGGLE_ALL_CHECKED = 'TOGGLE_ALL_CHECKED'; export const TOGGLE_ALL_CHECKED = 'TOGGLE_ALL_CHECKED';
export const TOGGLE_CHECKED = 'TOGGLE_SECTION_CHECKED'; export const TOGGLE_CHECKED = 'TOGGLE_SECTION_CHECKED';
export const MOVE_ITEMS = 'MOVE_ITEMS'; export const MOVE_ITEMS = 'MOVE_ITEMS';
......
...@@ -2,6 +2,7 @@ import { FETCH_ITEMS, FETCH_RESULTS, TOGGLE_SECTION, MOVE_SELECTION_DOWN, MOVE_S ...@@ -2,6 +2,7 @@ import { FETCH_ITEMS, FETCH_RESULTS, TOGGLE_SECTION, MOVE_SELECTION_DOWN, MOVE_S
import { searchReducer as reducer, dashboardsSearchState } from './dashboardSearch'; import { searchReducer as reducer, dashboardsSearchState } from './dashboardSearch';
import { searchResults, sections } from '../testData'; import { searchResults, sections } from '../testData';
const defaultState = { selectedIndex: 0, loading: false, results: sections as any[], initialLoading: false };
describe('Dashboard Search reducer', () => { describe('Dashboard Search reducer', () => {
it('should return the initial state', () => { it('should return the initial state', () => {
expect(reducer(dashboardsSearchState, {} as any)).toEqual(dashboardsSearchState); expect(reducer(dashboardsSearchState, {} as any)).toEqual(dashboardsSearchState);
...@@ -9,20 +10,14 @@ describe('Dashboard Search reducer', () => { ...@@ -9,20 +10,14 @@ describe('Dashboard Search reducer', () => {
it('should set the results and mark first item as selected', () => { it('should set the results and mark first item as selected', () => {
const newState = reducer(dashboardsSearchState, { type: FETCH_RESULTS, payload: searchResults }); const newState = reducer(dashboardsSearchState, { type: FETCH_RESULTS, payload: searchResults });
expect(newState).toEqual({ loading: false, selectedIndex: 0, results: searchResults }); expect(newState).toEqual({ loading: false, selectedIndex: 0, results: searchResults, initialLoading: false });
expect(newState.results[0].selected).toBeTruthy(); expect(newState.results[0].selected).toBeTruthy();
}); });
it('should toggle selected section', () => { it('should toggle selected section', () => {
const newState = reducer( const newState = reducer(defaultState, { type: TOGGLE_SECTION, payload: sections[5] });
{ selectedIndex: 0, loading: false, results: sections as any[] },
{ type: TOGGLE_SECTION, payload: sections[5] }
);
expect(newState.results[5].expanded).toBeFalsy(); expect(newState.results[5].expanded).toBeFalsy();
const newState2 = reducer( const newState2 = reducer(defaultState, { type: TOGGLE_SECTION, payload: sections[1] });
{ selectedIndex: 0, loading: false, results: sections as any[] },
{ type: TOGGLE_SECTION, payload: sections[1] }
);
expect(newState2.results[1].expanded).toBeTruthy(); expect(newState2.results[1].expanded).toBeTruthy();
}); });
...@@ -43,26 +38,20 @@ describe('Dashboard Search reducer', () => { ...@@ -43,26 +38,20 @@ describe('Dashboard Search reducer', () => {
isStarred: false, isStarred: false,
}, },
]; ];
const newState = reducer( const newState = reducer(defaultState, {
{ selectedIndex: 0, loading: false, results: sections as any[] }, type: FETCH_ITEMS,
{ payload: {
type: FETCH_ITEMS, section: sections[2],
payload: { items,
section: sections[2], },
items, });
},
}
);
expect(newState.results[2].items).toEqual(items); expect(newState.results[2].items).toEqual(items);
}); });
it('should handle MOVE_SELECTION_DOWN', () => { it('should handle MOVE_SELECTION_DOWN', () => {
const newState = reducer( const newState = reducer(defaultState, {
{ loading: false, selectedIndex: 0, results: sections as any[] }, type: MOVE_SELECTION_DOWN,
{ });
type: MOVE_SELECTION_DOWN,
}
);
expect(newState.selectedIndex).toEqual(1); expect(newState.selectedIndex).toEqual(1);
expect(newState.results[0].items[0].selected).toBeTruthy(); expect(newState.results[0].items[0].selected).toBeTruthy();
...@@ -76,7 +65,7 @@ describe('Dashboard Search reducer', () => { ...@@ -76,7 +65,7 @@ describe('Dashboard Search reducer', () => {
// Shouldn't go over the visible results length - 1 (9) // Shouldn't go over the visible results length - 1 (9)
const newState3 = reducer( const newState3 = reducer(
{ loading: false, selectedIndex: 9, results: sections as any[] }, { ...defaultState, selectedIndex: 9 },
{ {
type: MOVE_SELECTION_DOWN, type: MOVE_SELECTION_DOWN,
} }
...@@ -86,17 +75,14 @@ describe('Dashboard Search reducer', () => { ...@@ -86,17 +75,14 @@ describe('Dashboard Search reducer', () => {
it('should handle MOVE_SELECTION_UP', () => { it('should handle MOVE_SELECTION_UP', () => {
// shouldn't move beyond 0 // shouldn't move beyond 0
const newState = reducer( const newState = reducer(defaultState, {
{ loading: false, selectedIndex: 0, results: sections as any[] }, type: MOVE_SELECTION_UP,
{ });
type: MOVE_SELECTION_UP,
}
);
expect(newState.selectedIndex).toEqual(0); expect(newState.selectedIndex).toEqual(0);
const newState2 = reducer( const newState2 = reducer(
{ loading: false, selectedIndex: 3, results: sections as any[] }, { ...defaultState, selectedIndex: 3 },
{ {
type: MOVE_SELECTION_UP, type: MOVE_SELECTION_UP,
} }
......
import { DashboardSearchHit, DashboardSection, SearchAction } from '../types'; import { DashboardSection, SearchAction } from '../types';
import { getFlattenedSections, getLookupField, markSelected } from '../utils'; import { getFlattenedSections, getLookupField, markSelected } from '../utils';
import { import {
FETCH_ITEMS, FETCH_ITEMS,
...@@ -7,17 +7,21 @@ import { ...@@ -7,17 +7,21 @@ import {
MOVE_SELECTION_DOWN, MOVE_SELECTION_DOWN,
MOVE_SELECTION_UP, MOVE_SELECTION_UP,
SEARCH_START, SEARCH_START,
FETCH_ITEMS_START,
} from './actionTypes'; } from './actionTypes';
export interface DashboardsSearchState { export interface DashboardsSearchState {
results: DashboardSearchHit[]; results: DashboardSection[];
loading: boolean; loading: boolean;
selectedIndex: number; selectedIndex: number;
/** Used for first time page load */
initialLoading: boolean;
} }
export const dashboardsSearchState: DashboardsSearchState = { export const dashboardsSearchState: DashboardsSearchState = {
results: [], results: [],
loading: true, loading: true,
initialLoading: true,
selectedIndex: 0, selectedIndex: 0,
}; };
...@@ -34,7 +38,7 @@ export const searchReducer = (state: DashboardsSearchState, action: SearchAction ...@@ -34,7 +38,7 @@ export const searchReducer = (state: DashboardsSearchState, action: SearchAction
if (results.length > 0) { if (results.length > 0) {
results[0].selected = true; results[0].selected = true;
} }
return { ...state, results, loading: false }; return { ...state, results, loading: false, initialLoading: false };
} }
case TOGGLE_SECTION: { case TOGGLE_SECTION: {
const section = action.payload; const section = action.payload;
...@@ -53,14 +57,25 @@ export const searchReducer = (state: DashboardsSearchState, action: SearchAction ...@@ -53,14 +57,25 @@ export const searchReducer = (state: DashboardsSearchState, action: SearchAction
const { section, items } = action.payload; const { section, items } = action.payload;
return { return {
...state, ...state,
itemsFetching: false,
results: state.results.map((result: DashboardSection) => { results: state.results.map((result: DashboardSection) => {
if (section.id === result.id) { if (section.id === result.id) {
return { ...result, items }; return { ...result, items, itemsFetching: false };
} }
return result; return result;
}), }),
}; };
} }
case FETCH_ITEMS_START: {
const id = action.payload;
if (id) {
return {
...state,
results: state.results.map(result => (result.id === id ? { ...result, itemsFetching: true } : result)),
};
}
return state;
}
case MOVE_SELECTION_DOWN: { case MOVE_SELECTION_DOWN: {
const flatIds = getFlattenedSections(state.results); const flatIds = getFlattenedSections(state.results);
if (state.selectedIndex < flatIds.length - 1) { if (state.selectedIndex < flatIds.length - 1) {
......
import { import { TOGGLE_ALL_CHECKED, TOGGLE_CHECKED, DELETE_ITEMS, MOVE_ITEMS } from './actionTypes';
TOGGLE_CAN_SAVE,
TOGGLE_EDIT_PERMISSIONS,
TOGGLE_ALL_CHECKED,
TOGGLE_CHECKED,
DELETE_ITEMS,
MOVE_ITEMS,
} from './actionTypes';
import { manageDashboardsReducer as reducer, manageDashboardsState as state } from './manageDashboards'; import { manageDashboardsReducer as reducer, manageDashboardsState as state } from './manageDashboards';
import { sections } from '../testData'; import { sections } from '../testData';
import { UidsToDelete } from '../types'; import { UidsToDelete } from '../types';
...@@ -34,16 +27,6 @@ describe('Manage dashboards reducer', () => { ...@@ -34,16 +27,6 @@ describe('Manage dashboards reducer', () => {
expect(newState2.allChecked).toBe(false); expect(newState2.allChecked).toBe(false);
}); });
it('should handle TOGGLE_CAN_SAVE', () => {
const newState = reducer(state, { type: TOGGLE_CAN_SAVE, payload: true });
expect(newState.canSave).toBe(true);
});
it('should handle TOGGLE_EDIT_PERMISSIONS', () => {
const newState = reducer(state, { type: TOGGLE_EDIT_PERMISSIONS, payload: true });
expect(newState.hasEditPermissionInFolders).toBe(true);
});
it('should handle TOGGLE_CHECKED sections', () => { it('should handle TOGGLE_CHECKED sections', () => {
const newState = reducer({ ...state, results }, { type: TOGGLE_CHECKED, payload: results[0] }); const newState = reducer({ ...state, results }, { type: TOGGLE_CHECKED, payload: results[0] });
expect(newState.results[0].checked).toBe(true); expect(newState.results[0].checked).toBe(true);
......
import { DashboardSectionItem, SearchAction } from '../types'; import { DashboardSectionItem, SearchAction } from '../types';
import { import { TOGGLE_ALL_CHECKED, TOGGLE_CHECKED, MOVE_ITEMS, DELETE_ITEMS } from './actionTypes';
TOGGLE_CAN_SAVE,
TOGGLE_EDIT_PERMISSIONS,
TOGGLE_ALL_CHECKED,
TOGGLE_CHECKED,
MOVE_ITEMS,
DELETE_ITEMS,
} from './actionTypes';
import { dashboardsSearchState, DashboardsSearchState, searchReducer } from './dashboardSearch'; import { dashboardsSearchState, DashboardsSearchState, searchReducer } from './dashboardSearch';
import { mergeReducers } from '../utils'; import { mergeReducers } from '../utils';
export interface ManageDashboardsState extends DashboardsSearchState { export interface ManageDashboardsState extends DashboardsSearchState {
canSave: boolean;
allChecked: boolean; allChecked: boolean;
hasEditPermissionInFolders: boolean;
} }
export const manageDashboardsState: ManageDashboardsState = { export const manageDashboardsState: ManageDashboardsState = {
...dashboardsSearchState, ...dashboardsSearchState,
canSave: false,
allChecked: false, allChecked: false,
hasEditPermissionInFolders: false,
}; };
const reducer = (state: ManageDashboardsState, action: SearchAction) => { const reducer = (state: ManageDashboardsState, action: SearchAction) => {
switch (action.type) { switch (action.type) {
case TOGGLE_CAN_SAVE:
return { ...state, canSave: action.payload };
case TOGGLE_EDIT_PERMISSIONS:
return { ...state, hasEditPermissionInFolders: action.payload };
case TOGGLE_ALL_CHECKED: case TOGGLE_ALL_CHECKED:
const newAllChecked = !state.allChecked; const newAllChecked = !state.allChecked;
return { return {
......
...@@ -23,6 +23,7 @@ export interface DashboardSection { ...@@ -23,6 +23,7 @@ export interface DashboardSection {
selected?: boolean; selected?: boolean;
type: DashboardSearchItemType; type: DashboardSearchItemType;
slug?: string; slug?: string;
itemsFetching?: boolean;
} }
export interface DashboardSectionItem { export interface DashboardSectionItem {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment