Fix: Home and team page style adjustment, and some bug fixes #10703 (#10805)

### 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:
chanx
2025-10-27 15:15:12 +08:00
committed by GitHub
parent 33a189f620
commit 5312b75362
26 changed files with 628 additions and 276 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>
);

View File

@ -1,9 +0,0 @@
.teamWrapper {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
.teamCard {
// width: 100%;
}
}

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>
);
};