mirror of
https://github.com/sqlchat/sqlchat.git
synced 2026-03-13 07:59:44 +08:00
feat: implement conversation tabs in header
This commit is contained in:
@@ -203,6 +203,7 @@ const ConnectionSidebar = () => {
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
{/* TODO(steven): remove this after we finish left sidebar */}
|
||||
<ConversationList />
|
||||
</div>
|
||||
<div className="sticky bottom-0 w-full flex flex-col justify-center bg-gray-100 dark:bg-zinc-700 backdrop-blur bg-opacity-60 pb-4 py-2">
|
||||
|
||||
131
src/components/ConversationTabsView.tsx
Normal file
131
src/components/ConversationTabsView.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConnectionStore, useConversationStore, useLayoutStore } from "@/store";
|
||||
import { Conversation } from "@/types";
|
||||
import Icon from "./Icon";
|
||||
import Dropdown, { DropdownItem } from "./kit/Dropdown";
|
||||
import UpdateConversationModal from "./UpdateConversationModal";
|
||||
|
||||
interface ConversationTabProps {
|
||||
conversation: Conversation;
|
||||
selected: boolean;
|
||||
onClick: (conversation: Conversation) => void;
|
||||
onEdit: (conversation: Conversation) => void;
|
||||
onDelete: (conversation: Conversation) => void;
|
||||
}
|
||||
|
||||
const ConversationTab = (props: ConversationTabProps) => {
|
||||
const { conversation, selected, onClick, onEdit, onDelete } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`shrink-0 flex flex-row justify-center items-center cursor-pointer text-sm border px-2 py-1 rounded-md text-gray-600 dark:text-gray-400 hover:text-gray-700 hover:bg-gray-50 dark:hover:bg-zinc-700 dark:border-zinc-700 ${
|
||||
selected && "!bg-indigo-600 !text-white !border-transparent shadow"
|
||||
}`}
|
||||
onClick={() => onClick(conversation)}
|
||||
>
|
||||
<span>{conversation.title}</span>
|
||||
<Dropdown
|
||||
tigger={
|
||||
<button className="w-4 h-auto shrink-0 ml-0.5 flex justify-center items-center text-gray-300 hover:opacity-80">
|
||||
<Icon.FiMoreVertical className="w-full h-auto" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="p-1 flex flex-col justify-start items-start bg-white dark:bg-zinc-900 shadow-lg rounded-lg">
|
||||
<DropdownItem
|
||||
className="w-full p-1 px-2 flex flex-row justify-start items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||
onClick={() => onEdit(conversation)}
|
||||
>
|
||||
<Icon.FiEdit3 className="w-4 h-auto mr-2 opacity-70" />
|
||||
{t("common.edit")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
className="w-full p-1 px-2 flex flex-row justify-start items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||
onClick={() => onDelete(conversation)}
|
||||
>
|
||||
<Icon.IoTrash className="w-4 h-auto mr-2 opacity-70" />
|
||||
{t("common.delete")}
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConversationTabsView = () => {
|
||||
const { t } = useTranslation();
|
||||
const layoutStore = useLayoutStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const conversationStore = useConversationStore();
|
||||
const [updateConversationModalContext, setUpdateConversationModalContext] = useState<Conversation | undefined>(undefined);
|
||||
const currentConnectionCtx = connectionStore.currentConnectionCtx;
|
||||
const conversationList = conversationStore.conversationList.filter(
|
||||
(conversation) =>
|
||||
conversation.connectionId === currentConnectionCtx?.connection.id &&
|
||||
conversation.databaseName === currentConnectionCtx?.database?.name
|
||||
);
|
||||
|
||||
const handleCreateConversation = () => {
|
||||
if (!currentConnectionCtx) {
|
||||
conversationStore.createConversation();
|
||||
} else {
|
||||
conversationStore.createConversation(currentConnectionCtx.connection.id, currentConnectionCtx.database?.name);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConversationClick = (conversation: Conversation) => {
|
||||
conversationStore.setCurrentConversationId(conversation.id);
|
||||
if (layoutStore.isMobileView) {
|
||||
layoutStore.toggleSidebar(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditConversation = (conversation: Conversation) => {
|
||||
setUpdateConversationModalContext(conversation);
|
||||
};
|
||||
|
||||
const handleDeleteConversation = (conversation: Conversation) => {
|
||||
conversationStore.clearConversation((item) => item.id !== conversation.id);
|
||||
if (conversationStore.currentConversationId === conversation.id) {
|
||||
conversationStore.setCurrentConversationId(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-row justify-start items-center flex-nowrap px-4 mt-1">
|
||||
<div className="w-auto flex flex-row justify-start items-center overflow-x-auto py-1 gap-x-2">
|
||||
{conversationList.map((conversation) => {
|
||||
return (
|
||||
<ConversationTab
|
||||
key={conversation.id}
|
||||
conversation={conversation}
|
||||
selected={conversation.id === conversationStore.currentConversationId}
|
||||
onClick={handleConversationClick}
|
||||
onEdit={handleEditConversation}
|
||||
onDelete={handleDeleteConversation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`shrink-0 my-2 ${conversationList.length > 0 && "ml-2"}`}>
|
||||
<button
|
||||
className="border px-2 py-1.5 text-sm rounded-md flex flex-row justify-center items-center text-gray-500 dark:text-gray-400 hover:text-gray-700 hover:bg-gray-50 dark:hover:bg-zinc-700 dark:border-zinc-700"
|
||||
onClick={handleCreateConversation}
|
||||
>
|
||||
<Icon.AiOutlinePlus className="w-4 h-auto" />
|
||||
{conversationList.length === 0 && t("conversation.new-chat")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{updateConversationModalContext && (
|
||||
<UpdateConversationModal close={() => setUpdateConversationModalContext(undefined)} conversation={updateConversationModalContext} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationTabsView;
|
||||
@@ -5,6 +5,7 @@ import { hasFeature } from "@/utils";
|
||||
import Icon from "../Icon";
|
||||
import GitHubStarBadge from "../GitHubStarBadge";
|
||||
import SchemaDrawer from "../SchemaDrawer";
|
||||
import ConversationTabsView from "../ConversationTabsView";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -25,36 +26,41 @@ const Header = (props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${
|
||||
className || ""
|
||||
} w-full flex flex-row justify-between items-center lg:grid lg:grid-cols-3 py-1 border-b dark:border-zinc-700 z-1`}
|
||||
>
|
||||
<div className="ml-2 flex justify-start items-center">
|
||||
<button
|
||||
className="w-8 h-8 p-1 mr-1 block lg:hidden rounded-md cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700"
|
||||
onClick={() => layoutStore.toggleSidebar()}
|
||||
>
|
||||
<Icon.IoIosMenu className="text-gray-600 w-full h-auto" />
|
||||
</button>
|
||||
<span className="w-auto text-left block lg:hidden">{title}</span>
|
||||
<GitHubStarBadge className="hidden lg:flex ml-2" />
|
||||
</div>
|
||||
<span className="w-auto text-center h-8 p-1 hidden lg:block">{title}</span>
|
||||
<div className="mr-2 sm:mr-3 relative flex flex-row justify-end items-center">
|
||||
<a
|
||||
href="https://www.bytebase.com?source=sqlchat"
|
||||
className="hidden sm:flex flex-row justify-center items-center h-10 px-3 py-1 rounded-md whitespace-nowrap hover:bg-gray-100 dark:hover:bg-zinc-700"
|
||||
target="_blank"
|
||||
>
|
||||
<img className="h-5 sm:h-6 w-auto" src={isDarkMode ? "/craft-by-bytebase-dark-mode.webp" : "/craft-by-bytebase.webp"} alt="" />
|
||||
</a>
|
||||
{hasFeature("debug") && (
|
||||
<button className="p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700">
|
||||
<Icon.FiSettings className="w-5 h-auto" onClick={() => setShowSchemaDrawer(true)} />
|
||||
<div className={`${className || ""} w-full border-b dark:border-zinc-700 z-1`}>
|
||||
<div className="mt-2 w-full flex flex-row justify-between items-center lg:grid lg:grid-cols-3">
|
||||
<div className="ml-2 flex justify-start items-center">
|
||||
<button
|
||||
className="w-8 h-8 p-1 mr-1 block lg:hidden rounded-md cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700"
|
||||
onClick={() => layoutStore.toggleSidebar()}
|
||||
>
|
||||
<Icon.IoIosMenu className="text-gray-600 w-full h-auto" />
|
||||
</button>
|
||||
)}
|
||||
<span className="w-auto text-left block lg:hidden">{title}</span>
|
||||
<GitHubStarBadge className="hidden lg:flex ml-2" />
|
||||
</div>
|
||||
<span className="w-auto text-center hidden lg:flex justify-center items-center">
|
||||
<a
|
||||
href="https://www.bytebase.com?source=sqlchat"
|
||||
className="hidden sm:block w-auto h-auto py-1 px-2 rounded-md hover:bg-gray-100 dark:hover:bg-zinc-700"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="h-5 sm:h-6 w-auto"
|
||||
src={isDarkMode ? "/craft-by-bytebase-dark-mode.webp" : "/craft-by-bytebase.webp"}
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
<div className="mr-2 relative flex flex-row justify-end items-center">
|
||||
{hasFeature("debug") && (
|
||||
<button className="p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700">
|
||||
<Icon.FiSettings className="w-4 h-auto" onClick={() => setShowSchemaDrawer(true)} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasFeature("debug") && <ConversationTabsView />}
|
||||
</div>
|
||||
|
||||
{hasFeature("debug") && showSchemaDrawer && <SchemaDrawer close={() => setShowSchemaDrawer(false)} />}
|
||||
|
||||
Reference in New Issue
Block a user