mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search/virtualize list (#23710)
* Search: Add FixedSizeList for result items * Search: Move SectionHeader to a separate file * Search: Add useListHeight hook * Search: Fix horizontal scrollbar * Search: Remove custom scrollbar * Search: Do not fetch dashboard folder on route change * Search: Update tests * Search: Remove extra checkbox renders * Search: Move wrapper ref outside search results * Search: Fix param type * Search: Fix merge conflicts * Search: Virtualize dashboard list * Search: Update layout * Search: Pass wrapper to search results * Search: Update dashboard redirect * Search: Remove unused css * Search: Revert config * Search: Use AutoSizer * Search: Remove redundant appEvents call * Search: Use List layout in folder view
This commit is contained in:
parent
cf23f15a08
commit
fb8a555f19
@ -124,7 +124,7 @@ export class CustomScrollbar extends Component<Props> {
|
||||
autoHideTimeout={autoHideTimeout}
|
||||
hideTracksWhenNotNeeded={hideTracksWhenNotNeeded}
|
||||
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||
// Before these where set to inherit but that caused problems with cut of legends in firefox
|
||||
autoHeightMax={autoHeightMax}
|
||||
autoHeightMin={autoHeightMin}
|
||||
renderTrackHorizontal={this.renderTrackHorizontal}
|
||||
|
@ -4,3 +4,4 @@ export const getRouteParamsId = (state: LocationState) => state.routeParams.id;
|
||||
export const getRouteParamsPage = (state: LocationState) => state.routeParams.page;
|
||||
export const getRouteParams = (state: LocationState) => state.routeParams;
|
||||
export const getLocationQuery = (state: LocationState) => state.query;
|
||||
export const getUrl = (state: LocationState) => state.url;
|
||||
|
@ -5,7 +5,7 @@ import { NavModel, locationUtil } from '@grafana/data';
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { StoreState } from 'app/types';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getRouteParams } from 'app/core/selectors/location';
|
||||
import { getRouteParams, getUrl } from 'app/core/selectors/location';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { loadFolderPage } from '../loaders';
|
||||
import { ManageDashboards } from './ManageDashboards';
|
||||
@ -13,18 +13,19 @@ import { ManageDashboards } from './ManageDashboards';
|
||||
interface Props {
|
||||
navModel: NavModel;
|
||||
uid?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const DashboardListPage: FC<Props> = memo(({ navModel, uid }) => {
|
||||
export const DashboardListPage: FC<Props> = memo(({ navModel, uid, url }) => {
|
||||
const { loading, value } = useAsync(() => {
|
||||
if (!uid) {
|
||||
if (!uid || !url.startsWith('/dashboards')) {
|
||||
return Promise.resolve({ pageNavModel: navModel });
|
||||
}
|
||||
return loadFolderPage(uid, 'manage-folder-dashboards').then(({ folder, model }) => {
|
||||
const url = locationUtil.stripBaseFromUrl(folder.url);
|
||||
return loadFolderPage(uid!, 'manage-folder-dashboards').then(({ folder, model }) => {
|
||||
const path = locationUtil.stripBaseFromUrl(folder.url);
|
||||
|
||||
if (url !== location.pathname) {
|
||||
getLocationSrv().update({ path: url });
|
||||
if (path !== location.pathname) {
|
||||
getLocationSrv().update({ path });
|
||||
}
|
||||
|
||||
return { id: folder.id, pageNavModel: { ...navModel, ...model } };
|
||||
@ -40,9 +41,12 @@ export const DashboardListPage: FC<Props> = memo(({ navModel, uid }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const mapStateToProps: MapStateToProps<Props, {}, StoreState> = state => ({
|
||||
const mapStateToProps: MapStateToProps<Props, {}, StoreState> = state => {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'manage-dashboards'),
|
||||
uid: getRouteParams(state.location).uid as string | undefined,
|
||||
});
|
||||
url: getUrl(state.location),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(DashboardListPage);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, memo } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { useTheme, CustomScrollbar, stylesFactory, Button } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
@ -14,7 +14,7 @@ export interface Props {
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
export const DashboardSearch: FC<Props> = ({ onCloseSearch, folder }) => {
|
||||
export const DashboardSearch: FC<Props> = memo(({ onCloseSearch, folder }) => {
|
||||
const payload = folder ? { query: `folder:${folder}` } : {};
|
||||
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange } = useSearchQuery(payload);
|
||||
const { results, loading, onToggleSection, onKeyDown } = useDashboardSearch(query, onCloseSearch);
|
||||
@ -74,7 +74,7 @@ export const DashboardSearch: FC<Props> = ({ onCloseSearch, folder }) => {
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { FC, useState, memo } from 'react';
|
||||
import React, { FC, memo, useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { Icon, TagList, HorizontalGroup, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { HorizontalGroup, Icon, stylesFactory, TagList, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
@ -56,7 +56,8 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
|
||||
onMoveItems,
|
||||
} = useManageDashboards(query, { hasEditPermissionInFolders: contextSrv.hasEditPermissionInFolders }, folderUid);
|
||||
|
||||
const { layout, setLayout } = useSearchLayout(query);
|
||||
const defaultLayout = folderId ? SearchLayout.List : SearchLayout.Folders;
|
||||
const { layout, setLayout } = useSearchLayout(query, defaultLayout);
|
||||
|
||||
const onMoveTo = () => {
|
||||
setIsMoveModalOpen(true);
|
||||
@ -89,7 +90,8 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard-list">
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<SearchField query={query} onChange={onQueryChange} className={styles.searchField} />
|
||||
<DashboardActions isEditor={isEditor} canEdit={hasEditPermissionInFolders || canSave} folderId={folderId} />
|
||||
@ -140,8 +142,9 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="search-results">
|
||||
<div className={styles.results}>
|
||||
{results?.length > 0 && (
|
||||
<SearchResultsFilter
|
||||
allChecked={allChecked}
|
||||
@ -187,6 +190,13 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
height: 100%;
|
||||
|
||||
.results-container {
|
||||
padding: 5px 0 0;
|
||||
}
|
||||
`,
|
||||
searchField: css`
|
||||
height: auto;
|
||||
border-bottom: none;
|
||||
@ -196,5 +206,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
width: 400px;
|
||||
}
|
||||
`,
|
||||
results: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
margin-top: ${theme.spacing.xl};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, useCallback, useRef, useEffect } from 'react';
|
||||
import React, { FC, useCallback, useRef, useEffect, CSSProperties } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
@ -12,11 +12,12 @@ export interface Props {
|
||||
editable?: boolean;
|
||||
onTagSelected: (name: string) => any;
|
||||
onToggleChecked?: OnToggleChecked;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const { selectors } = e2e.pages.Dashboards;
|
||||
|
||||
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected }) => {
|
||||
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected, style }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getResultsItemStyles(theme);
|
||||
const inputEl = useRef<HTMLInputElement>(null);
|
||||
@ -60,6 +61,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
||||
|
||||
return (
|
||||
<li
|
||||
style={style}
|
||||
aria-label={selectors.dashboards(item.title)}
|
||||
className={cx(styles.wrapper, { [styles.selected]: item.selected })}
|
||||
>
|
||||
@ -83,7 +85,6 @@ const getResultsItemStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
${styleMixins.listItem(theme)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: ${theme.spacing.xxs};
|
||||
padding: 0 ${theme.spacing.sm};
|
||||
min-height: 37px;
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, MutableRefObject } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Icon, stylesFactory, useTheme, IconName, IconButton, Spinner } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { stylesFactory, useTheme, Spinner } from '@grafana/ui';
|
||||
import { DashboardSection, OnToggleChecked, SearchLayout } from '../types';
|
||||
import { getVisibleItems } from '../utils';
|
||||
import { ITEM_HEIGHT } from '../constants';
|
||||
import { SearchItem } from './SearchItem';
|
||||
import { SearchCheckbox } from './SearchCheckbox';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
|
||||
export interface Props {
|
||||
editable?: boolean;
|
||||
@ -14,8 +16,9 @@ export interface Props {
|
||||
onTagSelected: (name: string) => any;
|
||||
onToggleChecked?: OnToggleChecked;
|
||||
onToggleSection: (section: DashboardSection) => void;
|
||||
results: DashboardSection[] | undefined;
|
||||
results: DashboardSection[];
|
||||
layout?: string;
|
||||
wrapperRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export const SearchResults: FC<Props> = ({
|
||||
@ -25,19 +28,53 @@ export const SearchResults: FC<Props> = ({
|
||||
onToggleChecked,
|
||||
onToggleSection,
|
||||
results,
|
||||
wrapperRef,
|
||||
layout,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getSectionStyles(theme);
|
||||
const itemProps = { editable, onToggleChecked, onTagSelected };
|
||||
|
||||
const renderItems = (section: DashboardSection) => {
|
||||
if (!section.expanded && layout !== SearchLayout.List) {
|
||||
return null;
|
||||
}
|
||||
const renderFolders = () => {
|
||||
return (
|
||||
<ul className={styles.wrapper}>
|
||||
{results.map(section => {
|
||||
return (
|
||||
<li aria-label="Search section" className={styles.section} key={section.title}>
|
||||
<SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section }} />
|
||||
<ul aria-label="Search items">
|
||||
{section.expanded && section.items.map(item => <SearchItem key={item.id} {...itemProps} item={item} />)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
return section.items.map(item => (
|
||||
<SearchItem key={item.id} {...{ item, editable, onToggleChecked, onTagSelected }} />
|
||||
));
|
||||
const items = getVisibleItems(results);
|
||||
|
||||
const renderDashboards = () => {
|
||||
return (
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<FixedSizeList
|
||||
aria-label="Search items"
|
||||
className={styles.wrapper}
|
||||
innerElementType="ul"
|
||||
itemSize={ITEM_HEIGHT}
|
||||
height={height}
|
||||
itemCount={items.length}
|
||||
width="100%"
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const item = items[index];
|
||||
return <SearchItem key={item.id} {...itemProps} item={item} style={style} />;
|
||||
}}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@ -45,28 +82,15 @@ export const SearchResults: FC<Props> = ({
|
||||
} else if (!results || !results.length) {
|
||||
return <h6>No dashboards matching your query were found.</h6>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="search-results-container">
|
||||
<ul className={styles.wrapper}>
|
||||
{results.map(section =>
|
||||
layout !== SearchLayout.List ? (
|
||||
<li aria-label="Search section" className={styles.section} key={section.title}>
|
||||
<SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section, layout }} />
|
||||
<ul aria-label="Search items" className={styles.wrapper}>
|
||||
{renderItems(section)}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
renderItems(section)
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
<div className={cx('results-container', styles.resultsContainer)}>
|
||||
{layout !== SearchLayout.List ? renderFolders() : renderDashboards()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const { xs, sm, md } = theme.spacing;
|
||||
return {
|
||||
wrapper: css`
|
||||
list-style: none;
|
||||
@ -74,7 +98,7 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
section: css`
|
||||
background: ${theme.colors.panelBg};
|
||||
border-bottom: solid 1px ${theme.isLight ? theme.palette.gray95 : theme.palette.gray25};
|
||||
padding: 0px 4px 4px 4px;
|
||||
padding: 0px ${xs} ${xs};
|
||||
margin-bottom: 3px;
|
||||
`,
|
||||
spinner: css`
|
||||
@ -83,94 +107,15 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface SectionHeaderProps {
|
||||
editable?: boolean;
|
||||
onSectionClick: (section: DashboardSection) => void;
|
||||
onToggleChecked?: OnToggleChecked;
|
||||
section: DashboardSection;
|
||||
}
|
||||
|
||||
const SectionHeader: FC<SectionHeaderProps> = ({ section, onSectionClick, onToggleChecked, editable = false }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getSectionHeaderStyles(theme, section.selected);
|
||||
|
||||
const onSectionExpand = () => {
|
||||
onSectionClick(section);
|
||||
};
|
||||
|
||||
const onSectionChecked = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onToggleChecked) {
|
||||
onToggleChecked(section);
|
||||
}
|
||||
};
|
||||
|
||||
return !section.hideHeader ? (
|
||||
<div className={styles.wrapper} onClick={onSectionExpand}>
|
||||
<SearchCheckbox editable={editable} checked={section.checked} onClick={onSectionChecked} />
|
||||
<Icon className={styles.icon} name={section.icon as IconName} />
|
||||
|
||||
<span className={styles.text}>{section.title}</span>
|
||||
{section.url && (
|
||||
<a
|
||||
href={section.url}
|
||||
className={styles.link}
|
||||
onClick={() => appEvents.emit(CoreEvents.hideDashSearch, { target: 'search-item' })}
|
||||
>
|
||||
<IconButton name="cog" className={styles.button} />
|
||||
</a>
|
||||
)}
|
||||
<Icon name={section.expanded ? 'angle-down' : 'angle-right'} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.wrapper} />
|
||||
);
|
||||
};
|
||||
|
||||
const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false) => {
|
||||
const { sm, xs } = theme.spacing;
|
||||
return {
|
||||
wrapper: cx(
|
||||
css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${theme.typography.size.base};
|
||||
padding: ${sm} ${xs} ${xs};
|
||||
color: ${theme.colors.textWeak};
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pointer',
|
||||
{ selected }
|
||||
),
|
||||
icon: css`
|
||||
width: 43px;
|
||||
`,
|
||||
text: css`
|
||||
flex-grow: 1;
|
||||
line-height: 24px;
|
||||
`,
|
||||
link: css`
|
||||
padding: 2px 10px 0;
|
||||
color: ${theme.colors.textWeak};
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease-in-out;
|
||||
`,
|
||||
button: css`
|
||||
margin-top: 3px;
|
||||
resultsContainer: css`
|
||||
padding: ${sm};
|
||||
position: relative;
|
||||
flex-grow: 10;
|
||||
margin-bottom: ${md};
|
||||
background: ${theme.palette.gray10};
|
||||
border: 1px solid ${theme.palette.gray15};
|
||||
border-radius: 3px;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
99
public/app/features/search/components/SectionHeader.tsx
Normal file
99
public/app/features/search/components/SectionHeader.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Icon, IconButton, IconName, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { DashboardSection, OnToggleChecked } from '../types';
|
||||
import { SearchCheckbox } from './SearchCheckbox';
|
||||
|
||||
interface SectionHeaderProps {
|
||||
editable?: boolean;
|
||||
onSectionClick: (section: DashboardSection) => void;
|
||||
onToggleChecked?: OnToggleChecked;
|
||||
section: DashboardSection;
|
||||
}
|
||||
|
||||
export const SectionHeader: FC<SectionHeaderProps> = ({
|
||||
section,
|
||||
onSectionClick,
|
||||
onToggleChecked,
|
||||
editable = false,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getSectionHeaderStyles(theme, section.selected);
|
||||
|
||||
const onSectionExpand = () => {
|
||||
onSectionClick(section);
|
||||
};
|
||||
|
||||
const onSectionChecked = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onToggleChecked) {
|
||||
onToggleChecked(section);
|
||||
}
|
||||
},
|
||||
[section]
|
||||
);
|
||||
|
||||
return !section.hideHeader ? (
|
||||
<div className={styles.wrapper} onClick={onSectionExpand}>
|
||||
<SearchCheckbox editable={editable} checked={section.checked} onClick={onSectionChecked} />
|
||||
<Icon className={styles.icon} name={section.icon as IconName} />
|
||||
|
||||
<span className={styles.text}>{section.title}</span>
|
||||
{section.url && (
|
||||
<a href={section.url} className={styles.link}>
|
||||
<IconButton name="cog" className={styles.button} />
|
||||
</a>
|
||||
)}
|
||||
<Icon name={section.expanded ? 'angle-down' : 'angle-right'} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.wrapper} />
|
||||
);
|
||||
};
|
||||
|
||||
const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false) => {
|
||||
const { sm, xs } = theme.spacing;
|
||||
return {
|
||||
wrapper: cx(
|
||||
css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${theme.typography.size.base};
|
||||
padding: ${sm} ${xs} ${xs};
|
||||
color: ${theme.colors.textWeak};
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pointer',
|
||||
{ selected }
|
||||
),
|
||||
icon: css`
|
||||
width: 43px;
|
||||
`,
|
||||
text: css`
|
||||
flex-grow: 1;
|
||||
line-height: 24px;
|
||||
`,
|
||||
link: css`
|
||||
padding: 2px 10px 0;
|
||||
color: ${theme.colors.textWeak};
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease-in-out;
|
||||
`,
|
||||
button: css`
|
||||
margin-top: 3px;
|
||||
`,
|
||||
};
|
||||
});
|
@ -1,2 +1,4 @@
|
||||
export const NO_ID_SECTIONS = ['Recent', 'Starred'];
|
||||
// Height of the search result item
|
||||
export const ITEM_HEIGHT = 40;
|
||||
export const DEFAULT_SORT = { label: 'A-Z', value: 'alpha-asc' };
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SearchLayout } from '../types';
|
||||
import { DashboardQuery, SearchLayout } from '../types';
|
||||
|
||||
export const layoutOptions = [
|
||||
{ label: 'Folders', value: SearchLayout.Folders, icon: 'folder' },
|
||||
{ label: 'List', value: SearchLayout.List, icon: 'list-ul' },
|
||||
];
|
||||
|
||||
export const useSearchLayout = (query: any) => {
|
||||
const [layout, setLayout] = useState<string>(layoutOptions[0].value);
|
||||
export const useSearchLayout = (query: DashboardQuery, defaultLayout = SearchLayout.Folders) => {
|
||||
const [layout, setLayout] = useState<string>(defaultLayout);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.sort) {
|
||||
|
@ -28,6 +28,18 @@ export const getFlattenedSections = (sections: DashboardSection[]): string[] =>
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all items for currently expanded sections
|
||||
* @param sections
|
||||
*/
|
||||
export const getVisibleItems = (sections: DashboardSection[]) => {
|
||||
return sections.flatMap(section => {
|
||||
if (section.expanded) {
|
||||
return section.items;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Since Recent and Starred folders don't have id, title field is used as id
|
||||
* @param title - title field of the section
|
||||
|
Loading…
Reference in New Issue
Block a user