mirror of https://github.com/buster-so/buster.git
animation for collapse button
This commit is contained in:
parent
d7aeb14569
commit
9f4dcb6baa
|
@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
|||
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
|
||||
|
||||
const colors = [
|
||||
'red-200',
|
||||
|
@ -35,6 +36,7 @@ export const ChatContent: React.FC<{ chatContentRef: React.RefObject<HTMLDivElem
|
|||
const type = ['chat', 'dataset', 'collection', 'metric', 'dashboard'] as const;
|
||||
|
||||
const ChatContentItem = React.memo(({ index }: { index: number }) => {
|
||||
const onSetSelectedFile = useChatSplitterContextSelector((state) => state.onSetSelectedFile);
|
||||
const router = useRouter();
|
||||
const typeOfItem = type[index % type.length];
|
||||
const { isChat, isPureChat } = useMemo(
|
||||
|
@ -56,15 +58,19 @@ const ChatContentItem = React.memo(({ index }: { index: number }) => {
|
|||
}, [index, typeOfItem, isPureChat, isChat]);
|
||||
|
||||
const onClick = () => {
|
||||
router.push(link);
|
||||
if (isPureChat) {
|
||||
router.push(link);
|
||||
} else {
|
||||
onSetSelectedFile({ id: index.toString(), type: typeOfItem as AppChatMessageFileType });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={link}>
|
||||
<div className={`h-10 cursor-pointer hover:bg-gray-100 bg-${colors[index % 12]}`}>
|
||||
{typeOfItem} - {index} - {isPureChat ? 'pure chat' : isChat ? 'chat' : 'file'}
|
||||
</div>
|
||||
</Link>
|
||||
<div
|
||||
className={`h-10 cursor-pointer hover:bg-gray-100 bg-${colors[index % 12]}`}
|
||||
onClick={onClick}>
|
||||
{typeOfItem} - {index} - {isPureChat ? 'pure chat' : isChat ? 'chat' : 'file'}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { appContentHeaderHeight } from '@/components/layout/AppContentHeader';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
import { SelectedFile } from '../../interfaces';
|
||||
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
|
||||
import { ChatHeaderOptions } from './ChatHeaderOptions';
|
||||
import { ChatHeaderTitle } from './ChatHeaderTitle';
|
||||
|
@ -34,6 +33,7 @@ ChatHeader.displayName = 'ChatContainerHeader';
|
|||
const useStyles = createStyles(({ token }) => ({
|
||||
header: {
|
||||
height: appContentHeaderHeight,
|
||||
minHeight: appContentHeaderHeight,
|
||||
transition: 'box-shadow 0.2s ease-in-out'
|
||||
},
|
||||
scrollIndicator: {
|
||||
|
|
|
@ -11,23 +11,30 @@ import { useUpdateEffect, useUpdateLayoutEffect } from 'ahooks';
|
|||
|
||||
export interface ChatSplitterProps {
|
||||
showChatCollapse?: boolean;
|
||||
selectedLayout?: 'chat' | 'file' | 'both';
|
||||
selectedFile?: SelectedFile;
|
||||
defaultSelectedFile?: SelectedFile;
|
||||
defaultSelectedLayout?: 'chat' | 'file' | 'both';
|
||||
children?: React.ReactNode;
|
||||
chatId: string | undefined;
|
||||
}
|
||||
|
||||
export const ChatLayout: React.FC<ChatSplitterProps> = React.memo(
|
||||
({ selectedFile, selectedLayout = 'chat' }) => {
|
||||
({ defaultSelectedFile, defaultSelectedLayout = 'chat', children, chatId }) => {
|
||||
const appSplitterRef = useRef<AppSplitterRef>(null);
|
||||
const [isPureFile, setIsPureFile] = useState(selectedLayout === 'file');
|
||||
const [isPureFile, setIsPureFile] = useState(defaultSelectedLayout === 'file');
|
||||
|
||||
const defaultSplitterLayout = useMemo(() => {
|
||||
if (selectedLayout === 'chat') return ['100%', '0%'];
|
||||
if (selectedLayout === 'file') return ['0%', '100%'];
|
||||
if (defaultSelectedLayout === 'chat') return ['100%', '0%'];
|
||||
if (defaultSelectedLayout === 'file') return ['0%', '100%'];
|
||||
return ['325px', 'auto'];
|
||||
}, [selectedLayout]);
|
||||
}, [defaultSelectedLayout]);
|
||||
|
||||
const useChatSplitterProps = useChatLayout({ selectedFile });
|
||||
const { onSetSelectedFile, hasFile } = useChatSplitterProps;
|
||||
const useChatSplitterProps = useChatLayout({
|
||||
appSplitterRef,
|
||||
defaultSelectedFile,
|
||||
defaultSelectedLayout,
|
||||
chatId
|
||||
});
|
||||
const { onSetSelectedFile, selectedFileType, selectedLayout, hasFile } = useChatSplitterProps;
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (appSplitterRef.current) {
|
||||
|
@ -40,9 +47,7 @@ export const ChatLayout: React.FC<ChatSplitterProps> = React.memo(
|
|||
animateWidth('320px', 'left');
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFile) onSetSelectedFile(selectedFile);
|
||||
}, [selectedFile, selectedLayout]);
|
||||
}, [selectedLayout]);
|
||||
|
||||
useUpdateLayoutEffect(() => {
|
||||
if (isPureFile === true) setIsPureFile(selectedLayout === 'file');
|
||||
|
@ -53,13 +58,11 @@ export const ChatLayout: React.FC<ChatSplitterProps> = React.memo(
|
|||
<AppSplitter
|
||||
ref={appSplitterRef}
|
||||
leftChildren={isPureFile ? null : <ChatContainer />}
|
||||
rightChildren={<FileContainer />}
|
||||
rightChildren={<FileContainer children={children} />}
|
||||
autoSaveId="chat-splitter"
|
||||
defaultLayout={defaultSplitterLayout}
|
||||
preserveSide="left"
|
||||
leftPanelMaxSize={hasFile ? 625 : undefined}
|
||||
leftPanelMinSize={hasFile ? 250 : undefined}
|
||||
rightPanelMinSize={450}
|
||||
/>
|
||||
</ChatSplitterContextProvider>
|
||||
);
|
||||
|
|
|
@ -3,33 +3,80 @@ import {
|
|||
createContext,
|
||||
useContextSelector
|
||||
} from '@fluentui/react-context-selector';
|
||||
import React, { PropsWithChildren, useMemo, useState } from 'react';
|
||||
import { SelectedFile } from '../interfaces';
|
||||
import React, { PropsWithChildren, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import type { SelectedFile } from '../interfaces';
|
||||
import type { ChatSplitterProps } from '../ChatLayout';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { AppSplitterRef } from '@/components/layout';
|
||||
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
|
||||
|
||||
interface UseChatSplitterProps {
|
||||
selectedFile: SelectedFile | undefined;
|
||||
defaultSelectedFile: SelectedFile | undefined;
|
||||
defaultSelectedLayout: ChatSplitterProps['defaultSelectedLayout'];
|
||||
appSplitterRef: React.RefObject<AppSplitterRef>;
|
||||
chatId: string | undefined;
|
||||
}
|
||||
|
||||
export const useChatLayout = ({ selectedFile: selectedFileProp }: UseChatSplitterProps) => {
|
||||
const selectedFileId = selectedFileProp?.id;
|
||||
const selectedFileType = selectedFileProp?.type;
|
||||
export const useChatLayout = ({
|
||||
defaultSelectedFile,
|
||||
defaultSelectedLayout,
|
||||
appSplitterRef,
|
||||
chatId
|
||||
}: UseChatSplitterProps) => {
|
||||
const router = useRouter();
|
||||
const selectedFileId = defaultSelectedFile?.id;
|
||||
const selectedFileType = defaultSelectedFile?.type;
|
||||
const hasFile = !!selectedFileId;
|
||||
|
||||
const [selectedLayout, setSelectedLayout] = useState(defaultSelectedLayout);
|
||||
const [selectedFile, setSelectedFile] = useState(defaultSelectedFile);
|
||||
|
||||
const selectedFileTitle: string = useMemo(() => {
|
||||
if (!selectedFileId) return '';
|
||||
return 'test';
|
||||
}, [selectedFileId]);
|
||||
|
||||
const onSetSelectedFile = (file: SelectedFile) => {
|
||||
// setSelectedFileId(file.id);
|
||||
};
|
||||
const onSetSelectedFile = useMemoizedFn((file: SelectedFile) => {
|
||||
const isChatView = defaultSelectedLayout === 'chat' || defaultSelectedLayout === 'both';
|
||||
const fileType = file.type;
|
||||
const fileId = file.id;
|
||||
if (isChatView && chatId) {
|
||||
const routeRecord: Record<AppChatMessageFileType, string> = {
|
||||
collection: `/test/splitter/chat/${chatId}/collection/${file.id}`,
|
||||
dataset: `/test/splitter/chat/${chatId}/dataset/${file.id}`,
|
||||
metric: `/test/splitter/chat/${chatId}/metric/${file.id}`,
|
||||
dashboard: `/test/splitter/chat/${chatId}/dashboard/${file.id}`
|
||||
};
|
||||
if (routeRecord[fileType]) router.push(routeRecord[fileType]);
|
||||
} else {
|
||||
router.push(`/test/splitter/${fileType}/${fileId}`);
|
||||
}
|
||||
});
|
||||
|
||||
const onCollapseFileClick = useMemoizedFn((close?: boolean) => {
|
||||
const isCloseAction = close ?? selectedLayout === 'both';
|
||||
if (isCloseAction) {
|
||||
setSelectedLayout('chat');
|
||||
} else {
|
||||
setSelectedLayout('both');
|
||||
appSplitterRef.current?.animateWidth('320px', 'left');
|
||||
}
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (defaultSelectedLayout) setSelectedLayout(defaultSelectedLayout);
|
||||
}, [defaultSelectedLayout]);
|
||||
|
||||
return {
|
||||
selectedFileTitle,
|
||||
selectedFileType,
|
||||
selectedLayout,
|
||||
selectedFileId,
|
||||
hasFile,
|
||||
onSetSelectedFile
|
||||
|
||||
onSetSelectedFile,
|
||||
onCollapseFileClick
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,34 +1,15 @@
|
|||
import React from 'react';
|
||||
import { useChatSplitterContextSelector } from '../ChatLayoutContext';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { FileContainerHeader } from './FileContainerHeader';
|
||||
|
||||
interface FileContainerProps {}
|
||||
|
||||
const animation = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.15 }
|
||||
};
|
||||
|
||||
export const FileContainer: React.FC<FileContainerProps> = React.memo(({}) => {
|
||||
const selectedFileId = useChatSplitterContextSelector((state) => state.selectedFileId);
|
||||
const selectedFileType = useChatSplitterContextSelector((state) => state.selectedFileType);
|
||||
|
||||
const hasFile = !!selectedFileId;
|
||||
interface FileContainerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FileContainer: React.FC<FileContainerProps> = React.memo(({ children }) => {
|
||||
return (
|
||||
<div className="h-full w-full bg-blue-300">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{hasFile ? (
|
||||
<motion.div key={'file-container'} {...animation} className={`h-full w-full`}>
|
||||
<div
|
||||
className={`h-[300px] w-[200px] ${selectedFileType === 'metric' ? 'bg-green-500' : 'bg-red-500'}`}>
|
||||
{selectedFileType}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<div className="flex min-w-[450px] flex-col">
|
||||
<FileContainerHeader />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { AppMaterialIcons } from '@/components';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
const animation = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 }
|
||||
};
|
||||
|
||||
export const CollapseFileButton: React.FC<{
|
||||
showCollapseButton: boolean;
|
||||
isOpen: boolean;
|
||||
}> = React.memo(({ showCollapseButton, isOpen }) => {
|
||||
const onCollapseFileClick = useChatSplitterContextSelector((state) => state.onCollapseFileClick);
|
||||
const icon = !isOpen ? 'keyboard_double_arrow_left' : 'keyboard_double_arrow_right';
|
||||
|
||||
const onClick = useMemoizedFn(() => {
|
||||
onCollapseFileClick();
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{showCollapseButton && (
|
||||
<motion.div variants={animation}>
|
||||
<Button onClick={onClick} type="text" icon={<AppMaterialIcons icon={icon} />}></Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
});
|
||||
|
||||
CollapseFileButton.displayName = 'CollapseFileButton';
|
|
@ -0,0 +1,30 @@
|
|||
import { appContentHeaderHeight } from '@/components/layout';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
import { useChatSplitterContextSelector } from '../../ChatLayoutContext';
|
||||
import { CollapseFileButton } from './CollapseFileButton';
|
||||
|
||||
export const FileContainerHeader: React.FC = React.memo(() => {
|
||||
const { styles, cx } = useStyles();
|
||||
const selectedFileType = useChatSplitterContextSelector((state) => state.selectedFileType);
|
||||
const selectedLayout = useChatSplitterContextSelector((state) => state.selectedLayout);
|
||||
|
||||
const showCollapseButton = true;
|
||||
const isCollapseOpen = selectedLayout === 'both';
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, 'flex w-full items-center px-3')}>
|
||||
<CollapseFileButton showCollapseButton={showCollapseButton} isOpen={isCollapseOpen} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
FileContainerHeader.displayName = 'FileContainerHeader';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
container: css`
|
||||
min-height: ${appContentHeaderHeight}px;
|
||||
height: ${appContentHeaderHeight}px;
|
||||
border-bottom: 0.5px solid ${token.colorBorder};
|
||||
`
|
||||
}));
|
|
@ -0,0 +1 @@
|
|||
export * from './FileContainerHeader';
|
|
@ -22,7 +22,7 @@ export const useSelectedFileByParams = () => {
|
|||
if (dashboardId) return { id: dashboardId, type: AppChatMessageFileType.Dashboard };
|
||||
}, [metricId, collectionId, datasetId, dashboardId, chatId]);
|
||||
|
||||
const selectedLayout: ChatSplitterProps['selectedLayout'] = useMemo(() => {
|
||||
const selectedLayout: ChatSplitterProps['defaultSelectedLayout'] = useMemo(() => {
|
||||
const hasFileId = metricId || collectionId || datasetId || dashboardId;
|
||||
|
||||
if (chatId) {
|
||||
|
@ -35,5 +35,5 @@ export const useSelectedFileByParams = () => {
|
|||
return 'chat';
|
||||
}, [metricId, collectionId, datasetId, dashboardId, chatId]);
|
||||
|
||||
return { selectedFile, selectedLayout };
|
||||
return { selectedFile, selectedLayout, chatId };
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||
import '../styles/styles.scss';
|
||||
|
||||
import { BusterStyleProvider } from '@/context/BusterStyles/BusterStyles';
|
||||
import { GlobalErrorComponent } from '../components/error/GlobalErrorComponent';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Buster',
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export default function Page() {
|
||||
return <></>;
|
||||
return <div className="h-full w-full bg-orange-500">Dashboard swag</div>;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export default function Page() {
|
||||
return <></>;
|
||||
return <div className="h-full w-full bg-red-500">Dashboard swag</div>;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { ChatLayout, useSelectedFileByParams } from '@chatLayout/index';
|
||||
import { AppChatMessageFileType } from '@/components/messages/AppChatMessageContainer';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
export default function Layout({}: {}) {
|
||||
const { selectedFile, selectedLayout } = useSelectedFileByParams();
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { selectedFile, selectedLayout, chatId } = useSelectedFileByParams();
|
||||
const router = useRouter();
|
||||
|
||||
useHotkeys('m', () => {
|
||||
|
@ -31,7 +32,12 @@ export default function Layout({}: {}) {
|
|||
|
||||
return (
|
||||
<div className="h-screen w-screen">
|
||||
<ChatLayout selectedLayout={selectedLayout} selectedFile={selectedFile} />
|
||||
<ChatLayout
|
||||
chatId={chatId}
|
||||
defaultSelectedLayout={selectedLayout}
|
||||
defaultSelectedFile={selectedFile}>
|
||||
{children}
|
||||
</ChatLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue