mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardList/AlertList: Fix for missing All folder value (#39772)
* DashboardList/AlertList: Fix for missing All folder value * Refactor: Fixes case where folder does not exist in results
This commit is contained in:
parent
9de633d3a3
commit
7c7b21b39e
@ -200,6 +200,9 @@ export const Components = {
|
||||
FolderPicker: {
|
||||
container: 'Folder picker select container',
|
||||
},
|
||||
ReadonlyFolderPicker: {
|
||||
container: 'data-testid Readonly folder picker select container',
|
||||
},
|
||||
DataSourcePicker: {
|
||||
container: 'Data source picker select container',
|
||||
input: () => 'input[id="data-source-picker"]',
|
||||
|
@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { render, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { byTestId } from 'testing-library-selector';
|
||||
|
||||
import * as api from './api';
|
||||
import { FolderInfo, PermissionLevelString } from '../../../../types';
|
||||
import { ALL_FOLDER, GENERAL_FOLDER, ReadonlyFolderPicker, ReadonlyFolderPickerProps } from './ReadonlyFolderPicker';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
|
||||
const FOLDERS = [
|
||||
{ value: GENERAL_FOLDER, label: GENERAL_FOLDER.title },
|
||||
{ value: { id: 1, title: 'Test' }, label: 'Test' },
|
||||
];
|
||||
|
||||
async function getTestContext(
|
||||
propOverrides: Partial<ReadonlyFolderPickerProps> = {},
|
||||
folders: Array<SelectableValue<FolderInfo>> = [],
|
||||
folder: SelectableValue<FolderInfo> | undefined = undefined
|
||||
) {
|
||||
jest.clearAllMocks();
|
||||
const selectors = {
|
||||
container: byTestId(e2eSelectors.components.ReadonlyFolderPicker.container),
|
||||
};
|
||||
const getFoldersAsOptionsSpy = jest.spyOn(api, 'getFoldersAsOptions').mockResolvedValue(folders);
|
||||
const getFolderAsOptionSpy = jest.spyOn(api, 'getFolderAsOption').mockResolvedValue(folder);
|
||||
const props: ReadonlyFolderPickerProps = {
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
render(<ReadonlyFolderPicker {...props} />);
|
||||
await waitFor(() => expect(getFoldersAsOptionsSpy).toHaveBeenCalledTimes(1));
|
||||
|
||||
return { getFoldersAsOptionsSpy, getFolderAsOptionSpy, selectors };
|
||||
}
|
||||
|
||||
describe('ReadonlyFolderPicker', () => {
|
||||
describe('when there are no folders', () => {
|
||||
it('then the no folder should be selected and Choose should appear', async () => {
|
||||
const { selectors } = await getTestContext();
|
||||
|
||||
expect(within(selectors.container.get()).getByText('Choose')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when permissionLevel is set', () => {
|
||||
it('then permissionLevel is passed correctly to getFoldersAsOptions', async () => {
|
||||
const { getFoldersAsOptionsSpy } = await getTestContext({ permissionLevel: PermissionLevelString.Edit });
|
||||
|
||||
expect(getFoldersAsOptionsSpy).toHaveBeenCalledWith({
|
||||
query: '',
|
||||
permissionLevel: 'Edit',
|
||||
extraFolders: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when extraFolders is set', () => {
|
||||
it('then extraFolders is passed correctly to getFoldersAsOptions', async () => {
|
||||
const { getFoldersAsOptionsSpy } = await getTestContext({ extraFolders: [ALL_FOLDER] });
|
||||
|
||||
expect(getFoldersAsOptionsSpy).toHaveBeenCalledWith({
|
||||
query: '',
|
||||
permissionLevel: 'View',
|
||||
extraFolders: [{ id: undefined, title: 'All' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when entering a query in the input', () => {
|
||||
it('then query is passed correctly to getFoldersAsOptions', async () => {
|
||||
const { getFoldersAsOptionsSpy, selectors } = await getTestContext();
|
||||
|
||||
expect(within(selectors.container.get()).getByRole('textbox')).toBeInTheDocument();
|
||||
getFoldersAsOptionsSpy.mockClear();
|
||||
await userEvent.type(within(selectors.container.get()).getByRole('textbox'), 'A');
|
||||
await waitFor(() => expect(getFoldersAsOptionsSpy).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(getFoldersAsOptionsSpy).toHaveBeenCalledWith({
|
||||
query: 'A',
|
||||
permissionLevel: 'View',
|
||||
extraFolders: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are folders', () => {
|
||||
it('then the first folder in all folders should be selected', async () => {
|
||||
const { selectors } = await getTestContext({}, FOLDERS);
|
||||
|
||||
expect(within(selectors.container.get()).getByText('General')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('and initialFolderId is passed in props and it matches an existing folder', () => {
|
||||
it('then the folder with an id equal to initialFolderId should be selected', async () => {
|
||||
const { selectors } = await getTestContext({ initialFolderId: 1 }, FOLDERS);
|
||||
|
||||
expect(within(selectors.container.get()).getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and initialFolderId is passed in props and it does not match an existing folder from search api', () => {
|
||||
it('then getFolderAsOption should be called and correct folder should be selected', async () => {
|
||||
const folderById = {
|
||||
value: { id: 50000, title: 'Outside api search' },
|
||||
label: 'Outside api search',
|
||||
};
|
||||
const { selectors, getFolderAsOptionSpy } = await getTestContext(
|
||||
{ initialFolderId: 50000 },
|
||||
FOLDERS,
|
||||
folderById
|
||||
);
|
||||
|
||||
expect(within(selectors.container.get()).getByText('Outside api search')).toBeInTheDocument();
|
||||
expect(getFolderAsOptionSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getFolderAsOptionSpy).toHaveBeenCalledWith(50000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and initialFolderId is passed in props and folder does not exist', () => {
|
||||
it('then getFolderAsOption should be called and the first folder should be selected instead', async () => {
|
||||
const { selectors, getFolderAsOptionSpy } = await getTestContext(
|
||||
{ initialFolderId: 50000 },
|
||||
FOLDERS,
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(within(selectors.container.get()).getByText('General')).toBeInTheDocument();
|
||||
expect(getFolderAsOptionSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getFolderAsOptionSpy).toHaveBeenCalledWith(50000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,85 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import debouncePromise from 'debounce-promise';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { AsyncSelect } from '@grafana/ui';
|
||||
|
||||
import { FolderInfo, PermissionLevelString } from '../../../../types';
|
||||
import { findOptionWithId, getFolderAsOption, getFoldersAsOptions } from './api';
|
||||
import { PermissionLevel } from './types';
|
||||
import { GENERAL_FOLDER_ID, GENERAL_FOLDER_TITLE } from '../../../../features/search/constants';
|
||||
|
||||
export const ALL_FOLDER: FolderInfo = { id: undefined, title: 'All' };
|
||||
export const GENERAL_FOLDER: FolderInfo = { id: GENERAL_FOLDER_ID, title: GENERAL_FOLDER_TITLE };
|
||||
|
||||
export interface ReadonlyFolderPickerProps {
|
||||
onChange: (folder?: FolderInfo) => void;
|
||||
initialFolderId?: number;
|
||||
/**
|
||||
* By default the folders API doesn't include the General folder because it doesn't exist
|
||||
* Add any extra folders you need to appear in the folder picker with the extraFolders property
|
||||
*/
|
||||
extraFolders?: FolderInfo[];
|
||||
permissionLevel?: PermissionLevel;
|
||||
}
|
||||
|
||||
export function ReadonlyFolderPicker({
|
||||
onChange: propsOnChange,
|
||||
extraFolders = [],
|
||||
initialFolderId,
|
||||
permissionLevel = PermissionLevelString.View,
|
||||
}: ReadonlyFolderPickerProps): ReactElement {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [option, setOption] = useState<SelectableValue<FolderInfo> | undefined>(undefined);
|
||||
const [options, setOptions] = useState<Array<SelectableValue<FolderInfo>> | undefined>(undefined);
|
||||
const initialize = useCallback(
|
||||
async (options: Array<SelectableValue<FolderInfo>>) => {
|
||||
let option = findOptionWithId(options, initialFolderId);
|
||||
if (!option) {
|
||||
// we didn't find the option with the initialFolderId
|
||||
// might be because the folder doesn't exist any longer
|
||||
// might be because the folder is outside of the search limit of the api
|
||||
option = (await getFolderAsOption(initialFolderId)) ?? options[0]; // get folder by id or select the first item in the options and call propsOnChange
|
||||
propsOnChange(option.value);
|
||||
}
|
||||
|
||||
setInitialized(true);
|
||||
setOptions(options);
|
||||
setOption(option);
|
||||
},
|
||||
[initialFolderId, propsOnChange]
|
||||
);
|
||||
const loadOptions = useCallback(
|
||||
async (query: string) => {
|
||||
const options = await getFoldersAsOptions({ query, permissionLevel, extraFolders });
|
||||
if (!initialized) {
|
||||
await initialize(options);
|
||||
}
|
||||
return options;
|
||||
},
|
||||
[permissionLevel, extraFolders, initialized, initialize]
|
||||
);
|
||||
const debouncedLoadOptions = debouncePromise(loadOptions, 300, { leading: true });
|
||||
const onChange = useCallback(
|
||||
({ value }: SelectableValue<FolderInfo>) => {
|
||||
const option = findOptionWithId(options, value?.id);
|
||||
setOption(option);
|
||||
propsOnChange(value);
|
||||
},
|
||||
[options, propsOnChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-testid={selectors.components.ReadonlyFolderPicker.container}>
|
||||
<AsyncSelect
|
||||
menuShouldPortal
|
||||
loadingMessage="Loading folders..."
|
||||
defaultOptions
|
||||
defaultValue={option}
|
||||
value={option}
|
||||
loadOptions={debouncedLoadOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import * as api from '../../../../features/manage-dashboards/state/actions';
|
||||
import { getFolderAsOption, getFoldersAsOptions } from './api';
|
||||
import { DashboardSearchHit } from '../../../../features/search/types';
|
||||
import { PermissionLevelString } from '../../../../types';
|
||||
import { ALL_FOLDER, GENERAL_FOLDER } from './ReadonlyFolderPicker';
|
||||
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
||||
|
||||
function getTestContext(
|
||||
searchHits: DashboardSearchHit[] = [],
|
||||
folderById: { id: number; title: string } = { id: 1, title: 'Folder 1' }
|
||||
) {
|
||||
jest.clearAllMocks();
|
||||
const searchFoldersSpy = jest.spyOn(api, 'searchFolders').mockResolvedValue(searchHits);
|
||||
const getFolderByIdSpy = jest.spyOn(api, 'getFolderById').mockResolvedValue(folderById);
|
||||
|
||||
return { searchFoldersSpy, getFolderByIdSpy };
|
||||
}
|
||||
|
||||
describe('getFoldersAsOptions', () => {
|
||||
describe('when called without permissionLevel and query', () => {
|
||||
it('then the correct defaults are passed to the api', async () => {
|
||||
const { searchFoldersSpy } = getTestContext();
|
||||
|
||||
await getFoldersAsOptions({ query: '' });
|
||||
|
||||
expect(searchFoldersSpy).toHaveBeenCalledTimes(1);
|
||||
expect(searchFoldersSpy).toHaveBeenCalledWith('', 'View');
|
||||
});
|
||||
|
||||
describe('and extra folders are passed', () => {
|
||||
it('then extra folders should all appear first in the result', async () => {
|
||||
const args = { query: '', extraFolders: [ALL_FOLDER, GENERAL_FOLDER] };
|
||||
const searchHits: any[] = [{ id: 1, title: 'Folder 1' }];
|
||||
getTestContext(searchHits);
|
||||
|
||||
const result = await getFoldersAsOptions(args);
|
||||
expect(result).toEqual([
|
||||
{ value: { id: undefined, title: 'All' }, label: 'All' },
|
||||
{ value: { id: 0, title: 'General' }, label: 'General' },
|
||||
{ value: { id: 1, title: 'Folder 1' }, label: 'Folder 1' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with permissionLevel and query', () => {
|
||||
it('then the correct values are passed to the api', async () => {
|
||||
const { searchFoldersSpy } = getTestContext();
|
||||
|
||||
await getFoldersAsOptions({ query: 'Folder1', permissionLevel: PermissionLevelString.Edit });
|
||||
|
||||
expect(searchFoldersSpy).toHaveBeenCalledTimes(1);
|
||||
expect(searchFoldersSpy).toHaveBeenCalledWith('Folder1', 'Edit');
|
||||
});
|
||||
|
||||
describe('and extra folders are passed and extra folders contain query', () => {
|
||||
it('then correct extra folders should all appear first in the result', async () => {
|
||||
const args = { query: 'er', extraFolders: [ALL_FOLDER, GENERAL_FOLDER] };
|
||||
const searchHits: any[] = [{ id: 1, title: 'Folder 1' }];
|
||||
getTestContext(searchHits);
|
||||
|
||||
const result = await getFoldersAsOptions(args);
|
||||
expect(result).toEqual([
|
||||
{ value: { id: 0, title: 'General' }, label: 'General' },
|
||||
{ value: { id: 1, title: 'Folder 1' }, label: 'Folder 1' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and extra folders are passed and extra folders do not contain query', () => {
|
||||
it('then no extra folders should appear first in the result', async () => {
|
||||
const args = { query: '1', extraFolders: [ALL_FOLDER, GENERAL_FOLDER] };
|
||||
const searchHits: any[] = [{ id: 1, title: 'Folder 1' }];
|
||||
getTestContext(searchHits);
|
||||
|
||||
const result = await getFoldersAsOptions(args);
|
||||
expect(result).toEqual([{ value: { id: 1, title: 'Folder 1' }, label: 'Folder 1' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFolderAsOption', () => {
|
||||
describe('when called with undefined', () => {
|
||||
it('then it should return undefined', async () => {
|
||||
const { getFolderByIdSpy } = getTestContext();
|
||||
|
||||
const result = await getFolderAsOption(undefined);
|
||||
expect(result).toBeUndefined();
|
||||
expect(getFolderByIdSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a folder id that does not exist', () => {
|
||||
silenceConsoleOutput();
|
||||
it('then it should return undefined', async () => {
|
||||
const { getFolderByIdSpy } = getTestContext();
|
||||
getFolderByIdSpy.mockRejectedValue('Not found');
|
||||
|
||||
const result = await getFolderAsOption(-1);
|
||||
expect(result).toBeUndefined();
|
||||
expect(getFolderByIdSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a folder id that exist', () => {
|
||||
it('then it should return a SelectableValue of FolderInfo', async () => {
|
||||
const { getFolderByIdSpy } = getTestContext();
|
||||
|
||||
const result = await getFolderAsOption(1);
|
||||
expect(result).toEqual({ value: { id: 1, title: 'Folder 1' }, label: 'Folder 1' });
|
||||
expect(getFolderByIdSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { FolderInfo, PermissionLevelString } from '../../../../types';
|
||||
import { getFolderById, searchFolders } from '../../../../features/manage-dashboards/state/actions';
|
||||
import { PermissionLevel } from './types';
|
||||
|
||||
interface GetFoldersArgs {
|
||||
query: string;
|
||||
permissionLevel?: PermissionLevel;
|
||||
}
|
||||
|
||||
async function getFolders({ query, permissionLevel }: GetFoldersArgs): Promise<FolderInfo[]> {
|
||||
const searchHits = await searchFolders(query, permissionLevel);
|
||||
const folders: FolderInfo[] = searchHits.map((searchHit) => ({
|
||||
id: searchHit.id,
|
||||
title: searchHit.title,
|
||||
url: searchHit.url,
|
||||
}));
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
export interface GetFoldersWithEntriesArgs extends GetFoldersArgs {
|
||||
extraFolders?: FolderInfo[];
|
||||
}
|
||||
|
||||
async function getFoldersWithEntries({
|
||||
query,
|
||||
permissionLevel,
|
||||
extraFolders,
|
||||
}: GetFoldersWithEntriesArgs): Promise<FolderInfo[]> {
|
||||
const folders = await getFolders({ query, permissionLevel });
|
||||
const extra: FolderInfo[] = extraFolders ?? [];
|
||||
const filteredExtra = query ? extra.filter((f) => f.title?.toLowerCase().includes(query.toLowerCase())) : extra;
|
||||
if (folders) {
|
||||
return filteredExtra.concat(folders);
|
||||
}
|
||||
|
||||
return filteredExtra;
|
||||
}
|
||||
|
||||
export async function getFoldersAsOptions({
|
||||
query,
|
||||
permissionLevel = PermissionLevelString.View,
|
||||
extraFolders = [],
|
||||
}: GetFoldersWithEntriesArgs) {
|
||||
const folders = await getFoldersWithEntries({ query, permissionLevel, extraFolders });
|
||||
return folders.map((value) => {
|
||||
const option: SelectableValue<FolderInfo> = { value, label: value.title };
|
||||
return option;
|
||||
});
|
||||
}
|
||||
|
||||
export function findOptionWithId(
|
||||
options?: Array<SelectableValue<FolderInfo>>,
|
||||
id?: number
|
||||
): SelectableValue<FolderInfo> | undefined {
|
||||
return options?.find((o) => o.value?.id === id);
|
||||
}
|
||||
|
||||
export async function getFolderAsOption(folderId?: number): Promise<SelectableValue<FolderInfo> | undefined> {
|
||||
if (folderId === undefined || folderId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, title } = await getFolderById(folderId);
|
||||
return { value: { id, title }, label: title };
|
||||
} catch (err) {
|
||||
console.error(`Could not find folder with id:${folderId}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { PermissionLevelString } from '../../../../types';
|
||||
|
||||
export type PermissionLevel = Exclude<PermissionLevelString, PermissionLevelString.Admin>;
|
@ -5,3 +5,4 @@ export const SEARCH_ITEM_MARGIN = 8;
|
||||
export const DEFAULT_SORT = { label: 'A\u2013Z', value: 'alpha-asc' };
|
||||
export const SECTION_STORAGE_KEY = 'search.sections';
|
||||
export const GENERAL_FOLDER_ID = 0;
|
||||
export const GENERAL_FOLDER_TITLE = 'General';
|
||||
|
@ -3,11 +3,15 @@ import { PanelPlugin } from '@grafana/data';
|
||||
import { TagsInput } from '@grafana/ui';
|
||||
import { AlertList } from './AlertList';
|
||||
import { UnifiedAlertList } from './UnifiedAlertList';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { AlertListOptions, UnifiedAlertListOptions, ShowOption, SortOrder } from './types';
|
||||
import { AlertListOptions, ShowOption, SortOrder, UnifiedAlertListOptions } from './types';
|
||||
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { RuleFolderPicker } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker';
|
||||
import {
|
||||
ALL_FOLDER,
|
||||
GENERAL_FOLDER,
|
||||
ReadonlyFolderPicker,
|
||||
} from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker';
|
||||
|
||||
function showIfCurrentState(options: AlertListOptions) {
|
||||
return options.showOptions === ShowOption.Current;
|
||||
@ -74,13 +78,12 @@ const alertList = new PanelPlugin<AlertListOptions>(AlertList)
|
||||
name: 'Folder',
|
||||
id: 'folderId',
|
||||
defaultValue: null,
|
||||
editor: function RenderFolderPicker(props) {
|
||||
editor: function RenderFolderPicker({ value, onChange }) {
|
||||
return (
|
||||
<FolderPicker
|
||||
initialFolderId={props.value}
|
||||
initialTitle="All"
|
||||
enableReset={true}
|
||||
onChange={({ id }) => props.onChange(id)}
|
||||
<ReadonlyFolderPicker
|
||||
initialFolderId={value}
|
||||
onChange={(folder) => onChange(folder?.id)}
|
||||
extraFolders={[ALL_FOLDER, GENERAL_FOLDER]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { PanelModel, PanelPlugin } from '@grafana/data';
|
||||
import { DashList } from './DashList';
|
||||
import { DashListOptions } from './types';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import React from 'react';
|
||||
import { TagsInput } from '@grafana/ui';
|
||||
import { PermissionLevelString } from '../../../types';
|
||||
import {
|
||||
ALL_FOLDER,
|
||||
GENERAL_FOLDER,
|
||||
ReadonlyFolderPicker,
|
||||
} from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker';
|
||||
|
||||
export const plugin = new PanelPlugin<DashListOptions>(DashList)
|
||||
.setPanelOptions((builder) => {
|
||||
@ -43,15 +46,13 @@ export const plugin = new PanelPlugin<DashListOptions>(DashList)
|
||||
path: 'folderId',
|
||||
name: 'Folder',
|
||||
id: 'folderId',
|
||||
defaultValue: null,
|
||||
editor: function RenderFolderPicker(props) {
|
||||
defaultValue: undefined,
|
||||
editor: function RenderFolderPicker({ value, onChange }) {
|
||||
return (
|
||||
<FolderPicker
|
||||
initialFolderId={props.value}
|
||||
initialTitle="All"
|
||||
enableReset={true}
|
||||
permissionLevel={PermissionLevelString.View}
|
||||
onChange={({ id }) => props.onChange(id)}
|
||||
<ReadonlyFolderPicker
|
||||
initialFolderId={value}
|
||||
onChange={(folder) => onChange(folder?.id)}
|
||||
extraFolders={[ALL_FOLDER, GENERAL_FOLDER]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user