mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Fix: Home and team page style adjustment, and some bug fixes #10703 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@ -43,7 +43,7 @@ export function SeeAllCard() {
|
||||
const { navigateToDatasetList } = useNavigatePage();
|
||||
|
||||
return (
|
||||
<Card className="w-40 flex-none h-full" onClick={navigateToDatasetList}>
|
||||
<Card className="w-full flex-none h-full" onClick={navigateToDatasetList}>
|
||||
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
|
||||
See All <ChevronRight className="size-4" />
|
||||
</CardContent>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { HomeCard } from '@/components/home-card';
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useFetchAgentListByPage } from '@/hooks/use-agent-request';
|
||||
import { AgentDropdown } from '../agents/agent-dropdown';
|
||||
import { useRenameAgent } from '../agents/use-rename-agent';
|
||||
import { ApplicationCard } from './application-card';
|
||||
|
||||
export function Agents() {
|
||||
const { data } = useFetchAgentListByPage();
|
||||
@ -21,9 +21,9 @@ export function Agents() {
|
||||
return (
|
||||
<>
|
||||
{data.slice(0, 10).map((x) => (
|
||||
<ApplicationCard
|
||||
<HomeCard
|
||||
key={x.id}
|
||||
app={x}
|
||||
data={{ name: x.title, ...x } as any}
|
||||
onClick={navigateToAgent(x.id)}
|
||||
moreDropdown={
|
||||
<AgentDropdown
|
||||
@ -33,7 +33,7 @@ export function Agents() {
|
||||
<MoreButton></MoreButton>
|
||||
</AgentDropdown>
|
||||
}
|
||||
></ApplicationCard>
|
||||
></HomeCard>
|
||||
))}
|
||||
{agentRenameVisible && (
|
||||
<RenameDialog
|
||||
|
||||
@ -48,7 +48,7 @@ export type SeeAllAppCardProps = {
|
||||
|
||||
export function SeeAllAppCard({ click }: SeeAllAppCardProps) {
|
||||
return (
|
||||
<Card className="w-64 min-h-[76px]" onClick={click}>
|
||||
<Card className="w-full min-h-[76px]" onClick={click}>
|
||||
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
|
||||
See All <ChevronRight className="size-4" />
|
||||
</CardContent>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { CardSineLineContainer } from '@/components/card-singleline-container';
|
||||
import { IconFont } from '@/components/icon-font';
|
||||
import { Segmented, SegmentedValue } from '@/components/ui/segmented';
|
||||
import { Routes } from '@/routes';
|
||||
@ -34,7 +35,7 @@ export function Applications() {
|
||||
);
|
||||
|
||||
const handleChange = (path: SegmentedValue) => {
|
||||
setVal(path as string);
|
||||
setVal(path as Routes);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -51,16 +52,19 @@ export function Applications() {
|
||||
options={options}
|
||||
value={val}
|
||||
onChange={handleChange}
|
||||
className="bg-bg-card border border-border-button rounded-full"
|
||||
activeClassName="bg-text-primary border-none"
|
||||
buttonSize="xl"
|
||||
// className="bg-bg-card border border-border-button rounded-lg"
|
||||
// activeClassName="bg-text-primary border-none rounded-lg"
|
||||
></Segmented>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{/* <div className="flex flex-wrap gap-4"> */}
|
||||
<CardSineLineContainer>
|
||||
{val === Routes.Agents && <Agents></Agents>}
|
||||
{val === Routes.Chats && <ChatList></ChatList>}
|
||||
{val === Routes.Searches && <SearchList></SearchList>}
|
||||
{<SeeAllAppCard click={handleNavigate}></SeeAllAppCard>}
|
||||
</div>
|
||||
</CardSineLineContainer>
|
||||
{/* </div> */}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { HomeCard } from '@/components/home-card';
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
@ -5,7 +6,6 @@ import { useFetchDialogList } from '@/hooks/use-chat-request';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChatDropdown } from '../next-chats/chat-dropdown';
|
||||
import { useRenameChat } from '../next-chats/hooks/use-rename-chat';
|
||||
import { ApplicationCard } from './application-card';
|
||||
|
||||
export function ChatList() {
|
||||
const { t } = useTranslation();
|
||||
@ -24,12 +24,11 @@ export function ChatList() {
|
||||
return (
|
||||
<>
|
||||
{data.dialogs.slice(0, 10).map((x) => (
|
||||
<ApplicationCard
|
||||
<HomeCard
|
||||
key={x.id}
|
||||
app={{
|
||||
data={{
|
||||
avatar: x.icon,
|
||||
title: x.name,
|
||||
update_time: x.update_time,
|
||||
...x,
|
||||
}}
|
||||
onClick={navigateToChat(x.id)}
|
||||
moreDropdown={
|
||||
@ -37,7 +36,7 @@ export function ChatList() {
|
||||
<MoreButton></MoreButton>
|
||||
</ChatDropdown>
|
||||
}
|
||||
></ApplicationCard>
|
||||
></HomeCard>
|
||||
))}
|
||||
{chatRenameVisible && (
|
||||
<RenameDialog
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { CardSineLineContainer } from '@/components/card-singleline-container';
|
||||
import { IconFont } from '@/components/icon-font';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { CardSkeleton } from '@/components/ui/skeleton';
|
||||
@ -30,7 +31,8 @@ export function Datasets() {
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 3xl:grid-cols-7 max-h-[78vh] overflow-auto">
|
||||
// <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 3xl:grid-cols-7 max-h-[78vh] overflow-auto">
|
||||
<CardSineLineContainer>
|
||||
{kbs
|
||||
?.slice(0, 6)
|
||||
.map((dataset) => (
|
||||
@ -43,7 +45,8 @@ export function Datasets() {
|
||||
<div className="min-h-24">
|
||||
<SeeAllCard></SeeAllCard>
|
||||
</div>
|
||||
</div>
|
||||
</CardSineLineContainer>
|
||||
// </div>
|
||||
)}
|
||||
</div>
|
||||
{datasetRenameVisible && (
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { HomeCard } from '@/components/home-card';
|
||||
import { IconFont } from '@/components/icon-font';
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useFetchSearchList, useRenameSearch } from '../next-searches/hooks';
|
||||
import { SearchDropdown } from '../next-searches/search-dropdown';
|
||||
import { ApplicationCard } from './application-card';
|
||||
|
||||
export function SearchList() {
|
||||
const { data, refetch: refetchList } = useFetchSearchList();
|
||||
@ -25,13 +25,9 @@ export function SearchList() {
|
||||
return (
|
||||
<>
|
||||
{data?.data.search_apps.slice(0, 10).map((x) => (
|
||||
<ApplicationCard
|
||||
<HomeCard
|
||||
key={x.id}
|
||||
app={{
|
||||
avatar: x.avatar,
|
||||
title: x.name,
|
||||
update_time: x.update_time,
|
||||
}}
|
||||
data={x}
|
||||
onClick={navigateToSearch(x.id)}
|
||||
moreDropdown={
|
||||
<SearchDropdown
|
||||
@ -41,7 +37,7 @@ export function SearchList() {
|
||||
<MoreButton></MoreButton>
|
||||
</SearchDropdown>
|
||||
}
|
||||
></ApplicationCard>
|
||||
></HomeCard>
|
||||
))}
|
||||
{openCreateModal && (
|
||||
<RenameDialog
|
||||
|
||||
@ -26,7 +26,7 @@ export default function SearchPage({
|
||||
// const { data: userInfo } = useFetchUserInfo();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<section className="relative w-full flex transition-all justify-center items-center mt-32">
|
||||
<section className="relative w-full flex transition-all justify-center items-center mt-[15vh]">
|
||||
<div className="relative z-10 px-8 pt-8 flex text-transparent flex-col justify-center items-center w-[780px]">
|
||||
<h1
|
||||
className={cn(
|
||||
@ -36,7 +36,7 @@ export default function SearchPage({
|
||||
RAGFlow
|
||||
</h1>
|
||||
|
||||
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[230px] border">
|
||||
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[240px] border">
|
||||
{!isSearching && <Spotlight className="z-0" />}
|
||||
<div className="flex flex-col justify-center items-center w-2/3">
|
||||
{!isSearching && (
|
||||
@ -55,7 +55,7 @@ export default function SearchPage({
|
||||
<div className="relative w-full ">
|
||||
<Input
|
||||
placeholder={t('search.searchGreeting')}
|
||||
className="w-full rounded-full py-6 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700"
|
||||
className="w-full rounded-full py-7 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700"
|
||||
value={searchText}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
@ -91,7 +91,6 @@ export default function SearchingView({
|
||||
>
|
||||
RAGFlow
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
|
||||
@ -117,14 +116,14 @@ export default function SearchingView({
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
|
||||
<X
|
||||
className="text-text-secondary cursor-pointer"
|
||||
className="text-text-secondary cursor-pointer opacity-80"
|
||||
size={14}
|
||||
onClick={() => {
|
||||
setSearchtext('');
|
||||
handleClickRelatedQuestion('');
|
||||
}}
|
||||
/>
|
||||
<span className="text-text-secondary ml-4">|</span>
|
||||
<span className="text-text-secondary opacity-20 ml-4">|</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full bg-text-primary p-1 text-bg-base shadow w-12 h-8 ml-4"
|
||||
@ -170,11 +169,13 @@ export default function SearchingView({
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="w-full border-b border-border-default/80 my-6"></div>
|
||||
{answer.answer && !sendingLoading && (
|
||||
<div className="w-full border-b border-border-default/80 my-6"></div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* retrieval documents */}
|
||||
{!isSearchStrEmpty && (
|
||||
{!isSearchStrEmpty && !sendingLoading && (
|
||||
<>
|
||||
<div className=" mt-3 w-44 ">
|
||||
<RetrievalDocuments
|
||||
@ -183,7 +184,7 @@ export default function SearchingView({
|
||||
onTesting={handleTestChunk}
|
||||
></RetrievalDocuments>
|
||||
</div>
|
||||
<div className="w-full border-b border-border-default/80 my-6"></div>
|
||||
{/* <div className="w-full border-b border-border-default/80 my-6"></div> */}
|
||||
</>
|
||||
)}
|
||||
<div className="mt-3 ">
|
||||
@ -218,7 +219,7 @@ export default function SearchingView({
|
||||
</Popover>
|
||||
</div>
|
||||
<div
|
||||
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
|
||||
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit mt-3"
|
||||
onClick={() =>
|
||||
clickDocumentButton(chunk.doc_id, chunk as any)
|
||||
}
|
||||
@ -237,26 +238,30 @@ export default function SearchingView({
|
||||
)}
|
||||
{relatedQuestions?.length > 0 &&
|
||||
searchData.search_config.related_search && (
|
||||
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
|
||||
<p className="text-text-primary mb-2 text-xl">
|
||||
{t('search.relatedSearch')}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap justify-start gap-2">
|
||||
{relatedQuestions?.map((x, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="transparent"
|
||||
className="bg-bg-card text-text-secondary"
|
||||
onClick={handleClickRelatedQuestion(
|
||||
x,
|
||||
searchData.search_config.summary,
|
||||
)}
|
||||
>
|
||||
{x}
|
||||
</Button>
|
||||
))}
|
||||
<>
|
||||
<div className="w-full border-b border-border-default/80 mt-6"></div>
|
||||
|
||||
<div className="mt-6 w-full overflow-hidden opacity-100 max-h-96">
|
||||
<p className="text-text-primary mb-2 text-xl">
|
||||
{t('search.relatedSearch')}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap justify-start gap-2">
|
||||
{relatedQuestions?.map((x, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="transparent"
|
||||
className="bg-bg-card text-text-secondary"
|
||||
onClick={handleClickRelatedQuestion(
|
||||
x,
|
||||
searchData.search_config.summary,
|
||||
)}
|
||||
>
|
||||
{x}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -272,7 +277,6 @@ export default function SearchingView({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mindMapVisible && (
|
||||
<div className="flex-1 h-[88dvh] z-30 ml-32 mt-5">
|
||||
<MindMapDrawer
|
||||
|
||||
@ -14,7 +14,7 @@ export function ProfileSettingWrapperCard({
|
||||
children,
|
||||
}: ProfileSettingWrapperCardProps) {
|
||||
return (
|
||||
<Card className="w-full mb-5 border-border-button bg-transparent">
|
||||
<Card className="w-full border-border-button bg-transparent relative">
|
||||
<CardHeader className="border-b border-border-button p-5">
|
||||
{header}
|
||||
</CardHeader>
|
||||
|
||||
@ -151,7 +151,7 @@ export default function McpServer() {
|
||||
onOk={onImportOk}
|
||||
></ImportMcpDialog>
|
||||
)}
|
||||
<Spotlight className="z-0" opcity={0.7} coverage={70} />
|
||||
<Spotlight />
|
||||
</ProfileSettingWrapperCard>
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,10 +38,18 @@ const UserSetting = () => {
|
||||
</Breadcrumb>
|
||||
</PageHeader>
|
||||
<div
|
||||
className={cn(styles.settingWrapper, 'overflow-auto flex flex-1 pt-4')}
|
||||
className={cn(
|
||||
styles.settingWrapper,
|
||||
'overflow-auto flex flex-1 pt-4 pr-4 pb-4',
|
||||
)}
|
||||
>
|
||||
<SideBar></SideBar>
|
||||
<div className={cn(styles.outletWrapper, 'flex flex-1')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.outletWrapper,
|
||||
'flex flex-1 border border-border-button rounded-lg',
|
||||
)}
|
||||
>
|
||||
<Outlet></Outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// src/components/ProfilePage.tsx
|
||||
import { AvatarUpload } from '@/components/avatar-upload';
|
||||
import PasswordInput from '@/components/originui/password-input';
|
||||
import Spotlight from '@/components/spotlight';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
@ -121,7 +122,8 @@ const ProfilePage: FC = () => {
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-bg-base text-text-secondary p-5">
|
||||
<div className="h-full w-full text-text-secondary p-5 relative">
|
||||
<Spotlight />
|
||||
{/* Header */}
|
||||
<header className="flex flex-col gap-1 justify-between items-start mb-6">
|
||||
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as z from 'zod';
|
||||
|
||||
const AddingUserModal = ({
|
||||
visible,
|
||||
@ -8,42 +20,54 @@ const AddingUserModal = ({
|
||||
loading,
|
||||
onOk,
|
||||
}: IModalProps<string>) => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
type FieldType = {
|
||||
email?: string;
|
||||
};
|
||||
const formSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.min(1, { message: t('common.required') }),
|
||||
});
|
||||
|
||||
const handleOk = async () => {
|
||||
const ret = await form.validateFields();
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
return onOk?.(ret.email);
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleOk = async (data: FormData) => {
|
||||
return onOk?.(data.email);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('setting.add')}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={hideModal}
|
||||
okButtonProps={{ loading }}
|
||||
open={visible || false}
|
||||
onOpenChange={(open) => !open && hideModal?.()}
|
||||
onOk={form.handleSubmit(handleOk)}
|
||||
confirmLoading={loading}
|
||||
okText={t('common.ok')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label={t('setting.email')}
|
||||
name="email"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleOk)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('setting.email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('setting.email')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
.teamWrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
.teamCard {
|
||||
// width: 100%;
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,26 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
useFetchUserInfo,
|
||||
useListTenantUser,
|
||||
} from '@/hooks/user-setting-hooks';
|
||||
import { Button, Card, Flex, Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TeamOutlined, UserAddOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import Spotlight from '@/components/spotlight';
|
||||
import { SearchInput } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import AddingUserModal from './add-user-modal';
|
||||
import { useAddUser } from './hooks';
|
||||
import styles from './index.less';
|
||||
import TenantTable from './tenant-table';
|
||||
import UserTable from './user-table';
|
||||
|
||||
const iconStyle = { fontSize: 20, color: '#1677ff' };
|
||||
|
||||
const UserSettingTeam = () => {
|
||||
const { data: userInfo } = useFetchUserInfo();
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchUser, setSearchUser] = useState('');
|
||||
useListTenantUser();
|
||||
const {
|
||||
addingTenantModalVisible,
|
||||
@ -26,38 +30,58 @@ const UserSettingTeam = () => {
|
||||
} = useAddUser();
|
||||
|
||||
return (
|
||||
<div className={styles.teamWrapper}>
|
||||
<Card className={styles.teamCard}>
|
||||
<Flex align="center" justify={'space-between'}>
|
||||
<span>
|
||||
{userInfo.nickname} {t('setting.workspace')}
|
||||
</span>
|
||||
<Button type="primary" onClick={showAddingTenantModal}>
|
||||
<UserAddOutlined />
|
||||
{t('setting.invite')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<div className="w-full flex flex-col gap-4 p-4 relative">
|
||||
<Spotlight />
|
||||
<Card className="bg-transparent border-none px-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-0 pt-1">
|
||||
<CardTitle className="text-2xl font-medium">
|
||||
{userInfo?.nickname} {t('setting.workspace')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<UserOutlined style={iconStyle} /> {t('setting.teamMembers')}
|
||||
</Space>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<UserTable></UserTable>
|
||||
<Separator className="border-border-button bg-border-button w-[calc(100%+2rem)] -translate-x-4 -translate-y-4" />
|
||||
<Card className="bg-transparent border-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
|
||||
{/* <User className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
|
||||
<CardTitle className="text-base">
|
||||
{t('setting.teamMembers')}
|
||||
</CardTitle>
|
||||
<section className="flex gap-4 items-center">
|
||||
<SearchInput
|
||||
className="bg-bg-input border-border-default w-32"
|
||||
placeholder={t('common.search')}
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
<Button onClick={showAddingTenantModal}>
|
||||
<UserPlus className=" h-4 w-4" />
|
||||
{t('setting.invite')}
|
||||
</Button>
|
||||
</section>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<UserTable searchUser={searchUser}></UserTable>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<TeamOutlined style={iconStyle} /> {t('setting.joinedTeams')}
|
||||
</Space>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<TenantTable></TenantTable>
|
||||
|
||||
<Card className="bg-transparent border-none mt-8">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
|
||||
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
|
||||
<CardTitle className="text-base">
|
||||
{t('setting.joinedTeams')}
|
||||
</CardTitle>
|
||||
<SearchInput
|
||||
className="bg-bg-input border-border-default w-32"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={t('common.search')}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<TenantTable searchTerm={searchTerm}></TenantTable>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{addingTenantModalVisible && (
|
||||
<AddingUserModal
|
||||
visible
|
||||
|
||||
@ -1,75 +1,160 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useFetchUserInfo, useListTenant } from '@/hooks/user-setting-hooks';
|
||||
import { ITenant } from '@/interfaces/database/user-setting';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import type { TableProps } from 'antd';
|
||||
import { Button, Space, Table } from 'antd';
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, LogOut } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TenantRole } from '../constants';
|
||||
import { useHandleAgreeTenant, useHandleQuitUser } from './hooks';
|
||||
|
||||
const TenantTable = () => {
|
||||
const TenantTable = ({ searchTerm }: { searchTerm: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { data, loading } = useListTenant();
|
||||
const { handleAgree } = useHandleAgreeTenant();
|
||||
const { data: user } = useFetchUserInfo();
|
||||
const { handleQuitTenantUser } = useHandleQuitUser();
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortedData = useMemo(() => {
|
||||
if (!data || data.length === 0) return data;
|
||||
let filtered = data;
|
||||
if (searchTerm) {
|
||||
filtered = data.filter(
|
||||
(tenant) =>
|
||||
tenant.nickname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
tenant.email.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (sortOrder) {
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const dateA = new Date(a.update_date).getTime();
|
||||
const dateB = new Date(b.update_date).getTime();
|
||||
|
||||
const columns: TableProps<ITenant>['columns'] = [
|
||||
{
|
||||
title: t('common.name'),
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
},
|
||||
{
|
||||
title: t('setting.email'),
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: t('setting.updateDate'),
|
||||
dataIndex: 'update_date',
|
||||
key: 'update_date',
|
||||
render(value) {
|
||||
return formatDate(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('common.action'),
|
||||
key: 'action',
|
||||
render: (_, { role, tenant_id }) => {
|
||||
if (role === TenantRole.Invite) {
|
||||
return (
|
||||
<Space>
|
||||
<Button type="link" onClick={handleAgree(tenant_id, true)}>
|
||||
{t(`setting.agree`)}
|
||||
</Button>
|
||||
<Button type="link" onClick={handleAgree(tenant_id, false)}>
|
||||
{t(`setting.refuse`)}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
} else if (role === TenantRole.Normal && user.id !== tenant_id) {
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={handleQuitTenantUser(user.id, tenant_id)}
|
||||
>
|
||||
{t('setting.quit')}
|
||||
</Button>
|
||||
);
|
||||
if (sortOrder === 'asc') {
|
||||
return dateA - dateB;
|
||||
} else {
|
||||
return dateB - dateA;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [data, sortOrder, searchTerm]);
|
||||
|
||||
const toggleSortOrder = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
setSortOrder('desc');
|
||||
} else if (sortOrder === 'desc') {
|
||||
setSortOrder(null);
|
||||
} else {
|
||||
setSortOrder('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const renderSortIcon = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
return <ArrowUp className="ml-1 h-4 w-4" />;
|
||||
} else if (sortOrder === 'desc') {
|
||||
return <ArrowDown className="ml-1 h-4 w-4" />;
|
||||
} else {
|
||||
return <ArrowUpDown className="ml-1 h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table<ITenant>
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey={'tenant_id'}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
|
||||
<Table rootClassName="rounded-lg">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
|
||||
<TableHead
|
||||
className="h-12 px-4 cursor-pointer"
|
||||
onClick={toggleSortOrder}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{t('setting.updateDate')}
|
||||
{renderSortIcon()}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('common.action')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="h-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : sortedData && sortedData.length > 0 ? (
|
||||
sortedData.map((tenant) => (
|
||||
<TableRow key={tenant.tenant_id} className="hover:bg-bg-card">
|
||||
<TableCell className="p-4 flex gap-1 items-center">
|
||||
<RAGFlowAvatar
|
||||
isPerson
|
||||
className="size-4"
|
||||
avatar={tenant.avatar}
|
||||
name={tenant.nickname}
|
||||
/>
|
||||
{tenant.nickname}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{formatDate(tenant.update_date)}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">{tenant.email}</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{tenant.role === TenantRole.Invite ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto"
|
||||
onClick={handleAgree(tenant.tenant_id, true)}
|
||||
>
|
||||
{t(`setting.agree`)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto"
|
||||
onClick={handleAgree(tenant.tenant_id, false)}
|
||||
>
|
||||
{t(`setting.refuse`)}
|
||||
</Button>
|
||||
</div>
|
||||
) : tenant.role === TenantRole.Normal &&
|
||||
user.id !== tenant.tenant_id ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleQuitTenantUser(user.id, tenant.tenant_id)}
|
||||
>
|
||||
{/* {t('setting.quit')} */}
|
||||
<LogOut />
|
||||
</Button>
|
||||
) : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="h-24 text-center">
|
||||
{t('common.noData')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,75 +1,160 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useListTenantUser } from '@/hooks/user-setting-hooks';
|
||||
import { ITenantUser } from '@/interfaces/database/user-setting';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import { Button, Table, Tag } from 'antd';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Trash2 } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TenantRole } from '../constants';
|
||||
import { useHandleDeleteUser } from './hooks';
|
||||
|
||||
const ColorMap = {
|
||||
[TenantRole.Normal]: 'green',
|
||||
[TenantRole.Invite]: 'orange',
|
||||
[TenantRole.Owner]: 'red',
|
||||
const ColorMap: Record<string, string> = {
|
||||
[TenantRole.Normal]: 'bg-transparent text-text-primary',
|
||||
[TenantRole.Invite]: 'bg-accent-primary-5 bg-accent-primary rounded-sm',
|
||||
[TenantRole.Owner]: 'bg-red-100 text-red-800',
|
||||
};
|
||||
|
||||
const UserTable = () => {
|
||||
const UserTable = ({ searchUser }: { searchUser: string }) => {
|
||||
const { data, loading } = useListTenantUser();
|
||||
const { handleDeleteTenantUser } = useHandleDeleteUser();
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const sortedData = useMemo(() => {
|
||||
console.log('sortedData', data, searchUser);
|
||||
if (!data || data.length === 0) return data;
|
||||
let filtered = data;
|
||||
if (searchUser) {
|
||||
filtered = filtered.filter(
|
||||
(tenant) =>
|
||||
tenant.nickname.toLowerCase().includes(searchUser.toLowerCase()) ||
|
||||
tenant.email.toLowerCase().includes(searchUser.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (sortOrder) {
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const dateA = new Date(a.update_date).getTime();
|
||||
const dateB = new Date(b.update_date).getTime();
|
||||
|
||||
const columns: TableProps<ITenantUser>['columns'] = [
|
||||
{
|
||||
title: t('common.name'),
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
},
|
||||
{
|
||||
title: t('setting.email'),
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: t('setting.role'),
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render(value, { role }) {
|
||||
return (
|
||||
<Tag color={ColorMap[role as keyof typeof ColorMap]}>
|
||||
{upperFirst(role)}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('setting.updateDate'),
|
||||
dataIndex: 'update_date',
|
||||
key: 'update_date',
|
||||
render(value) {
|
||||
return formatDate(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('common.action'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Button type="text" onClick={handleDeleteTenantUser(record.user_id)}>
|
||||
<DeleteOutlined size={20} />
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (sortOrder === 'asc') {
|
||||
return dateA - dateB;
|
||||
} else {
|
||||
return dateB - dateA;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [data, sortOrder, searchUser]);
|
||||
const toggleSortOrder = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
setSortOrder('desc');
|
||||
} else if (sortOrder === 'desc') {
|
||||
setSortOrder(null);
|
||||
} else {
|
||||
setSortOrder('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const renderSortIcon = () => {
|
||||
if (sortOrder === 'asc') {
|
||||
return <ArrowUp className="ml-1 h-4 w-4 " />;
|
||||
} else if (sortOrder === 'desc') {
|
||||
return <ArrowDown className="ml-1 h-4 w-4" />;
|
||||
} else {
|
||||
return <ArrowUpDown className="ml-1 h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Table<ITenantUser>
|
||||
rowKey={'user_id'}
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
|
||||
<Table rootClassName="rounded-lg">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
|
||||
<TableHead
|
||||
className="h-12 px-4 cursor-pointer"
|
||||
onClick={toggleSortOrder}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{t('setting.updateDate')}
|
||||
{renderSortIcon()}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('setting.role')}</TableHead>
|
||||
<TableHead className="h-12 px-4">{t('common.action')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : sortedData && sortedData.length > 0 ? (
|
||||
sortedData.map((record) => (
|
||||
<TableRow key={record.user_id} className="hover:bg-bg-card">
|
||||
<TableCell className="p-4 ">
|
||||
<div className="flex gap-1 items-center">
|
||||
<RAGFlowAvatar
|
||||
isPerson
|
||||
className="size-4"
|
||||
avatar={record.avatar}
|
||||
name={record.nickname}
|
||||
/>
|
||||
{record.nickname}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{formatDate(record.update_date)}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">{record.email}</TableCell>
|
||||
<TableCell className="p-4">
|
||||
{record.role === TenantRole.Normal && (
|
||||
<Badge className={ColorMap[record.role]}>
|
||||
{upperFirst('Member')}
|
||||
</Badge>
|
||||
)}
|
||||
{record.role !== TenantRole.Normal && (
|
||||
<Badge className={ColorMap[record.role]}>
|
||||
{upperFirst(record.role)}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleDeleteTenantUser(record.user_id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
{t('common.noData')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user