Commit 3869ea07 by light5980 Committed by GitHub

feat(app): Implement local virtual pagination for dashboard app lists (#7021)

* feat(app): Implement local virtual pagination for dashboard app lists

* fix: add tsx

* fix(app): align skill virtual grid row gap

---------

Co-authored-by: archer <545436317@qq.com>
parent e4259e54
Subproject commit f500e41b5c6dd29ab937b82267dd15749cd17201
Subproject commit 69af7442cacaf436a45cf28998edcd4a0cd189be
......@@ -43,6 +43,7 @@ import { createAppTypeMap } from '@/pageComponents/app/constants';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import ListCreateCard from '@/pageComponents/dashboard/ListCreateCard';
import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal'));
......@@ -79,6 +80,13 @@ const List = () => {
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
const [editPerAppId, setEditPerAppId] = useState<string>();
const { gridRef, renderVirtualGridItems } = useVirtualGridList({
list: myApps,
listKey: `${router.pathname}-${appType}-${parentId || ''}-${searchKey}`,
reservedSlotCount: 1,
estimatedRowHeight: 160,
estimatedRowGap: 20
});
const editPerApp = useMemo(
() =>
......@@ -152,42 +160,7 @@ const List = () => {
}
}
);
if (myApps.length === 0 && isFetchingApps) return null;
return (
<>
{myApps.length === 0 && !folderDetail ? (
searchKey ? (
<EmptyTip />
) : isPc && hasCreatePer ? (
<CreateButton appType={appType} />
) : (
<Grid
py={4}
gridTemplateColumns={
folderDetail
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
: ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
}
gridGap={5}
alignItems={'stretch'}
>
{hasCreatePer ? <ListCreateButton appType={appType} /> : <ForbiddenCreateButton />}
</Grid>
)
) : (
<Grid
py={4}
gridTemplateColumns={
folderDetail
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
: ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
}
gridGap={5}
alignItems={'stretch'}
>
{hasCreatePer ? <ListCreateButton appType={appType} /> : <ForbiddenCreateButton />}
{myApps.map((app) => {
const renderAppCard = (app: (typeof myApps)[number]) => {
const isAgent = AppTypeList.includes(app.type);
const isTool = ToolTypeList.includes(app.type);
const isFolder = AppFolderTypeList.includes(app.type);
......@@ -203,6 +176,7 @@ const List = () => {
}
>
<MyBox
data-virtual-item=""
py={4}
px={5}
cursor={'pointer'}
......@@ -370,8 +344,7 @@ const List = () => {
}
},
...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission)
.hasManagePer
!(parentApp ? parentApp.permission : app.permission).hasManagePer
? []
: [
{
......@@ -427,8 +400,7 @@ const List = () => {
onClick: () =>
openConfirmDelete({
customContent: (() => {
if (isFolder)
return t('app:confirm_delete_folder_tip');
if (isFolder) return t('app:confirm_delete_folder_tip');
if (isAgent) return t('app:confirm_del_app_tip');
if (isTool) return t('app:confirm_del_tool_tip');
return t('app:confirm_del_app_tip');
......@@ -451,8 +423,48 @@ const List = () => {
</MyBox>
</MyTooltip>
);
})}
};
if (myApps.length === 0 && isFetchingApps) return null;
return (
<>
{myApps.length === 0 && !folderDetail ? (
searchKey ? (
<EmptyTip />
) : isPc && hasCreatePer ? (
<CreateButton appType={appType} />
) : (
<Grid
py={4}
gridTemplateColumns={
folderDetail
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
: ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
}
gridGap={5}
alignItems={'stretch'}
>
{hasCreatePer ? <ListCreateButton appType={appType} /> : <ForbiddenCreateButton />}
</Grid>
)
) : (
<>
<Grid
ref={gridRef}
py={4}
gridTemplateColumns={
folderDetail
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
: ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
}
gridGap={5}
alignItems={'stretch'}
>
{hasCreatePer ? <ListCreateButton appType={appType} /> : <ForbiddenCreateButton />}
{renderVirtualGridItems(renderAppCard)}
</Grid>
</>
)}
<DeleteConfirmModal />
<ConfirmCopyModal />
......
import React, { useState, useMemo, useEffect } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Box, Grid, IconButton, HStack, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
......@@ -46,6 +46,7 @@ import type {
} from '@fastgpt/global/common/parentFolder/type';
import ListCreateCard from '@/pageComponents/dashboard/ListCreateCard';
import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
......@@ -202,6 +203,13 @@ const List = ({
const [editedSkill, setEditedSkill] = useState<EditResourceInfoFormType>();
const [moveSkillId, setMoveSkillId] = useState<string>();
const [editPerSkillId, setEditPerSkillId] = useState<string>();
const { gridRef, renderVirtualGridItems } = useVirtualGridList({
list: skills,
listKey: `${router.pathname}-${router.query.parentId || ''}-${searchKey}`,
reservedSlotCount: onClickCreate && !searchKey ? 1 : 0,
estimatedRowHeight: 160,
estimatedRowGap: 12
});
const selectedSkill = useMemo(
() =>
......@@ -289,35 +297,13 @@ const List = ({
}
);
if (skills.length === 0 && isFetchingSkills) return null;
if (skills.length === 0 && (!onClickCreate || !!searchKey)) {
return <EmptyTip text={searchKey ? undefined : t('skill:no_skills')} />;
}
return (
<>
<Grid
py={4}
gridTemplateColumns={[
'1fr',
'repeat(2,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)'
]}
gridGap={3}
alignItems={'stretch'}
>
{onClickCreate && !searchKey && <ListCreateCard onClick={onClickCreate} />}
{skills.map((skill) => {
const renderSkillCard = (skill: (typeof skills)[number]) => {
const isFolder = skill.type === AgentSkillTypeEnum.folder;
const isPersonal = skill.source === AgentSkillSourceEnum.personal;
const relatedAppsCount = skill.appCount ?? 0;
const isSkillReady =
isFolder ||
(skill.creationStatus === AgentSkillCreationStatusEnum.ready &&
!!skill.currentVersionId);
(skill.creationStatus === AgentSkillCreationStatusEnum.ready && !!skill.currentVersionId);
const isSkillCreating = skill.creationStatus === AgentSkillCreationStatusEnum.creating;
const isSkillCreateFailed = skill.creationStatus === AgentSkillCreationStatusEnum.failed;
const menuList = [
......@@ -330,18 +316,13 @@ const List = ({
type: 'grayBg' as const,
label: t('common:dataset.Edit Info'),
onClick: () => {
if (
!isFolder &&
guardSkillSandboxOperation &&
!guardSkillSandboxOperation()
) {
if (!isFolder && guardSkillSandboxOperation && !guardSkillSandboxOperation()) {
return;
}
setEditedSkill({
id: skill._id,
avatar:
skill.avatar ??
(isFolder ? 'common/folderFill' : 'core/skill/default'),
skill.avatar ?? (isFolder ? 'common/folderFill' : 'core/skill/default'),
name: skill.name,
intro: skill.description
});
......@@ -416,6 +397,7 @@ const List = ({
return (
<MyBox
key={skill._id}
data-virtual-item=""
py={4}
px={5}
cursor={'pointer'}
......@@ -439,21 +421,14 @@ const List = ({
if (isFolder) {
router.push({ query: { ...router.query, parentId: skill._id } });
} else {
if (isSkillReady && guardSkillSandboxOperation && !guardSkillSandboxOperation())
return;
if (isSkillReady && guardSkillSandboxOperation && !guardSkillSandboxOperation()) return;
router.push(`/skill/detail?skillId=${skill._id}`);
}
}}
>
{/* Top row: avatar + name */}
<Flex alignItems={'center'} gap={2}>
{isFolder ? (
<MyIcon
name={'common/folderFill'}
w={'1.5rem'}
flexShrink={0}
color={'myGray.500'}
/>
<MyIcon name={'common/folderFill'} w={'1.5rem'} flexShrink={0} color={'myGray.500'} />
) : (
<Avatar
src={skill.avatar || 'core/skill/default'}
......@@ -480,7 +455,6 @@ const List = ({
)}
</Flex>
{/* Description */}
<Box
flex={'1 0 56px'}
mt={3}
......@@ -494,7 +468,6 @@ const List = ({
</Box>
</Box>
{/* Bottom row */}
<HStack h={'24px'} fontSize={'mini'} color={'myGray.500'} w="full">
<HStack flex={'1 0 0'} spacing={3}>
<UserBox
......@@ -528,11 +501,7 @@ const List = ({
</HStack>
)}
{isPersonal && (
<Box
className="more"
display={['', 'none']}
onClick={(e) => e.stopPropagation()}
>
<Box className="more" display={['', 'none']} onClick={(e) => e.stopPropagation()}>
<MyMenu
Button={
<IconButton
......@@ -550,7 +519,31 @@ const List = ({
</HStack>
</MyBox>
);
})}
};
if (skills.length === 0 && isFetchingSkills) return null;
if (skills.length === 0 && (!onClickCreate || !!searchKey)) {
return <EmptyTip text={searchKey ? undefined : t('skill:no_skills')} />;
}
return (
<>
<Grid
ref={gridRef}
py={4}
gridTemplateColumns={[
'1fr',
'repeat(2,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)'
]}
gridGap={3}
alignItems={'stretch'}
>
{onClickCreate && !searchKey && <ListCreateCard onClick={onClickCreate} />}
{renderVirtualGridItems(renderSkillCard)}
</Grid>
<DeleteConfirmModal />
<ConfirmCopyModal />
......
......@@ -31,6 +31,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import SideTag from './SideTag';
import UserBox from '@fastgpt/web/components/common/UserBox';
import { ReadRoleVal } from '@fastgpt/global/support/permission/constant';
import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
......@@ -48,11 +49,32 @@ function List() {
onUpdateDataset,
myDatasets,
folderDetail,
searchKey,
setSearchKey
} = useContextSelector(DatasetsContext, (v) => v);
const [editPerDatasetId, setEditPerDatasetId] = useState<string>();
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const formatDatasets = useMemo(
() =>
myDatasets.map((item) => {
return {
...item,
label: DatasetTypeMap[item.type]?.label,
icon: DatasetTypeMap[item.type]?.icon
};
}),
[myDatasets]
);
const { gridRef, renderVirtualGridItems } = useVirtualGridList({
list: formatDatasets,
listKey: `${router.pathname}-${parentId || ''}-${searchKey}`,
estimatedRowHeight: 160,
estimatedRowGap: 20
});
const parentDataset = useMemo(
() => myDatasets.find((item) => item._id === parentId),
[parentId, myDatasets]
......@@ -108,43 +130,22 @@ function List() {
}
);
const DeleteTipsMap = useRef({
const DeleteTipsMap = useRef<Record<DatasetTypeEnum, string>>({
[DatasetTypeEnum.folder]: t('common:dataset.deleteFolderTips'),
[DatasetTypeEnum.dataset]: t('common:core.dataset.Delete Confirm'),
[DatasetTypeEnum.websiteDataset]: t('common:core.dataset.Delete Confirm'),
[DatasetTypeEnum.externalFile]: t('common:core.dataset.Delete Confirm')
[DatasetTypeEnum.externalFile]: t('common:core.dataset.Delete Confirm'),
[DatasetTypeEnum.apiDataset]: t('common:core.dataset.Delete Confirm'),
[DatasetTypeEnum.feishu]: t('common:core.dataset.Delete Confirm'),
[DatasetTypeEnum.yuque]: t('common:core.dataset.Delete Confirm'),
[DatasetTypeEnum.dingtalk]: t('common:core.dataset.Delete Confirm')
});
const formatDatasets = useMemo(
() =>
myDatasets.map((item) => {
return {
...item,
label: DatasetTypeMap[item.type]?.label,
icon: DatasetTypeMap[item.type]?.icon
};
}),
[myDatasets]
);
const { openConfirm, ConfirmModal } = useConfirm({
type: 'delete'
});
return (
<>
{formatDatasets.length > 0 && (
<Grid
py={4}
gridTemplateColumns={
folderDetail
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
: ['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
}
gridGap={5}
alignItems={'stretch'}
>
{formatDatasets.map((dataset, index) => {
const renderDatasetCard = (dataset: (typeof formatDatasets)[number]) => {
const vectorModelAvatar = getModelProvider(dataset.vectorModel.provider)?.avatar;
return (
......@@ -161,6 +162,7 @@ function List() {
}
>
<MyBox
data-virtual-item=""
display={'flex'}
flexDirection={'column'}
lineHeight={1.5}
......@@ -322,8 +324,7 @@ function List() {
avatar: dataset.avatar
})
},
...((parentDataset ? parentDataset : dataset)?.permission
.hasManagePer
...((parentDataset ? parentDataset : dataset)?.permission.hasManagePer
? [
{
icon: 'common/file/move',
......@@ -367,7 +368,7 @@ function List() {
{
icon: 'delete',
label: t('common:Delete'),
type: 'danger' as 'danger',
type: 'danger' as const,
onClick: () =>
openConfirm({
onConfirm: () =>
......@@ -375,8 +376,7 @@ function List() {
refetchPaths();
loadMyDatasets();
}),
customContent:
DeleteTipsMap.current[DatasetTypeEnum.dataset],
customContent: DeleteTipsMap.current[dataset.type],
inputConfirmText: dataset.name
})()
}
......@@ -393,7 +393,23 @@ function List() {
</MyBox>
</MyTooltip>
);
})}
};
return (
<>
{formatDatasets.length > 0 && (
<Grid
ref={gridRef}
py={4}
gridTemplateColumns={
folderDetail
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
: ['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
}
gridGap={5}
alignItems={'stretch'}
>
{renderVirtualGridItems(renderDatasetCard)}
</Grid>
)}
{myDatasets.length === 0 && (
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment