mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
NestedFolders: Paginate nested folder picker (#71489)
* Paginate nested folder picker items * Prevent dashboards from appearing in the folder picker * fix tests * remove SkeletonGroup * more excludeKinds * cleanup
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useId, useMemo } from 'react';
|
||||
import React, { useCallback, useId, useMemo, useRef } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, useStyles2 } from '@grafana/ui';
|
||||
@@ -22,6 +24,9 @@ interface NestedFolderListProps {
|
||||
selectedFolder: FolderUID | undefined;
|
||||
onFolderClick: (uid: string, newOpenState: boolean) => void;
|
||||
onSelectionChange: (event: React.FormEvent<HTMLInputElement>, item: DashboardViewItem) => void;
|
||||
|
||||
isItemLoaded: (itemIndex: number) => boolean;
|
||||
requestLoadMore: (folderUid: string | undefined) => void;
|
||||
}
|
||||
|
||||
export function NestedFolderList({
|
||||
@@ -30,7 +35,10 @@ export function NestedFolderList({
|
||||
selectedFolder,
|
||||
onFolderClick,
|
||||
onSelectionChange,
|
||||
isItemLoaded,
|
||||
requestLoadMore,
|
||||
}: NestedFolderListProps) {
|
||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const virtualData = useMemo(
|
||||
@@ -38,18 +46,44 @@ export function NestedFolderList({
|
||||
[items, foldersAreOpenable, selectedFolder, onFolderClick, onSelectionChange]
|
||||
);
|
||||
|
||||
const handleIsItemLoaded = useCallback(
|
||||
(itemIndex: number) => {
|
||||
return isItemLoaded(itemIndex);
|
||||
},
|
||||
[isItemLoaded]
|
||||
);
|
||||
|
||||
const handleLoadMore = useCallback(
|
||||
(startIndex: number, endIndex: number) => {
|
||||
const { parentUID } = items[startIndex];
|
||||
requestLoadMore(parentUID);
|
||||
},
|
||||
[requestLoadMore, items]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
{items.length > 0 ? (
|
||||
<List
|
||||
height={ROW_HEIGHT * Math.min(6.5, items.length)}
|
||||
width="100%"
|
||||
itemData={virtualData}
|
||||
itemSize={ROW_HEIGHT}
|
||||
{items.length ? (
|
||||
<InfiniteLoader
|
||||
ref={infiniteLoaderRef}
|
||||
itemCount={items.length}
|
||||
isItemLoaded={handleIsItemLoaded}
|
||||
loadMoreItems={handleLoadMore}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
{({ onItemsRendered, ref }) => (
|
||||
<List
|
||||
ref={ref}
|
||||
height={ROW_HEIGHT * Math.min(6.5, items.length)}
|
||||
width="100%"
|
||||
itemData={virtualData}
|
||||
itemSize={ROW_HEIGHT}
|
||||
itemCount={items.length}
|
||||
onItemsRendered={onItemsRendered}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
) : (
|
||||
<div className={styles.emptyMessage}>
|
||||
<Trans i18nKey="browse-dashboards.folder-picker.empty-message">No folders found</Trans>
|
||||
@@ -59,7 +93,7 @@ export function NestedFolderList({
|
||||
);
|
||||
}
|
||||
|
||||
interface VirtualData extends NestedFolderListProps {}
|
||||
interface VirtualData extends Omit<NestedFolderListProps, 'isItemLoaded' | 'requestLoadMore'> {}
|
||||
|
||||
interface RowProps {
|
||||
index: number;
|
||||
@@ -67,6 +101,8 @@ interface RowProps {
|
||||
data: VirtualData;
|
||||
}
|
||||
|
||||
const SKELETON_WIDTHS = [100, 200, 130, 160, 150];
|
||||
|
||||
function Row({ index, style: virtualStyles, data }: RowProps) {
|
||||
const { items, foldersAreOpenable, selectedFolder, onFolderClick, onSelectionChange } = data;
|
||||
const { item, isOpen, level } = items[index];
|
||||
@@ -102,10 +138,19 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
|
||||
[item.uid, foldersAreOpenable, onFolderClick]
|
||||
);
|
||||
|
||||
if (item.kind === 'ui' && item.uiKind === 'pagination-placeholder') {
|
||||
return (
|
||||
<span style={virtualStyles} className={styles.row}>
|
||||
<Indent level={level} />
|
||||
<Skeleton width={SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.kind !== 'folder') {
|
||||
return process.env.NODE_ENV !== 'production' ? (
|
||||
<span style={virtualStyles} className={styles.row}>
|
||||
Non-folder item {item.uid}
|
||||
Non-folder {item.kind} {item.uid}
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -78,9 +78,11 @@ describe('NestedFolderPicker', () => {
|
||||
|
||||
it('clicking the button opens the folder picker', async () => {
|
||||
render(<NestedFolderPicker onChange={mockOnChange} />);
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
|
||||
// Open the picker and wait for children to load
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
await userEvent.click(button);
|
||||
await screen.findByLabelText(folderA.item.title);
|
||||
|
||||
// Select folder button is no longer visible
|
||||
expect(screen.queryByRole('button', { name: 'Select folder' })).not.toBeInTheDocument();
|
||||
@@ -95,9 +97,11 @@ describe('NestedFolderPicker', () => {
|
||||
|
||||
it('can select a folder from the picker', async () => {
|
||||
render(<NestedFolderPicker onChange={mockOnChange} />);
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
|
||||
// Open the picker and wait for children to load
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
await userEvent.click(button);
|
||||
await screen.findByLabelText(folderA.item.title);
|
||||
|
||||
await userEvent.click(screen.getByLabelText(folderA.item.title));
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
@@ -108,9 +112,11 @@ describe('NestedFolderPicker', () => {
|
||||
|
||||
it('can expand and collapse a folder to show its children', async () => {
|
||||
render(<NestedFolderPicker onChange={mockOnChange} />);
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
|
||||
// Open the picker and wait for children to load
|
||||
const button = await screen.findByRole('button', { name: 'Select folder' });
|
||||
await userEvent.click(button);
|
||||
await screen.findByLabelText(folderA.item.title);
|
||||
|
||||
// Expand Folder A
|
||||
await userEvent.click(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` }));
|
||||
|
||||
@@ -9,20 +9,25 @@ import { Alert, Button, Icon, Input, LoadingBar, useStyles2 } from '@grafana/ui'
|
||||
import { Text } from '@grafana/ui/src/components/Text/Text';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { skipToken, useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
import { listFolders, PAGE_SIZE } from 'app/features/browse-dashboards/api/services';
|
||||
import { createFlatTree } from 'app/features/browse-dashboards/state';
|
||||
import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services';
|
||||
import {
|
||||
childrenByParentUIDSelector,
|
||||
createFlatTree,
|
||||
fetchNextChildrenPage,
|
||||
rootItemsSelector,
|
||||
useBrowseLoadingStatus,
|
||||
useLoadNextChildrenPage,
|
||||
} from 'app/features/browse-dashboards/state';
|
||||
import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils';
|
||||
import { DashboardViewItemCollection } from 'app/features/browse-dashboards/types';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service';
|
||||
import { queryResultToViewItem } from 'app/features/search/service/utils';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { NestedFolderList } from './NestedFolderList';
|
||||
import { FolderChange, FolderUID } from './types';
|
||||
|
||||
async function fetchRootFolders() {
|
||||
return await listFolders(undefined, undefined, 1, PAGE_SIZE);
|
||||
}
|
||||
|
||||
interface NestedFolderPickerProps {
|
||||
value?: FolderUID;
|
||||
// TODO: think properly (and pragmatically) about how to communicate moving to general folder,
|
||||
@@ -30,15 +35,19 @@ interface NestedFolderPickerProps {
|
||||
onChange?: (folder: FolderChange) => void;
|
||||
}
|
||||
|
||||
const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const];
|
||||
|
||||
export function NestedFolderPicker({ value, onChange }: NestedFolderPickerProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const selectedFolder = useGetFolderQuery(value || skipToken);
|
||||
|
||||
const rootStatus = useBrowseLoadingStatus(undefined);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||
const [folderOpenState, setFolderOpenState] = useState<Record<string, boolean>>({});
|
||||
const [childrenForUID, setChildrenForUID] = useState<Record<string, DashboardViewItem[]>>({});
|
||||
const rootFoldersState = useAsync(fetchRootFolders);
|
||||
const selectedFolder = useGetFolderQuery(value || skipToken);
|
||||
const [error] = useState<Error | undefined>(undefined); // TODO: error not populated anymore
|
||||
|
||||
const searchState = useAsync(async () => {
|
||||
if (!search) {
|
||||
@@ -56,72 +65,19 @@ export function NestedFolderPicker({ value, onChange }: NestedFolderPickerProps)
|
||||
return { ...queryResponse, items };
|
||||
}, [search]);
|
||||
|
||||
const handleFolderClick = useCallback(async (uid: string, newOpenState: boolean) => {
|
||||
setFolderOpenState((old) => ({ ...old, [uid]: newOpenState }));
|
||||
const handleFolderClick = useCallback(
|
||||
async (uid: string, newOpenState: boolean) => {
|
||||
setFolderOpenState((old) => ({ ...old, [uid]: newOpenState }));
|
||||
|
||||
if (newOpenState) {
|
||||
const folders = await listFolders(uid, undefined, 1, PAGE_SIZE);
|
||||
setChildrenForUID((old) => ({ ...old, [uid]: folders }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const flatTree = useMemo(() => {
|
||||
const searchResults = search && searchState.value;
|
||||
const rootCollection: DashboardViewItemCollection = searchResults
|
||||
? {
|
||||
isFullyLoaded: searchResults.items.length === searchResults.totalRows,
|
||||
lastKindHasMoreItems: false, // not relevent for search
|
||||
lastFetchedKind: 'folder', // not relevent for search
|
||||
lastFetchedPage: 1, // not relevent for search
|
||||
items: searchResults.items ?? [],
|
||||
}
|
||||
: {
|
||||
isFullyLoaded: !rootFoldersState.loading,
|
||||
lastKindHasMoreItems: false,
|
||||
lastFetchedKind: 'folder',
|
||||
lastFetchedPage: 1,
|
||||
items: rootFoldersState.value ?? [],
|
||||
};
|
||||
|
||||
const childrenCollections: Record<string, DashboardViewItemCollection | undefined> = {};
|
||||
|
||||
if (!searchResults) {
|
||||
// We don't expand folders when searching
|
||||
for (const parentUID in childrenForUID) {
|
||||
const children = childrenForUID[parentUID];
|
||||
childrenCollections[parentUID] = {
|
||||
isFullyLoaded: !!children,
|
||||
lastKindHasMoreItems: false,
|
||||
lastFetchedKind: 'folder',
|
||||
lastFetchedPage: 1,
|
||||
items: children,
|
||||
};
|
||||
if (newOpenState) {
|
||||
dispatch(fetchNextChildrenPage({ parentUID: uid, pageSize: PAGE_SIZE, excludeKinds: EXCLUDED_KINDS }));
|
||||
}
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const result = createFlatTree(
|
||||
undefined,
|
||||
rootCollection,
|
||||
childrenCollections,
|
||||
searchResults ? {} : folderOpenState,
|
||||
searchResults ? 0 : 1,
|
||||
false
|
||||
);
|
||||
|
||||
if (!searchResults) {
|
||||
result.unshift({
|
||||
isOpen: true,
|
||||
level: 0,
|
||||
item: {
|
||||
kind: 'folder',
|
||||
title: 'Dashboards',
|
||||
uid: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [search, searchState.value, rootFoldersState.loading, rootFoldersState.value, folderOpenState, childrenForUID]);
|
||||
const rootCollection = useSelector(rootItemsSelector);
|
||||
const childrenCollections = useSelector(childrenByParentUIDSelector);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(event: React.FormEvent<HTMLInputElement>, item: DashboardViewItem) => {
|
||||
@@ -151,10 +107,73 @@ export function NestedFolderPicker({ value, onChange }: NestedFolderPickerProps)
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = rootFoldersState.loading || searchState.loading;
|
||||
const error = rootFoldersState.error || searchState.error;
|
||||
const baseHandleLoadMore = useLoadNextChildrenPage(EXCLUDED_KINDS);
|
||||
const handleLoadMore = useCallback(
|
||||
(folderUID: string | undefined) => {
|
||||
if (search) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tree = flatTree;
|
||||
baseHandleLoadMore(folderUID);
|
||||
},
|
||||
[search, baseHandleLoadMore]
|
||||
);
|
||||
|
||||
const flatTree = useMemo(() => {
|
||||
const searchResults = search && searchState.value;
|
||||
|
||||
if (searchResults) {
|
||||
const searchCollection: DashboardViewItemCollection = {
|
||||
isFullyLoaded: true, //searchResults.items.length === searchResults.totalRows,
|
||||
lastKindHasMoreItems: false, // TODO: paginate search
|
||||
lastFetchedKind: 'folder', // TODO: paginate search
|
||||
lastFetchedPage: 1, // TODO: paginate search
|
||||
items: searchResults.items ?? [],
|
||||
};
|
||||
|
||||
return createFlatTree(undefined, searchCollection, childrenCollections, {}, 0, EXCLUDED_KINDS);
|
||||
}
|
||||
|
||||
let flatTree = createFlatTree(undefined, rootCollection, childrenCollections, folderOpenState, 0, EXCLUDED_KINDS);
|
||||
|
||||
// Increase the level of each item to 'make way' for the fake root Dashboards item
|
||||
for (const item of flatTree) {
|
||||
item.level += 1;
|
||||
}
|
||||
|
||||
flatTree.unshift({
|
||||
isOpen: true,
|
||||
level: 0,
|
||||
item: {
|
||||
kind: 'folder',
|
||||
title: 'Dashboards',
|
||||
uid: '',
|
||||
},
|
||||
});
|
||||
|
||||
// If the root collection hasn't loaded yet, create loading placeholders
|
||||
if (!rootCollection) {
|
||||
flatTree = flatTree.concat(getPaginationPlaceholders(PAGE_SIZE, undefined, 0));
|
||||
}
|
||||
|
||||
return flatTree;
|
||||
}, [search, searchState.value, rootCollection, childrenCollections, folderOpenState]);
|
||||
|
||||
const isItemLoaded = useCallback(
|
||||
(itemIndex: number) => {
|
||||
const treeItem = flatTree[itemIndex];
|
||||
if (!treeItem) {
|
||||
return false;
|
||||
}
|
||||
const item = treeItem.item;
|
||||
const result = !(item.kind === 'ui' && item.uiKind === 'pagination-placeholder');
|
||||
|
||||
return result;
|
||||
},
|
||||
[flatTree]
|
||||
);
|
||||
|
||||
const isLoading = rootStatus === 'pending' || searchState.loading;
|
||||
|
||||
let label = selectedFolder.data?.title;
|
||||
if (value === '') {
|
||||
@@ -218,11 +237,13 @@ export function NestedFolderPicker({ value, onChange }: NestedFolderPickerProps)
|
||||
)}
|
||||
|
||||
<NestedFolderList
|
||||
items={tree}
|
||||
items={flatTree}
|
||||
selectedFolder={value}
|
||||
onFolderClick={handleFolderClick}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
foldersAreOpenable={!(search && searchState.value)}
|
||||
isItemLoaded={isItemLoaded}
|
||||
requestLoadMore={handleLoadMore}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,11 +3,15 @@ import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/ty
|
||||
import { createAsyncThunk } from 'app/types';
|
||||
|
||||
import { listDashboards, listFolders, PAGE_SIZE } from '../api/services';
|
||||
import { DashboardViewItemWithUIItems, UIDashboardViewItem } from '../types';
|
||||
|
||||
import { findItem } from './utils';
|
||||
|
||||
interface FetchNextChildrenPageArgs {
|
||||
parentUID: string | undefined;
|
||||
|
||||
// Allow UI items to be excluded (they're always excluded) for convenience for callers
|
||||
excludeKinds?: Array<DashboardViewItemWithUIItems['kind'] | UIDashboardViewItem['uiKind']>;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
@@ -87,9 +91,12 @@ export const refetchChildren = createAsyncThunk(
|
||||
export const fetchNextChildrenPage = createAsyncThunk(
|
||||
'browseDashboards/fetchNextChildrenPage',
|
||||
async (
|
||||
{ parentUID, pageSize }: FetchNextChildrenPageArgs,
|
||||
{ parentUID, excludeKinds = [], pageSize }: FetchNextChildrenPageArgs,
|
||||
thunkAPI
|
||||
): Promise<undefined | FetchNextChildrenPageResult> => {
|
||||
// TODO: invert prop to `includeKinds`, but also support not loading folders
|
||||
const loadDashboards = !excludeKinds.includes('dashboard');
|
||||
|
||||
const uid = parentUID === GENERAL_FOLDER_UID ? undefined : parentUID;
|
||||
|
||||
const state = thunkAPI.getState().browseDashboards;
|
||||
@@ -107,13 +114,13 @@ export const fetchNextChildrenPage = createAsyncThunk(
|
||||
fetchKind = 'folder';
|
||||
} else if (collection.lastFetchedKind === 'dashboard' && !collection.lastKindHasMoreItems) {
|
||||
// There's nothing to load at all
|
||||
console.warn(`FetchedChildren called for ${uid} but that collection is fully loaded`);
|
||||
console.warn(`fetchNextChildrenPage called for ${uid} but that collection is fully loaded`);
|
||||
// return;
|
||||
} else if (collection.lastFetchedKind === 'folder' && collection.lastKindHasMoreItems) {
|
||||
// Load additional pages of folders
|
||||
page = collection.lastFetchedPage + 1;
|
||||
fetchKind = 'folder';
|
||||
} else {
|
||||
} else if (loadDashboards) {
|
||||
// We've already checked if there's more folders to load, so if the last fetched is folder
|
||||
// then we fetch first page of dashboards
|
||||
page = collection.lastFetchedKind === 'folder' ? 1 : collection.lastFetchedPage + 1;
|
||||
@@ -133,7 +140,7 @@ export const fetchNextChildrenPage = createAsyncThunk(
|
||||
|
||||
// If we've loaded all folders, load the first page of dashboards.
|
||||
// This ensures dashboards are loaded if a folder contains only dashboards.
|
||||
if (fetchKind === 'folder' && lastPageOfKind) {
|
||||
if (fetchKind === 'folder' && lastPageOfKind && loadDashboards) {
|
||||
fetchKind = 'dashboard';
|
||||
page = 1;
|
||||
|
||||
|
||||
@@ -5,9 +5,16 @@ import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useSelector, StoreState, useDispatch } from 'app/types';
|
||||
|
||||
import { PAGE_SIZE } from '../api/services';
|
||||
import { BrowseDashboardsState, DashboardsTreeItem, DashboardTreeSelection } from '../types';
|
||||
import {
|
||||
BrowseDashboardsState,
|
||||
DashboardsTreeItem,
|
||||
DashboardTreeSelection,
|
||||
DashboardViewItemWithUIItems,
|
||||
UIDashboardViewItem,
|
||||
} from '../types';
|
||||
|
||||
import { fetchNextChildrenPage } from './actions';
|
||||
import { getPaginationPlaceholders } from './utils';
|
||||
|
||||
export const rootItemsSelector = (wholeState: StoreState) => wholeState.browseDashboards.rootItems;
|
||||
export const childrenByParentUIDSelector = (wholeState: StoreState) => wholeState.browseDashboards.childrenByParentUID;
|
||||
@@ -97,7 +104,9 @@ export function useActionSelectionState() {
|
||||
return useSelector((state) => selectedItemsForActionsSelector(state));
|
||||
}
|
||||
|
||||
export function useLoadNextChildrenPage() {
|
||||
export function useLoadNextChildrenPage(
|
||||
excludeKinds: Array<DashboardViewItemWithUIItems['kind'] | UIDashboardViewItem['uiKind']> = []
|
||||
) {
|
||||
const dispatch = useDispatch();
|
||||
const requestInFlightRef = useRef(false);
|
||||
|
||||
@@ -109,12 +118,12 @@ export function useLoadNextChildrenPage() {
|
||||
|
||||
requestInFlightRef.current = true;
|
||||
|
||||
const promise = dispatch(fetchNextChildrenPage({ parentUID: folderUID, pageSize: PAGE_SIZE }));
|
||||
const promise = dispatch(fetchNextChildrenPage({ parentUID: folderUID, excludeKinds, pageSize: PAGE_SIZE }));
|
||||
promise.finally(() => (requestInFlightRef.current = false));
|
||||
|
||||
return promise;
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, excludeKinds]
|
||||
);
|
||||
|
||||
return handleLoadMore;
|
||||
@@ -135,21 +144,25 @@ export function createFlatTree(
|
||||
childrenByUID: BrowseDashboardsState['childrenByParentUID'],
|
||||
openFolders: Record<string, boolean>,
|
||||
level = 0,
|
||||
insertEmptyFolderIndicator = true
|
||||
excludeKinds: Array<DashboardViewItemWithUIItems['kind'] | UIDashboardViewItem['uiKind']> = []
|
||||
): DashboardsTreeItem[] {
|
||||
function mapItem(item: DashboardViewItem, parentUID: string | undefined, level: number): DashboardsTreeItem[] {
|
||||
if (excludeKinds.includes(item.kind)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const mappedChildren = createFlatTree(
|
||||
item.uid,
|
||||
rootCollection,
|
||||
childrenByUID,
|
||||
openFolders,
|
||||
level + 1,
|
||||
insertEmptyFolderIndicator
|
||||
excludeKinds
|
||||
);
|
||||
|
||||
const isOpen = Boolean(openFolders[item.uid]);
|
||||
const emptyFolder = childrenByUID[item.uid]?.items.length === 0;
|
||||
if (isOpen && emptyFolder && insertEmptyFolderIndicator) {
|
||||
if (isOpen && emptyFolder && !excludeKinds.includes('empty-folder')) {
|
||||
mappedChildren.push({
|
||||
isOpen: false,
|
||||
level: level + 1,
|
||||
@@ -186,18 +199,3 @@ export function createFlatTree(
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function getPaginationPlaceholders(amount: number, parentUID: string | undefined, level: number) {
|
||||
return new Array(amount).fill(null).map((_, index) => {
|
||||
return {
|
||||
parentUID,
|
||||
level,
|
||||
isOpen: false,
|
||||
item: {
|
||||
kind: 'ui' as const,
|
||||
uiKind: 'pagination-placeholder' as const,
|
||||
uid: `${parentUID}-pagination-${index}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function fetchNextChildrenPageFulfilled(
|
||||
}
|
||||
|
||||
const { children, page, kind, lastPageOfKind } = payload;
|
||||
const { parentUID } = action.meta.arg;
|
||||
const { parentUID, excludeKinds = [] } = action.meta.arg;
|
||||
|
||||
const collection = parentUID ? state.childrenByParentUID[parentUID] : state.rootItems;
|
||||
const prevItems = collection?.items ?? [];
|
||||
@@ -50,7 +50,7 @@ export function fetchNextChildrenPageFulfilled(
|
||||
lastFetchedKind: kind,
|
||||
lastFetchedPage: page,
|
||||
lastKindHasMoreItems: !lastPageOfKind,
|
||||
isFullyLoaded: kind === 'dashboard' && lastPageOfKind,
|
||||
isFullyLoaded: !excludeKinds.includes('dashboard') ? kind === 'dashboard' && lastPageOfKind : lastPageOfKind,
|
||||
};
|
||||
|
||||
if (!parentUID) {
|
||||
|
||||
@@ -28,3 +28,18 @@ export function findItem(
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getPaginationPlaceholders(amount: number, parentUID: string | undefined, level: number) {
|
||||
return new Array(amount).fill(null).map((_, index) => {
|
||||
return {
|
||||
parentUID,
|
||||
level,
|
||||
isOpen: false,
|
||||
item: {
|
||||
kind: 'ui' as const,
|
||||
uiKind: 'pagination-placeholder' as const,
|
||||
uid: `${parentUID}-pagination-${index}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface UIDashboardViewItem {
|
||||
uid: string;
|
||||
}
|
||||
|
||||
type DashboardViewItemWithUIItems = DashboardViewItem | UIDashboardViewItem;
|
||||
export type DashboardViewItemWithUIItems = DashboardViewItem | UIDashboardViewItem;
|
||||
|
||||
export interface DashboardsTreeItem<T extends DashboardViewItemWithUIItems = DashboardViewItemWithUIItems> {
|
||||
item: T;
|
||||
|
||||
Reference in New Issue
Block a user