mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
tidy(ui): app layout components
This commit is contained in:
@@ -9,7 +9,7 @@ import { selectEntityCountActive } from 'features/controlLayers/store/selectors'
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import RightPanelContent from 'features/gallery/components/GalleryTopBar';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
@@ -61,7 +61,7 @@ export const CanvasRightPanel = memo(() => {
|
||||
</CanvasManagerProviderGate>
|
||||
</TabPanel>
|
||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
||||
<GalleryPanelContent />
|
||||
<RightPanelContent />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { selectEntityCountActive } from 'features/controlLayers/store/selectors'
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import RightPanelContent from 'features/gallery/components/GalleryTopBar';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
@@ -49,7 +49,7 @@ export const CanvasRightPanelStacked = memo(() => {
|
||||
return (
|
||||
<PanelGroup direction="vertical">
|
||||
<Panel>
|
||||
<GalleryPanelContent />
|
||||
<RightPanelContent />
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { selectAllowPrivateBoards } from 'features/system/store/configSelectors';
|
||||
import { useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
@@ -21,7 +21,7 @@ type Props = {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
|
||||
export const BoardsList = ({ isPrivate }: Props) => {
|
||||
export const BoardsList = memo(({ isPrivate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
@@ -118,4 +118,5 @@ export const BoardsList = ({ isPrivate }: Props) => {
|
||||
</Collapse>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
BoardsList.displayName = 'BoardsList';
|
||||
|
||||
@@ -17,7 +17,7 @@ const overlayScrollbarsStyles: CSSProperties = {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const BoardsListWrapper = () => {
|
||||
export const BoardsListWrapper = memo(() => {
|
||||
const allowPrivateBoards = useAppSelector(selectAllowPrivateBoards);
|
||||
const [os, osRef] = useState<OverlayScrollbarsComponentRef | null>(null);
|
||||
useEffect(() => {
|
||||
@@ -54,5 +54,6 @@ const BoardsListWrapper = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default memo(BoardsListWrapper);
|
||||
});
|
||||
|
||||
BoardsListWrapper.displayName = 'BoardsListWrapper';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
const BoardsSearch = () => {
|
||||
export const BoardsSearch = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const { t } = useTranslation();
|
||||
@@ -62,6 +62,5 @@ const BoardsSearch = () => {
|
||||
)}
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BoardsSearch);
|
||||
});
|
||||
BoardsSearch.displayName = 'BoardsSearch';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectBoardsListOrderBy, selectBoardsListOrderDir } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardsListOrderByChanged, boardsListOrderDirChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -15,7 +15,7 @@ const zDirection = z.enum(['ASC', 'DESC']);
|
||||
type Direction = z.infer<typeof zDirection>;
|
||||
const isDirection = (v: unknown): v is Direction => zDirection.safeParse(v).success;
|
||||
|
||||
export const BoardsListSortControls = () => {
|
||||
export const BoardsListSortControls = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const orderBy = useAppSelector(selectBoardsListOrderBy);
|
||||
@@ -83,4 +83,5 @@ export const BoardsListSortControls = () => {
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
BoardsListSortControls.displayName = 'BoardsListSortControls';
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PiGearSixFill } from 'react-icons/pi';
|
||||
|
||||
import { BoardsListSortControls } from './BoardsListSortControls';
|
||||
|
||||
const BoardsSettingsPopover = () => {
|
||||
export const BoardsSettingsPopover = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -48,6 +48,5 @@ const BoardsSettingsPopover = () => {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(BoardsSettingsPopover);
|
||||
});
|
||||
BoardsSettingsPopover.displayName = 'BoardsSettingsPopover';
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
|
||||
import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper';
|
||||
import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
|
||||
export const BoardsListPanelContent = memo(
|
||||
({ boardSearchDisclosure }: { boardSearchDisclosure: UseDisclosureReturn }) => {
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full">
|
||||
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<BoardsSearch />
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Divider pt={2} />
|
||||
<BoardsListWrapper />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
BoardsListPanelContent.displayName = 'BoardsListPanelContent';
|
||||
@@ -18,12 +18,12 @@ import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useG
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
|
||||
import { GallerySettingsPopover } from './GallerySettingsPopover/GallerySettingsPopover';
|
||||
import { GalleryUploadButton } from './GalleryUploadButton';
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
||||
@@ -46,7 +46,7 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '10
|
||||
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
|
||||
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
|
||||
|
||||
export const Gallery = () => {
|
||||
export const Gallery = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const galleryView = useAppSelector(selectGalleryView);
|
||||
@@ -116,4 +116,5 @@ export const Gallery = () => {
|
||||
<GalleryPagination />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
Gallery.displayName = 'Gallery';
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
Divider,
|
||||
Flex,
|
||||
IconButton,
|
||||
type SystemStyleObject,
|
||||
useDisclosure,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
import BoardsListWrapper from './Boards/BoardsList/BoardsListWrapper';
|
||||
import BoardsSearch from './Boards/BoardsList/BoardsSearch';
|
||||
import BoardsSettingsPopover from './Boards/BoardsSettingsPopover';
|
||||
import { Gallery } from './Gallery';
|
||||
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
|
||||
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const GalleryPanelContent = () => {
|
||||
const { t } = useTranslation();
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||
|
||||
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'boards-list-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const boardsListPanel = usePanel(boardsListPanelOptions);
|
||||
|
||||
const galleryPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'gallery-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const galleryPanel = usePanel(galleryPanelOptions);
|
||||
|
||||
const canvasLayersPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'canvas-layers-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
|
||||
|
||||
const handleClickBoardSearch = useCallback(() => {
|
||||
if (boardSearchText.length) {
|
||||
dispatch(boardSearchTextChanged(''));
|
||||
}
|
||||
boardSearchDisclosure.onToggle();
|
||||
boardsListPanel.expand();
|
||||
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||
|
||||
return (
|
||||
<FocusRegionWrapper region="gallery" sx={FOCUS_REGION_STYLES}>
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
<Flex flexGrow={1} flexBasis={0}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={boardsListPanel.toggle}
|
||||
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
>
|
||||
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<GalleryHeader />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
|
||||
<BoardsSettingsPopover />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={handleClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<PanelGroup ref={imperativePanelGroupRef} direction="vertical" autoSaveId="boards-list-panel">
|
||||
<Panel order={0} id="boards-panel" collapsible {...boardsListPanel.panelProps}>
|
||||
<Flex flexDir="column" w="full" h="full">
|
||||
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<BoardsSearch />
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Divider pt={2} />
|
||||
<BoardsListWrapper />
|
||||
</Flex>
|
||||
</Panel>
|
||||
<HorizontalResizeHandle id="boards-list-to-gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
|
||||
<Panel order={1} id="gallery-wrapper-panel" collapsible {...galleryPanel.panelProps}>
|
||||
<Gallery />
|
||||
</Panel>
|
||||
{sessionType === 'advanced' && (
|
||||
<>
|
||||
<HorizontalResizeHandle id="gallery-panel-to-layers-handle" {...galleryPanel.resizeHandleProps} />
|
||||
<Panel order={2} id="canvas-layers-panel" collapsible {...canvasLayersPanel.panelProps}>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</FocusRegionWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GalleryPanelContent);
|
||||
@@ -8,7 +8,7 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiGearSixFill } from 'react-icons/pi';
|
||||
|
||||
const GallerySettingsPopover = () => {
|
||||
export const GallerySettingsPopover = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -37,6 +37,5 @@ const GallerySettingsPopover = () => {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GallerySettingsPopover);
|
||||
});
|
||||
GallerySettingsPopover.displayName = 'GallerySettingsPopover';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
|
||||
import { Button, Flex, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { BoardsSettingsPopover } from 'features/gallery/components/Boards/BoardsSettingsPopover';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
|
||||
export const GalleryTopBar = memo(
|
||||
({
|
||||
boardsListPanel,
|
||||
boardSearchDisclosure,
|
||||
}: {
|
||||
boardsListPanel: UsePanelReturn;
|
||||
boardSearchDisclosure: UseDisclosureReturn;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
|
||||
const onClickBoardSearch = useCallback(() => {
|
||||
if (boardSearchText.length) {
|
||||
dispatch(boardSearchTextChanged(''));
|
||||
}
|
||||
boardSearchDisclosure.onToggle();
|
||||
boardsListPanel.expand();
|
||||
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
<Flex flexGrow={1} flexBasis={0}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={boardsListPanel.toggle}
|
||||
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
>
|
||||
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<GalleryHeader />
|
||||
</Flex>
|
||||
<Flex flexGrow={1} flexBasis={0} justifyContent="flex-end">
|
||||
<BoardsSettingsPopover />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={onClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
GalleryTopBar.displayName = 'GalleryTopBar';
|
||||
@@ -1,10 +1,13 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { t } from 'i18next';
|
||||
import { memo } from 'react';
|
||||
import { PiUploadBold } from 'react-icons/pi';
|
||||
|
||||
export const GalleryUploadButton = () => {
|
||||
const uploadApi = useImageUploadButton({ allowMultiple: true });
|
||||
const UPLOAD_OPTIONS: Parameters<typeof useImageUploadButton>[0] = { allowMultiple: true };
|
||||
|
||||
export const GalleryUploadButton = memo(() => {
|
||||
const uploadApi = useImageUploadButton(UPLOAD_OPTIONS);
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -19,4 +22,5 @@ export const GalleryUploadButton = () => {
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
GalleryUploadButton.displayName = 'GalleryUploadButton';
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
||||
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||
import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
|
||||
import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
|
||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
||||
import { RightPanelContent } from 'features/ui/components/RightPanelContent';
|
||||
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
||||
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
||||
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
|
||||
@@ -30,6 +29,8 @@ import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
||||
import { VerticalResizeHandle } from './tabs/ResizeHandle';
|
||||
@@ -128,26 +129,21 @@ export const AppContent = memo(() => {
|
||||
>
|
||||
{withLeftPanel && (
|
||||
<>
|
||||
<Panel order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
<LeftPanelContent />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Panel id="left-panel" order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
|
||||
<LeftPanelContent />
|
||||
</Panel>
|
||||
<VerticalResizeHandle id="left-main-handle" {...leftPanel.resizeHandleProps} />
|
||||
</>
|
||||
)}
|
||||
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
|
||||
<MainPanelContent />
|
||||
{withLeftPanel && <FloatingParametersPanelButtons togglePanel={leftPanel.toggle} />}
|
||||
{withRightPanel && <FloatingGalleryButton panelApi={rightPanel} />}
|
||||
{withLeftPanel && <FloatingLeftPanelButtons onToggle={leftPanel.toggle} />}
|
||||
{withRightPanel && <FloatingRightPanelButtons onToggle={rightPanel.toggle} />}
|
||||
</Panel>
|
||||
{withRightPanel && (
|
||||
<>
|
||||
<VerticalResizeHandle id="main-right-handle" {...rightPanel.resizeHandleProps} />
|
||||
<Panel order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
|
||||
<Panel id="right-panel" order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
|
||||
<RightPanelContent />
|
||||
</Panel>
|
||||
</>
|
||||
@@ -156,35 +152,21 @@ export const AppContent = memo(() => {
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
AppContent.displayName = 'AppContent';
|
||||
|
||||
const RightPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||
|
||||
if (tab === 'upscaling' || tab === 'workflows' || tab === 'canvas') {
|
||||
return <GalleryPanelContent />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
RightPanelContent.displayName = 'RightPanelContent';
|
||||
|
||||
const LeftPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
if (tab === 'canvas') {
|
||||
return <ParametersPanelTextToImage />;
|
||||
}
|
||||
if (tab === 'upscaling') {
|
||||
return <ParametersPanelUpscale />;
|
||||
}
|
||||
if (tab === 'workflows') {
|
||||
return <WorkflowsTabLeftPanel />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
{tab === 'canvas' && <ParametersPanelTextToImage />}
|
||||
{tab === 'upscaling' && <ParametersPanelUpscale />}
|
||||
{tab === 'workflows' && <WorkflowsTabLeftPanel />}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
LeftPanelContent.displayName = 'LeftPanelContent';
|
||||
|
||||
@@ -207,6 +189,6 @@ const MainPanelContent = memo(() => {
|
||||
return <QueueTab />;
|
||||
}
|
||||
|
||||
return null;
|
||||
assert<Equals<never, typeof tab>>(false);
|
||||
});
|
||||
MainPanelContent.displayName = 'MainPanelContent';
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesSquareBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
panelApi: UsePanelReturn;
|
||||
};
|
||||
|
||||
const FloatingGalleryButton = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex pos="absolute" transform="translate(0, -50%)" minW={8} top="50%" insetInlineEnd={2}>
|
||||
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleRightPanel')}
|
||||
onClick={props.panelApi.toggle}
|
||||
icon={<PiImagesSquareBold />}
|
||||
h={48}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FloatingGalleryButton);
|
||||
@@ -2,15 +2,14 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiCircleNotchBold,
|
||||
@@ -23,60 +22,48 @@ import {
|
||||
} from 'react-icons/pi';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
type Props = {
|
||||
togglePanel: () => void;
|
||||
};
|
||||
|
||||
const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
|
||||
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||
const session = useAppSelector(selectCanvasSession);
|
||||
|
||||
return (
|
||||
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
|
||||
{tab === 'canvas' && sessionType === 'advanced' && (
|
||||
{tab === 'canvas' && session?.type === 'advanced' && (
|
||||
<CanvasManagerProviderGate>
|
||||
<ToolChooser />
|
||||
</CanvasManagerProviderGate>
|
||||
)}
|
||||
<ButtonGroup orientation="vertical" h={48}>
|
||||
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleLeftPanel')}
|
||||
onClick={togglePanel}
|
||||
icon={<PiSlidersHorizontalBold />}
|
||||
flexGrow={1}
|
||||
/>
|
||||
</Tooltip>
|
||||
<ToggleLeftPanelButton onToggle={props.onToggle} />
|
||||
<InvokeIconButton />
|
||||
<CancelCurrentIconButton />
|
||||
{/* Show the cancel all except current button instead of cancel and clear all when it is disabled */}
|
||||
{isCancelAndClearAllEnabled && <CancelAndClearAllIconButton />}
|
||||
{!isCancelAndClearAllEnabled && <CancelAllExceptCurrentIconButton />}
|
||||
<CancelAllExceptCurrentIconButton />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(FloatingSidePanelButtons);
|
||||
FloatingLeftPanelButtons.displayName = 'FloatingLeftPanelButtons';
|
||||
|
||||
const ToggleLeftPanelButton = memo((props: { onToggle: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleLeftPanel')}
|
||||
onClick={props.onToggle}
|
||||
icon={<PiSlidersHorizontalBold />}
|
||||
flexGrow={1}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
ToggleLeftPanelButton.displayName = 'ToggleLeftPanelButton';
|
||||
|
||||
const InvokeIconButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const queue = useInvoke();
|
||||
const shift = useShiftModifier();
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
|
||||
const queueButtonIcon = useMemo(() => {
|
||||
const isProcessing = (queueStatus?.queue.in_progress ?? 0) > 0;
|
||||
if (!queue.isDisabled && isProcessing) {
|
||||
return <Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />;
|
||||
}
|
||||
if (shift) {
|
||||
return <PiLightningFill />;
|
||||
}
|
||||
return <PiSparkleFill />;
|
||||
}, [queue.isDisabled, queueStatus?.queue.in_progress, shift]);
|
||||
|
||||
return (
|
||||
<InvokeButtonTooltip prepend={shift} placement="end">
|
||||
@@ -85,7 +72,7 @@ const InvokeIconButton = memo(() => {
|
||||
onClick={shift ? queue.enqueueFront : queue.enqueueBack}
|
||||
isLoading={queue.isLoading}
|
||||
isDisabled={queue.isDisabled}
|
||||
icon={queueButtonIcon}
|
||||
icon={<InvokeIconButtonIcon />}
|
||||
colorScheme="invokeYellow"
|
||||
flexGrow={1}
|
||||
/>
|
||||
@@ -94,6 +81,30 @@ const InvokeIconButton = memo(() => {
|
||||
});
|
||||
InvokeIconButton.displayName = 'InvokeIconButton';
|
||||
|
||||
const InvokeIconButtonIcon = memo(() => {
|
||||
const shift = useShiftModifier();
|
||||
const queue = useInvoke();
|
||||
const { isProcessing } = useGetQueueStatusQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
if (!data) {
|
||||
return { isProcessing: false };
|
||||
}
|
||||
return { isProcessing: data.queue.in_progress > 0 };
|
||||
},
|
||||
});
|
||||
|
||||
if (!queue.isDisabled && isProcessing) {
|
||||
return <Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />;
|
||||
}
|
||||
|
||||
if (shift) {
|
||||
return <PiLightningFill />;
|
||||
}
|
||||
|
||||
return <PiSparkleFill />;
|
||||
});
|
||||
InvokeIconButtonIcon.displayName = 'InvokeIconButtonIcon';
|
||||
|
||||
const CancelCurrentIconButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesSquareBold } from 'react-icons/pi';
|
||||
|
||||
export const FloatingRightPanelButtons = memo((props: { onToggle: () => void }) => {
|
||||
return (
|
||||
<Flex pos="absolute" transform="translate(0, -50%)" minW={8} top="50%" insetInlineEnd={2}>
|
||||
<ToggleRightPanelButton onToggle={props.onToggle} />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons';
|
||||
|
||||
const ToggleRightPanelButton = memo((props: { onToggle: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleRightPanel')}
|
||||
onClick={props.onToggle}
|
||||
icon={<PiImagesSquareBold />}
|
||||
h={48}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
ToggleRightPanelButton.displayName = 'ToggleRightPanelButton';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import WorkflowsTabLeftPanel from 'features/nodes/components/sidePanel/WorkflowsTabLeftPanel';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
||||
|
||||
const LeftPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
{tab === 'canvas' && <ParametersPanelTextToImage />}
|
||||
{tab === 'upscaling' && <ParametersPanelUpscale />}
|
||||
{tab === 'workflows' && <WorkflowsTabLeftPanel />}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
LeftPanelContent.displayName = 'LeftPanelContent';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
||||
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
||||
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const MainPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
if (tab === 'canvas') {
|
||||
return <CanvasMainPanelContent />;
|
||||
}
|
||||
if (tab === 'upscaling') {
|
||||
return <ImageViewer />;
|
||||
}
|
||||
if (tab === 'workflows') {
|
||||
return <WorkflowsMainPanel />;
|
||||
}
|
||||
if (tab === 'models') {
|
||||
return <ModelManagerTab />;
|
||||
}
|
||||
if (tab === 'queue') {
|
||||
return <QueueTab />;
|
||||
}
|
||||
|
||||
assert<Equals<never, typeof tab>>(false);
|
||||
});
|
||||
MainPanelContent.displayName = 'MainPanelContent';
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
|
||||
import { Gallery } from 'features/gallery/components/Gallery';
|
||||
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { HorizontalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
|
||||
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||
import { usePanel } from 'features/ui/hooks/usePanel';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
const FOCUS_REGION_STYLES: SystemStyleObject = {
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
export const RightPanelContent = memo(() => {
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const session = useAppSelector(selectCanvasSession);
|
||||
|
||||
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'boards-list-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const boardsListPanel = usePanel(boardsListPanelOptions);
|
||||
|
||||
const galleryPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'gallery-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const galleryPanel = usePanel(galleryPanelOptions);
|
||||
|
||||
const canvasLayersPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'canvas-layers-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
|
||||
|
||||
return (
|
||||
<FocusRegionWrapper region="gallery" sx={FOCUS_REGION_STYLES}>
|
||||
<GalleryTopBar boardsListPanel={boardsListPanel} boardSearchDisclosure={boardSearchDisclosure} />
|
||||
<PanelGroup ref={imperativePanelGroupRef} direction="vertical" autoSaveId="boards-list-panel">
|
||||
<Panel order={0} id="boards-panel" collapsible {...boardsListPanel.panelProps}>
|
||||
<BoardsListPanelContent boardSearchDisclosure={boardSearchDisclosure} />
|
||||
</Panel>
|
||||
<HorizontalResizeHandle id="boards-list-to-gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
|
||||
<Panel order={1} id="gallery-wrapper-panel" collapsible {...galleryPanel.panelProps}>
|
||||
<Gallery />
|
||||
</Panel>
|
||||
{session?.type === 'advanced' && (
|
||||
<>
|
||||
<HorizontalResizeHandle id="gallery-panel-to-layers-handle" {...galleryPanel.resizeHandleProps} />
|
||||
<Panel order={2} id="canvas-layers-panel" collapsible {...canvasLayersPanel.panelProps}>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</FocusRegionWrapper>
|
||||
);
|
||||
});
|
||||
RightPanelContent.displayName = 'RightPanelContent';
|
||||
Reference in New Issue
Block a user