From 77366f33a4be9a23fcc3e246653bdcb2de11074c Mon Sep 17 00:00:00 2001 From: Pegasus <42954461+leonace924@users.noreply.github.com> Date: Sat, 17 Jan 2026 04:36:07 -0500 Subject: [PATCH] feat(web): add loading indicators for infinite scroll pagination (#31110) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> --- .../dataset-config/select-dataset/index.tsx | 1 + web/app/components/apps/app-card-skeleton.tsx | 10 +++++----- web/app/components/apps/list.tsx | 3 +++ web/app/components/base/loading/index.tsx | 17 +++++++++++------ web/app/components/datasets/list/datasets.tsx | 3 +++ web/app/components/header/app-nav/index.tsx | 2 ++ web/app/components/header/dataset-nav/index.tsx | 2 ++ web/app/components/header/nav/index.tsx | 2 ++ .../header/nav/nav-selector/index.tsx | 9 ++++++++- .../plugins/marketplace/list/list-wrapper.tsx | 6 ++++++ web/app/components/plugins/marketplace/state.ts | 3 ++- .../plugins/plugin-page/plugins-panel.tsx | 17 +++++++++++------ 12 files changed, 56 insertions(+), 19 deletions(-) diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx index b60f6797bf..36d78920fa 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx @@ -189,6 +189,7 @@ const SelectDataSet: FC = ({ } ))} + {isFetchingNextPage && } )} diff --git a/web/app/components/apps/app-card-skeleton.tsx b/web/app/components/apps/app-card-skeleton.tsx index 806f19973a..5fc41f76ab 100644 --- a/web/app/components/apps/app-card-skeleton.tsx +++ b/web/app/components/apps/app-card-skeleton.tsx @@ -21,15 +21,15 @@ export const AppCardSkeleton = React.memo(({ count = 6 }: AppCardSkeletonProps) > - +
- - + +
- - + +
diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 84150ad480..8a236fe260 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -248,6 +248,9 @@ const List = () => { // No apps - show empty state return })()} + {isFetchingNextPage && ( + + )} {isCurrentWorkspaceEditor && ( diff --git a/web/app/components/base/loading/index.tsx b/web/app/components/base/loading/index.tsx index 0f0cc9f24e..a946edf36c 100644 --- a/web/app/components/base/loading/index.tsx +++ b/web/app/components/base/loading/index.tsx @@ -1,21 +1,25 @@ 'use client' -import * as React from 'react' import { useTranslation } from 'react-i18next' - +import { cn } from '@/utils/classnames' import './style.css' type ILoadingProps = { type?: 'area' | 'app' + className?: string } -const Loading = ( - { type = 'area' }: ILoadingProps = { type: 'area' }, -) => { + +const Loading = (props?: ILoadingProps) => { + const { type = 'area', className } = props || {} const { t } = useTranslation() return (
) } + export default Loading diff --git a/web/app/components/datasets/list/datasets.tsx b/web/app/components/datasets/list/datasets.tsx index 44b6553750..817b927e6b 100644 --- a/web/app/components/datasets/list/datasets.tsx +++ b/web/app/components/datasets/list/datasets.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' +import Loading from '@/app/components/base/loading' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset' import DatasetCard from './dataset-card' @@ -25,6 +26,7 @@ const Datasets = ({ fetchNextPage, hasNextPage, isFetching, + isFetchingNextPage, } = useDatasetList({ initialPage: 1, tag_ids: tags, @@ -60,6 +62,7 @@ const Datasets = ({ {datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => ( ), ))} + {isFetchingNextPage && }
diff --git a/web/app/components/header/app-nav/index.tsx b/web/app/components/header/app-nav/index.tsx index cc4f32a7ca..737dd96bab 100644 --- a/web/app/components/header/app-nav/index.tsx +++ b/web/app/components/header/app-nav/index.tsx @@ -33,6 +33,7 @@ const AppNav = () => { data: appsData, fetchNextPage, hasNextPage, + isFetchingNextPage, refetch, } = useInfiniteAppList({ page: 1, @@ -111,6 +112,7 @@ const AppNav = () => { createText={t('menus.newApp', { ns: 'common' })} onCreate={openModal} onLoadMore={handleLoadMore} + isLoadingMore={isFetchingNextPage} /> { data: datasetList, fetchNextPage, hasNextPage, + isFetchingNextPage, } = useDatasetList({ initialPage: 1, limit: 30, @@ -93,6 +94,7 @@ const DatasetNav = () => { createText={t('menus.newDataset', { ns: 'common' })} onCreate={() => router.push(createRoute)} onLoadMore={handleLoadMore} + isLoadingMore={isFetchingNextPage} /> ) } diff --git a/web/app/components/header/nav/index.tsx b/web/app/components/header/nav/index.tsx index 2edc64486e..ca4498e4fb 100644 --- a/web/app/components/header/nav/index.tsx +++ b/web/app/components/header/nav/index.tsx @@ -30,6 +30,7 @@ const Nav = ({ createText, onCreate, onLoadMore, + isLoadingMore, isApp, }: INavProps) => { const setAppDetail = useAppStore(state => state.setAppDetail) @@ -81,6 +82,7 @@ const Nav = ({ createText={createText} onCreate={onCreate} onLoadMore={onLoadMore} + isLoadingMore={isLoadingMore} /> ) diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index 178cba2f28..e66837c06c 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -14,6 +14,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' +import Loading from '@/app/components/base/loading' import { useAppContext } from '@/context/app-context' import { cn } from '@/utils/classnames' @@ -34,9 +35,10 @@ export type INavSelectorProps = { isApp?: boolean onCreate: (state: string) => void onLoadMore?: () => void + isLoadingMore?: boolean } -const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore }: INavSelectorProps) => { +const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore, isLoadingMore }: INavSelectorProps) => { const { t } = useTranslation() const router = useRouter() const { isCurrentWorkspaceEditor } = useAppContext() @@ -106,6 +108,11 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL )) } + {isLoadingMore && ( +
+ +
+ )}
{!isApp && isCurrentWorkspaceEditor && ( diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index a1b0c2529a..950b73ddbd 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -19,6 +19,7 @@ const ListWrapper = ({ marketplaceCollections, marketplaceCollectionPluginsMap, isLoading, + isFetchingNextPage, page, } = useMarketplaceData() @@ -53,6 +54,11 @@ const ListWrapper = ({ /> ) } + { + isFetchingNextPage && ( + + ) + }
) } diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts index 9c76a21e92..4954acd60c 100644 --- a/web/app/components/plugins/marketplace/state.ts +++ b/web/app/components/plugins/marketplace/state.ts @@ -33,7 +33,7 @@ export function useMarketplaceData() { }, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort]) const pluginsQuery = useMarketplacePlugins(queryParams) - const { hasNextPage, fetchNextPage, isFetching } = pluginsQuery + const { hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = pluginsQuery const handlePageChange = useCallback(() => { if (hasNextPage && !isFetching) @@ -50,5 +50,6 @@ export function useMarketplaceData() { pluginsTotal: pluginsQuery.data?.pages[0]?.total, page: pluginsQuery.data?.pages.length || 1, isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading, + isFetchingNextPage, } } diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index ff765d39ab..d2ba8b2363 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -5,11 +5,11 @@ import { useDebounceFn } from 'ahooks' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' +import Loading from '@/app/components/base/loading' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' import { useGetLanguage } from '@/context/i18n' import { renderI18nObject } from '@/i18n-config' import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' -import Loading from '../../base/loading' import { PluginSource } from '../types' import { usePluginPageContext } from './context' import Empty from './empty' @@ -107,12 +107,17 @@ const PluginsPanel = () => {
- {!isLastPage && !isFetching && ( - + {!isLastPage && ( +
+ {isFetching + ? + : ( + + )} +
)} - {isFetching &&
{t('detail.loading', { ns: 'appLog' })}
} ) : (