diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx index adcaa15264a..1c99caefd60 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx @@ -15,8 +15,6 @@ import { DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { DashboardViewItem } from 'app/features/search/types'; import { useSelector } from 'app/types'; -import { FolderUID } from './types'; - const ROW_HEIGHT = 40; const CHEVRON_SIZE = 'md'; @@ -27,7 +25,7 @@ interface NestedFolderListProps { focusedItemIndex: number; foldersAreOpenable: boolean; idPrefix: string; - selectedFolder: FolderUID | undefined; + selectedFolder: string | undefined; onFolderExpand: (uid: string, newOpenState: boolean) => void; onFolderSelect: (item: DashboardViewItem) => void; isItemLoaded: (itemIndex: number) => boolean; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx index 90351d8ab72..fc71b260d5c 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx @@ -109,10 +109,7 @@ describe('NestedFolderPicker', () => { await screen.findByLabelText(folderA.item.title); await userEvent.click(screen.getByLabelText(folderA.item.title)); - expect(mockOnChange).toHaveBeenCalledWith({ - uid: folderA.item.uid, - title: folderA.item.title, - }); + expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title); }); it('can select a folder from the picker with the keyboard', async () => { @@ -122,10 +119,7 @@ describe('NestedFolderPicker', () => { await userEvent.click(button); await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}'); - expect(mockOnChange).toHaveBeenCalledWith({ - uid: folderA.item.uid, - title: folderA.item.title, - }); + expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title); }); it('can expand and collapse a folder to show its children', async () => { @@ -156,10 +150,7 @@ describe('NestedFolderPicker', () => { // Select the first child await userEvent.click(screen.getByLabelText(folderA_folderA.item.title)); - expect(mockOnChange).toHaveBeenCalledWith({ - uid: folderA_folderA.item.uid, - title: folderA_folderA.item.title, - }); + expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title); }); it('can expand and collapse a folder to show its children with the keyboard', async () => { @@ -185,9 +176,6 @@ describe('NestedFolderPicker', () => { // Select the first child await userEvent.keyboard('{ArrowDown}{Enter}'); - expect(mockOnChange).toHaveBeenCalledWith({ - uid: folderA_folderA.item.uid, - title: folderA_folderA.item.title, - }); + expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title); }); }); diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index c98d12ce8c5..78e11c61ed5 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -26,19 +26,24 @@ import { useDispatch, useSelector } from 'app/types/store'; import { getDOMId, NestedFolderList } from './NestedFolderList'; import Trigger from './Trigger'; import { useTreeInteractions } from './hooks'; -import { FolderChange, FolderUID } from './types'; -interface NestedFolderPickerProps { - value?: FolderUID; - // TODO: think properly (and pragmatically) about how to communicate moving to general folder, - // vs removing selection (if possible?) - onChange?: (folder: FolderChange) => void; +export interface NestedFolderPickerProps { + /* Folder UID to show as selected */ + value?: string; + + /* Whether to show the root 'Dashboards' (formally General) folder as selectable */ + showRootFolder?: boolean; + + /* Folder UIDs to exclude from the picker, to prevent invalid operations */ excludeUIDs?: string[]; + + /* Callback for when the user selects a folder */ + onChange?: (folderUID: string, folderName: string) => void; } const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const]; -export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: NestedFolderPickerProps) { +export function NestedFolderPicker({ value, showRootFolder = true, excludeUIDs, onChange }: NestedFolderPickerProps) { const styles = useStyles2(getStyles); const dispatch = useDispatch(); const selectedFolder = useGetFolderQuery(value || skipToken); @@ -101,10 +106,7 @@ export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: Nested const handleFolderSelect = useCallback( (item: DashboardViewItem) => { if (onChange) { - onChange({ - uid: item.uid, - title: item.title, - }); + onChange(item.uid, item.title); } setOverlayOpen(false); }, @@ -150,20 +152,22 @@ export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: Nested excludeUIDs ); - // Increase the level of each item to 'make way' for the fake root Dashboards item - for (const item of flatTree) { - item.level += 1; - } + if (showRootFolder) { + // 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: '', - }, - }); + 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) { @@ -171,7 +175,7 @@ export function NestedFolderPicker({ value, onChange, excludeUIDs = [] }: Nested } return flatTree; - }, [search, searchState.value, rootCollection, childrenCollections, folderOpenState, excludeUIDs]); + }, [search, searchState.value, rootCollection, childrenCollections, folderOpenState, excludeUIDs, showRootFolder]); const isItemLoaded = useCallback( (itemIndex: number) => { diff --git a/public/app/core/components/NestedFolderPicker/types.ts b/public/app/core/components/NestedFolderPicker/types.ts deleted file mode 100644 index 78e48ce7a1c..00000000000 --- a/public/app/core/components/NestedFolderPicker/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type FolderUID = string; -export type FolderChange = { title: string; uid: FolderUID }; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.tsx b/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.tsx index 153be08199a..f4c8d87c8d5 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.tsx @@ -5,7 +5,6 @@ import { config } from '@grafana/runtime'; import { Alert, Button, Field, Modal } from '@grafana/ui'; import { Text } from '@grafana/ui/src/unstable'; import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker'; -import { FolderChange } from 'app/core/components/NestedFolderPicker/types'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { t, Trans } from 'app/core/internationalization'; @@ -25,7 +24,7 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro const [isMoving, setIsMoving] = useState(false); const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]); - const handleFolderChange = (newFolder: FolderChange) => { + const handleFolderChange = (newFolder: { uid: string; title: string }) => { setMoveTarget(newFolder.uid); }; @@ -61,7 +60,7 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro {config.featureToggles.nestedFolderPicker ? ( - + ) : ( )} diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx index efef51b7b67..c6e19910918 100644 --- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx @@ -5,7 +5,6 @@ import { TimeZone } from '@grafana/data'; import { config } from '@grafana/runtime'; import { CollapsableSection, Field, Input, RadioButtonGroup, TagsInput } from '@grafana/ui'; import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker'; -import { FolderChange } from 'app/core/components/NestedFolderPicker/types'; import { Page } from 'app/core/components/Page/Page'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { updateTimeZoneDashboard, updateWeekStartDashboard } from 'app/features/dashboard/state/actions'; @@ -31,13 +30,20 @@ export function GeneralSettingsUnconnected({ }: Props): JSX.Element { const [renderCounter, setRenderCounter] = useState(0); - const onFolderChange = (newFolder: FolderChange) => { + const onFolderChange = (newFolder: { uid: string; title: string }) => { dashboard.meta.folderUid = newFolder.uid; dashboard.meta.folderTitle = newFolder.title; dashboard.meta.hasUnsavedFolderChange = true; setRenderCounter(renderCounter + 1); }; + const onNestedFolderChange = (newUID: string, newTitle: string) => { + dashboard.meta.folderUid = newUID; + dashboard.meta.folderTitle = newTitle; + dashboard.meta.hasUnsavedFolderChange = true; + setRenderCounter(renderCounter + 1); + }; + const onBlur = (event: React.FocusEvent) => { if (event.currentTarget.name === 'title' || event.currentTarget.name === 'description') { dashboard[event.currentTarget.name] = event.currentTarget.value; @@ -110,7 +116,7 @@ export function GeneralSettingsUnconnected({ {config.featureToggles.nestedFolderPicker ? ( - + ) : ( config.featureToggles.nestedFolderPicker ? ( - + field.onChange({ uid, title })} + value={field.value?.uid} + /> ) : (