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'; ...@@ -43,6 +43,7 @@ import { createAppTypeMap } from '@/pageComponents/app/constants';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import ListCreateCard from '@/pageComponents/dashboard/ListCreateCard'; import ListCreateCard from '@/pageComponents/dashboard/ListCreateCard';
import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal')); const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal'));
...@@ -79,6 +80,13 @@ const List = () => { ...@@ -79,6 +80,13 @@ const List = () => {
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>(); const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
const [editPerAppId, setEditPerAppId] = useState<string>(); 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( const editPerApp = useMemo(
() => () =>
...@@ -152,42 +160,7 @@ const List = () => { ...@@ -152,42 +160,7 @@ const List = () => {
} }
} }
); );
if (myApps.length === 0 && isFetchingApps) return null; const renderAppCard = (app: (typeof myApps)[number]) => {
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 isAgent = AppTypeList.includes(app.type); const isAgent = AppTypeList.includes(app.type);
const isTool = ToolTypeList.includes(app.type); const isTool = ToolTypeList.includes(app.type);
const isFolder = AppFolderTypeList.includes(app.type); const isFolder = AppFolderTypeList.includes(app.type);
...@@ -203,6 +176,7 @@ const List = () => { ...@@ -203,6 +176,7 @@ const List = () => {
} }
> >
<MyBox <MyBox
data-virtual-item=""
py={4} py={4}
px={5} px={5}
cursor={'pointer'} cursor={'pointer'}
...@@ -370,8 +344,7 @@ const List = () => { ...@@ -370,8 +344,7 @@ const List = () => {
} }
}, },
...(folderDetail?.type === AppTypeEnum.httpPlugin && ...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission) !(parentApp ? parentApp.permission : app.permission).hasManagePer
.hasManagePer
? [] ? []
: [ : [
{ {
...@@ -427,8 +400,7 @@ const List = () => { ...@@ -427,8 +400,7 @@ const List = () => {
onClick: () => onClick: () =>
openConfirmDelete({ openConfirmDelete({
customContent: (() => { customContent: (() => {
if (isFolder) if (isFolder) return t('app:confirm_delete_folder_tip');
return t('app:confirm_delete_folder_tip');
if (isAgent) return t('app:confirm_del_app_tip'); if (isAgent) return t('app:confirm_del_app_tip');
if (isTool) return t('app:confirm_del_tool_tip'); if (isTool) return t('app:confirm_del_tool_tip');
return t('app:confirm_del_app_tip'); return t('app:confirm_del_app_tip');
...@@ -451,8 +423,48 @@ const List = () => { ...@@ -451,8 +423,48 @@ const List = () => {
</MyBox> </MyBox>
</MyTooltip> </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> </Grid>
</>
)} )}
<DeleteConfirmModal /> <DeleteConfirmModal />
<ConfirmCopyModal /> <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 { Box, Grid, IconButton, HStack, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
...@@ -46,6 +46,7 @@ import type { ...@@ -46,6 +46,7 @@ import type {
} from '@fastgpt/global/common/parentFolder/type'; } from '@fastgpt/global/common/parentFolder/type';
import ListCreateCard from '@/pageComponents/dashboard/ListCreateCard'; import ListCreateCard from '@/pageComponents/dashboard/ListCreateCard';
import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal')); const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
...@@ -202,6 +203,13 @@ const List = ({ ...@@ -202,6 +203,13 @@ const List = ({
const [editedSkill, setEditedSkill] = useState<EditResourceInfoFormType>(); const [editedSkill, setEditedSkill] = useState<EditResourceInfoFormType>();
const [moveSkillId, setMoveSkillId] = useState<string>(); const [moveSkillId, setMoveSkillId] = useState<string>();
const [editPerSkillId, setEditPerSkillId] = 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( const selectedSkill = useMemo(
() => () =>
...@@ -289,35 +297,13 @@ const List = ({ ...@@ -289,35 +297,13 @@ const List = ({
} }
); );
if (skills.length === 0 && isFetchingSkills) return null; const renderSkillCard = (skill: (typeof skills)[number]) => {
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 isFolder = skill.type === AgentSkillTypeEnum.folder; const isFolder = skill.type === AgentSkillTypeEnum.folder;
const isPersonal = skill.source === AgentSkillSourceEnum.personal; const isPersonal = skill.source === AgentSkillSourceEnum.personal;
const relatedAppsCount = skill.appCount ?? 0; const relatedAppsCount = skill.appCount ?? 0;
const isSkillReady = const isSkillReady =
isFolder || isFolder ||
(skill.creationStatus === AgentSkillCreationStatusEnum.ready && (skill.creationStatus === AgentSkillCreationStatusEnum.ready && !!skill.currentVersionId);
!!skill.currentVersionId);
const isSkillCreating = skill.creationStatus === AgentSkillCreationStatusEnum.creating; const isSkillCreating = skill.creationStatus === AgentSkillCreationStatusEnum.creating;
const isSkillCreateFailed = skill.creationStatus === AgentSkillCreationStatusEnum.failed; const isSkillCreateFailed = skill.creationStatus === AgentSkillCreationStatusEnum.failed;
const menuList = [ const menuList = [
...@@ -330,18 +316,13 @@ const List = ({ ...@@ -330,18 +316,13 @@ const List = ({
type: 'grayBg' as const, type: 'grayBg' as const,
label: t('common:dataset.Edit Info'), label: t('common:dataset.Edit Info'),
onClick: () => { onClick: () => {
if ( if (!isFolder && guardSkillSandboxOperation && !guardSkillSandboxOperation()) {
!isFolder &&
guardSkillSandboxOperation &&
!guardSkillSandboxOperation()
) {
return; return;
} }
setEditedSkill({ setEditedSkill({
id: skill._id, id: skill._id,
avatar: avatar:
skill.avatar ?? skill.avatar ?? (isFolder ? 'common/folderFill' : 'core/skill/default'),
(isFolder ? 'common/folderFill' : 'core/skill/default'),
name: skill.name, name: skill.name,
intro: skill.description intro: skill.description
}); });
...@@ -416,6 +397,7 @@ const List = ({ ...@@ -416,6 +397,7 @@ const List = ({
return ( return (
<MyBox <MyBox
key={skill._id} key={skill._id}
data-virtual-item=""
py={4} py={4}
px={5} px={5}
cursor={'pointer'} cursor={'pointer'}
...@@ -439,21 +421,14 @@ const List = ({ ...@@ -439,21 +421,14 @@ const List = ({
if (isFolder) { if (isFolder) {
router.push({ query: { ...router.query, parentId: skill._id } }); router.push({ query: { ...router.query, parentId: skill._id } });
} else { } else {
if (isSkillReady && guardSkillSandboxOperation && !guardSkillSandboxOperation()) if (isSkillReady && guardSkillSandboxOperation && !guardSkillSandboxOperation()) return;
return;
router.push(`/skill/detail?skillId=${skill._id}`); router.push(`/skill/detail?skillId=${skill._id}`);
} }
}} }}
> >
{/* Top row: avatar + name */}
<Flex alignItems={'center'} gap={2}> <Flex alignItems={'center'} gap={2}>
{isFolder ? ( {isFolder ? (
<MyIcon <MyIcon name={'common/folderFill'} w={'1.5rem'} flexShrink={0} color={'myGray.500'} />
name={'common/folderFill'}
w={'1.5rem'}
flexShrink={0}
color={'myGray.500'}
/>
) : ( ) : (
<Avatar <Avatar
src={skill.avatar || 'core/skill/default'} src={skill.avatar || 'core/skill/default'}
...@@ -480,7 +455,6 @@ const List = ({ ...@@ -480,7 +455,6 @@ const List = ({
)} )}
</Flex> </Flex>
{/* Description */}
<Box <Box
flex={'1 0 56px'} flex={'1 0 56px'}
mt={3} mt={3}
...@@ -494,7 +468,6 @@ const List = ({ ...@@ -494,7 +468,6 @@ const List = ({
</Box> </Box>
</Box> </Box>
{/* Bottom row */}
<HStack h={'24px'} fontSize={'mini'} color={'myGray.500'} w="full"> <HStack h={'24px'} fontSize={'mini'} color={'myGray.500'} w="full">
<HStack flex={'1 0 0'} spacing={3}> <HStack flex={'1 0 0'} spacing={3}>
<UserBox <UserBox
...@@ -528,11 +501,7 @@ const List = ({ ...@@ -528,11 +501,7 @@ const List = ({
</HStack> </HStack>
)} )}
{isPersonal && ( {isPersonal && (
<Box <Box className="more" display={['', 'none']} onClick={(e) => e.stopPropagation()}>
className="more"
display={['', 'none']}
onClick={(e) => e.stopPropagation()}
>
<MyMenu <MyMenu
Button={ Button={
<IconButton <IconButton
...@@ -550,7 +519,31 @@ const List = ({ ...@@ -550,7 +519,31 @@ const List = ({
</HStack> </HStack>
</MyBox> </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> </Grid>
<DeleteConfirmModal /> <DeleteConfirmModal />
<ConfirmCopyModal /> <ConfirmCopyModal />
......
...@@ -31,6 +31,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem'; ...@@ -31,6 +31,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import SideTag from './SideTag'; import SideTag from './SideTag';
import UserBox from '@fastgpt/web/components/common/UserBox'; import UserBox from '@fastgpt/web/components/common/UserBox';
import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { ReadRoleVal } from '@fastgpt/global/support/permission/constant';
import { useVirtualGridList } from '@fastgpt/web/hooks/useVirtualGridList';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
...@@ -48,11 +49,32 @@ function List() { ...@@ -48,11 +49,32 @@ function List() {
onUpdateDataset, onUpdateDataset,
myDatasets, myDatasets,
folderDetail, folderDetail,
searchKey,
setSearchKey setSearchKey
} = useContextSelector(DatasetsContext, (v) => v); } = useContextSelector(DatasetsContext, (v) => v);
const [editPerDatasetId, setEditPerDatasetId] = useState<string>(); const [editPerDatasetId, setEditPerDatasetId] = useState<string>();
const router = useRouter(); const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null }; 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( const parentDataset = useMemo(
() => myDatasets.find((item) => item._id === parentId), () => myDatasets.find((item) => item._id === parentId),
[parentId, myDatasets] [parentId, myDatasets]
...@@ -108,43 +130,22 @@ function List() { ...@@ -108,43 +130,22 @@ function List() {
} }
); );
const DeleteTipsMap = useRef({ const DeleteTipsMap = useRef<Record<DatasetTypeEnum, string>>({
[DatasetTypeEnum.folder]: t('common:dataset.deleteFolderTips'), [DatasetTypeEnum.folder]: t('common:dataset.deleteFolderTips'),
[DatasetTypeEnum.dataset]: t('common:core.dataset.Delete Confirm'), [DatasetTypeEnum.dataset]: t('common:core.dataset.Delete Confirm'),
[DatasetTypeEnum.websiteDataset]: 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({ const { openConfirm, ConfirmModal } = useConfirm({
type: 'delete' type: 'delete'
}); });
return ( const renderDatasetCard = (dataset: (typeof formatDatasets)[number]) => {
<>
{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 vectorModelAvatar = getModelProvider(dataset.vectorModel.provider)?.avatar; const vectorModelAvatar = getModelProvider(dataset.vectorModel.provider)?.avatar;
return ( return (
...@@ -161,6 +162,7 @@ function List() { ...@@ -161,6 +162,7 @@ function List() {
} }
> >
<MyBox <MyBox
data-virtual-item=""
display={'flex'} display={'flex'}
flexDirection={'column'} flexDirection={'column'}
lineHeight={1.5} lineHeight={1.5}
...@@ -322,8 +324,7 @@ function List() { ...@@ -322,8 +324,7 @@ function List() {
avatar: dataset.avatar avatar: dataset.avatar
}) })
}, },
...((parentDataset ? parentDataset : dataset)?.permission ...((parentDataset ? parentDataset : dataset)?.permission.hasManagePer
.hasManagePer
? [ ? [
{ {
icon: 'common/file/move', icon: 'common/file/move',
...@@ -367,7 +368,7 @@ function List() { ...@@ -367,7 +368,7 @@ function List() {
{ {
icon: 'delete', icon: 'delete',
label: t('common:Delete'), label: t('common:Delete'),
type: 'danger' as 'danger', type: 'danger' as const,
onClick: () => onClick: () =>
openConfirm({ openConfirm({
onConfirm: () => onConfirm: () =>
...@@ -375,8 +376,7 @@ function List() { ...@@ -375,8 +376,7 @@ function List() {
refetchPaths(); refetchPaths();
loadMyDatasets(); loadMyDatasets();
}), }),
customContent: customContent: DeleteTipsMap.current[dataset.type],
DeleteTipsMap.current[DatasetTypeEnum.dataset],
inputConfirmText: dataset.name inputConfirmText: dataset.name
})() })()
} }
...@@ -393,7 +393,23 @@ function List() { ...@@ -393,7 +393,23 @@ function List() {
</MyBox> </MyBox>
</MyTooltip> </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> </Grid>
)} )}
{myDatasets.length === 0 && ( {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