mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 12:32: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:
46
web/src/components/card-singleline-container/index.less
Normal file
46
web/src/components/card-singleline-container/index.less
Normal file
@ -0,0 +1,46 @@
|
||||
.single :nth-child(n + 2):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 1):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.single :nth-child(n + 2):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 1):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.single :nth-child(n + 3):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 2):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.single :nth-child(n + 4):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 3):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.single :nth-child(n + 5):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 4):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
.single :nth-child(n + 6):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.single :nth-child(-n + 5):not(:last-child) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
41
web/src/components/card-singleline-container/index.tsx
Normal file
41
web/src/components/card-singleline-container/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isValidElement, PropsWithChildren, ReactNode } from 'react';
|
||||
import './index.less';
|
||||
|
||||
type CardContainerProps = { className?: string } & PropsWithChildren;
|
||||
|
||||
export function CardSineLineContainer({
|
||||
children,
|
||||
className,
|
||||
}: CardContainerProps) {
|
||||
const flattenChildren = (children: ReactNode): ReactNode[] => {
|
||||
const result: ReactNode[] = [];
|
||||
|
||||
const traverse = (child: ReactNode) => {
|
||||
if (Array.isArray(child)) {
|
||||
child.forEach(traverse);
|
||||
} else if (isValidElement(child) && child.props.children) {
|
||||
result.push(child);
|
||||
} else {
|
||||
result.push(child);
|
||||
}
|
||||
};
|
||||
|
||||
traverse(children);
|
||||
return result;
|
||||
};
|
||||
const childArray = flattenChildren(children);
|
||||
const childCount = childArray.length;
|
||||
console.log(childArray, childCount);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'grid gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 single',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -29,7 +29,7 @@ export function HomeCard({
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-4 flex gap-2 items-start group h-full">
|
||||
<CardContent className="p-4 flex gap-2 items-start group h-full w-full">
|
||||
<div className="flex justify-between mb-4">
|
||||
<RAGFlowAvatar
|
||||
className="w-[32px] h-[32px]"
|
||||
|
||||
@ -7,10 +7,10 @@ type SkeletonCardProps = {
|
||||
export function SkeletonCard(props: SkeletonCardProps) {
|
||||
const { className } = props;
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
<Skeleton className="h-4 w-full bg-bg-card" />
|
||||
<Skeleton className="h-4 w-full bg-bg-card" />
|
||||
<Skeleton className="h-4 w-2/3 bg-bg-card" />
|
||||
<div className={cn('space-y-4', className)}>
|
||||
<Skeleton className="h-8 w-full bg-bg-card rounded-lg" />
|
||||
<Skeleton className="h-8 w-4/5 bg-bg-card rounded-lg" />
|
||||
<Skeleton className="h-8 w-3/5 bg-bg-card rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,6 +13,33 @@ export interface SegmentedLabeledOption {
|
||||
title?: string;
|
||||
}
|
||||
declare type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[];
|
||||
const segmentedVariants = {
|
||||
round: {
|
||||
default: 'rounded-md',
|
||||
none: 'rounded-none',
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
xl: 'rounded-xl',
|
||||
xxl: 'rounded-2xl',
|
||||
xxxl: 'rounded-3xl',
|
||||
full: 'rounded-full',
|
||||
},
|
||||
size: {
|
||||
default: 'px-1 py-1',
|
||||
sm: 'px-1 py-1',
|
||||
md: 'px-2 py-1.5',
|
||||
lg: 'px-4 px-2',
|
||||
xl: 'px-5 py-2.5',
|
||||
xxl: 'px-6 py-3',
|
||||
},
|
||||
buttonSize: {
|
||||
default: 'px-2 py-1',
|
||||
md: 'px-2 py-1',
|
||||
lg: 'px-4 px-1.5',
|
||||
xl: 'px-6 py-2',
|
||||
},
|
||||
};
|
||||
export interface SegmentedProps
|
||||
extends Omit<React.HTMLProps<HTMLDivElement>, 'onChange'> {
|
||||
options: SegmentedOptions;
|
||||
@ -24,6 +51,9 @@ export interface SegmentedProps
|
||||
direction?: 'ltr' | 'rtl';
|
||||
motionName?: string;
|
||||
activeClassName?: string;
|
||||
rounded?: keyof typeof segmentedVariants.round;
|
||||
sizeType?: keyof typeof segmentedVariants.size;
|
||||
buttonSize?: keyof typeof segmentedVariants.buttonSize;
|
||||
}
|
||||
|
||||
export function Segmented({
|
||||
@ -32,6 +62,9 @@ export function Segmented({
|
||||
onChange,
|
||||
className,
|
||||
activeClassName,
|
||||
rounded = 'default',
|
||||
sizeType = 'default',
|
||||
buttonSize = 'default',
|
||||
}: SegmentedProps) {
|
||||
const [selectedValue, setSelectedValue] = React.useState<
|
||||
SegmentedValue | undefined
|
||||
@ -45,7 +78,9 @@ export function Segmented({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center rounded-3xl p-1 gap-2 bg-bg-card px-5 py-2.5',
|
||||
'flex items-center p-1 gap-2 bg-bg-card',
|
||||
segmentedVariants.round[rounded],
|
||||
segmentedVariants.size[sizeType],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@ -57,10 +92,11 @@ export function Segmented({
|
||||
<div
|
||||
key={actualValue}
|
||||
className={cn(
|
||||
'inline-flex items-center px-6 py-2 text-base font-normal rounded-3xl cursor-pointer',
|
||||
'inline-flex items-center text-base font-normal cursor-pointer',
|
||||
segmentedVariants.round[rounded],
|
||||
segmentedVariants.buttonSize[buttonSize],
|
||||
{
|
||||
'text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2':
|
||||
selectedValue === actualValue,
|
||||
'text-text-primary bg-bg-base': selectedValue === actualValue,
|
||||
},
|
||||
activeClassName && selectedValue === actualValue
|
||||
? activeClassName
|
||||
|
||||
@ -116,9 +116,13 @@ export function Header() {
|
||||
/>
|
||||
</div>
|
||||
<Segmented
|
||||
rounded="xxxl"
|
||||
sizeType="xl"
|
||||
buttonSize="xl"
|
||||
options={options}
|
||||
value={pathname}
|
||||
onChange={handleChange}
|
||||
activeClassName="text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2"
|
||||
></Segmented>
|
||||
<div className="flex items-center gap-5 text-text-badge">
|
||||
<a
|
||||
|
||||
@ -352,7 +352,7 @@ export default {
|
||||
manual: `<p>Only <b>PDF</b> is supported.</p><p>
|
||||
We assume that the manual has a hierarchical section structure, using the lowest section titles as basic unit for chunking documents. Therefore, figures and tables in the same section will not be separated, which may result in larger chunk sizes.
|
||||
</p>`,
|
||||
naive: `<p>Supported file formats are <b>MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPT, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML</b>.</p>
|
||||
naive: `<p>Supported file formats are <b>MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPTX, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML</b>.</p>
|
||||
<p>This method chunks files using a 'naive' method: </p>
|
||||
<p>
|
||||
<li>Use vision detection model to split the texts into smaller segments.</li>
|
||||
@ -710,7 +710,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
timezone: 'Time zone',
|
||||
timezoneMessage: 'Please input your timezone!',
|
||||
timezonePlaceholder: 'select your timezone',
|
||||
email: 'Email address',
|
||||
email: 'Email',
|
||||
emailDescription: 'Once registered, E-mail cannot be changed.',
|
||||
currentPassword: 'Current password',
|
||||
currentPasswordMessage: 'Please input your password!',
|
||||
@ -865,9 +865,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
apiVersion: 'API-Version',
|
||||
apiVersionMessage: 'Please input API version',
|
||||
add: 'Add',
|
||||
updateDate: 'Update Date',
|
||||
role: 'Role',
|
||||
invite: 'Invite',
|
||||
updateDate: 'Date',
|
||||
role: 'State',
|
||||
invite: 'Invite Member',
|
||||
agree: 'Accept',
|
||||
refuse: 'Decline',
|
||||
teamMembers: 'Team Members',
|
||||
@ -1631,7 +1631,7 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
email: 'Email',
|
||||
'text&markdown': 'Text & Markup',
|
||||
word: 'Word',
|
||||
slides: 'PPT',
|
||||
slides: 'PPTX',
|
||||
audio: 'Audio',
|
||||
video: 'Video',
|
||||
},
|
||||
@ -1803,13 +1803,13 @@ Important structured information may include: names, dates, locations, events, k
|
||||
confirmRerun: 'Confirm Rerun Process',
|
||||
confirmRerunModalContent: `
|
||||
<p class="text-sm text-text-disabled font-medium mb-2">
|
||||
You are about to rerun the process starting from the <strong class="text-text-primary">{{step}}</strong> step.
|
||||
You are about to rerun the process starting from the <span class="text-text-secondary">{{step}}</span> step.
|
||||
</p>
|
||||
<p class="text-sm mb-3 text-text-secondary">This will:</p>
|
||||
<p class="text-sm mb-3 text-text-disabled">This will:</p><br />
|
||||
<ul class="list-disc list-inside space-y-1 text-sm text-text-secondary">
|
||||
<li>Overwrite existing results from the current step onwards</li>
|
||||
<li>Create a new log entry for tracking</li>
|
||||
<li>Previous steps will remain unchanged</li>
|
||||
<li>• Overwrite existing results from the current step onwards</li>
|
||||
<li>• Create a new log entry for tracking</li>
|
||||
<li>• Previous steps will remain unchanged</li>
|
||||
</ul>`,
|
||||
changeStepModalTitle: 'Step Switch Warning',
|
||||
changeStepModalContent: `
|
||||
|
||||
@ -336,7 +336,7 @@ export default {
|
||||
我们假设手册具有分层部分结构。 我们使用最低的部分标题作为对文档进行切片的枢轴。
|
||||
因此,同一部分中的图和表不会被分割,并且块大小可能会很大。
|
||||
</p>`,
|
||||
naive: `<p>支持的文件格式为<b>MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPT、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML</b>。</p>
|
||||
naive: `<p>支持的文件格式为<b>MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPTX、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML</b>。</p>
|
||||
<p>此方法将简单的方法应用于块文件:</p>
|
||||
<p>
|
||||
<li>系统将使用视觉检测模型将连续文本分割成多个片段。</li>
|
||||
@ -701,7 +701,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
timezone: '时区',
|
||||
timezoneMessage: '请选择时区',
|
||||
timezonePlaceholder: '请选择时区',
|
||||
email: '邮箱地址',
|
||||
email: '邮箱',
|
||||
emailDescription: '一旦注册,电子邮件将无法更改。',
|
||||
currentPassword: '当前密码',
|
||||
currentPasswordMessage: '请输入当前密码',
|
||||
@ -821,9 +821,9 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
apiVersion: 'API版本',
|
||||
apiVersionMessage: '请输入API版本!',
|
||||
add: '添加',
|
||||
updateDate: '更新日期',
|
||||
role: '角色',
|
||||
invite: '邀请',
|
||||
updateDate: '日期',
|
||||
role: '状态',
|
||||
invite: '邀请成员',
|
||||
agree: '同意',
|
||||
refuse: '拒绝',
|
||||
teamMembers: '团队成员',
|
||||
@ -1690,13 +1690,13 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
|
||||
confirmRerun: '确认重新运行流程',
|
||||
confirmRerunModalContent: `
|
||||
<p class="text-sm text-text-disabled font-medium mb-2">
|
||||
您即将从 <strong class="text-text-primary">{{step}}</strong> 步骤开始重新运行该过程
|
||||
您即将从 <span class="text-text-secondary">{{step}}</span> 步骤开始重新运行该过程
|
||||
</p>
|
||||
<p class="text-sm mb-3 text-text-secondary">这将:</p>
|
||||
<p class="text-sm mb-3 text-text-disabled">这将:</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm text-text-secondary">
|
||||
<li>从当前步骤开始覆盖现有结果</li>
|
||||
<li>创建新的日志条目进行跟踪</li>
|
||||
<li>之前的步骤将保持不变</li>
|
||||
<li>• 从当前步骤开始覆盖现有结果</li>
|
||||
<li>• 创建新的日志条目进行跟踪</li>
|
||||
<li>• 之前的步骤将保持不变</li>
|
||||
</ul>`,
|
||||
changeStepModalTitle: '切换步骤警告',
|
||||
changeStepModalContent: `
|
||||
|
||||
@ -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