mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 13:37:24 +08:00
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>
This commit is contained in:
parent
e3b0918dd9
commit
77366f33a4
@ -189,6 +189,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{isFetchingNextPage && <Loading />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -21,15 +21,15 @@ export const AppCardSkeleton = React.memo(({ count = 6 }: AppCardSkeletonProps)
|
|||||||
>
|
>
|
||||||
<SkeletonContainer className="h-full">
|
<SkeletonContainer className="h-full">
|
||||||
<SkeletonRow>
|
<SkeletonRow>
|
||||||
<SkeletonRectangle className="h-10 w-10 rounded-lg" />
|
<SkeletonRectangle className="h-10 w-10 animate-pulse rounded-lg" />
|
||||||
<div className="flex flex-1 flex-col gap-1">
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
<SkeletonRectangle className="h-4 w-2/3" />
|
<SkeletonRectangle className="h-4 w-2/3 animate-pulse" />
|
||||||
<SkeletonRectangle className="h-3 w-1/3" />
|
<SkeletonRectangle className="h-3 w-1/3 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</SkeletonRow>
|
</SkeletonRow>
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
<SkeletonRectangle className="h-3 w-full" />
|
<SkeletonRectangle className="h-3 w-full animate-pulse" />
|
||||||
<SkeletonRectangle className="h-3 w-4/5" />
|
<SkeletonRectangle className="h-3 w-4/5 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</SkeletonContainer>
|
</SkeletonContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -248,6 +248,9 @@ const List = () => {
|
|||||||
// No apps - show empty state
|
// No apps - show empty state
|
||||||
return <Empty />
|
return <Empty />
|
||||||
})()}
|
})()}
|
||||||
|
{isFetchingNextPage && (
|
||||||
|
<AppCardSkeleton count={3} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCurrentWorkspaceEditor && (
|
{isCurrentWorkspaceEditor && (
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import * as React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { cn } from '@/utils/classnames'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
type ILoadingProps = {
|
type ILoadingProps = {
|
||||||
type?: 'area' | 'app'
|
type?: 'area' | 'app'
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
const Loading = (
|
|
||||||
{ type = 'area' }: ILoadingProps = { type: 'area' },
|
const Loading = (props?: ILoadingProps) => {
|
||||||
) => {
|
const { type = 'area', className } = props || {}
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex w-full items-center justify-center ${type === 'app' ? 'h-full' : ''}`}
|
className={cn(
|
||||||
|
'flex w-full items-center justify-center',
|
||||||
|
type === 'app' && 'h-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label={t('loading', { ns: 'appApi' })}
|
aria-label={t('loading', { ns: 'appApi' })}
|
||||||
@ -37,4 +41,5 @@ const Loading = (
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Loading
|
export default Loading
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||||
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||||
import DatasetCard from './dataset-card'
|
import DatasetCard from './dataset-card'
|
||||||
@ -25,6 +26,7 @@ const Datasets = ({
|
|||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
isFetchingNextPage,
|
||||||
} = useDatasetList({
|
} = useDatasetList({
|
||||||
initialPage: 1,
|
initialPage: 1,
|
||||||
tag_ids: tags,
|
tag_ids: tags,
|
||||||
@ -60,6 +62,7 @@ const Datasets = ({
|
|||||||
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
|
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
|
||||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} />),
|
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} />),
|
||||||
))}
|
))}
|
||||||
|
{isFetchingNextPage && <Loading />}
|
||||||
<div ref={anchorRef} className="h-0" />
|
<div ref={anchorRef} className="h-0" />
|
||||||
</nav>
|
</nav>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ const AppNav = () => {
|
|||||||
data: appsData,
|
data: appsData,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
refetch,
|
refetch,
|
||||||
} = useInfiniteAppList({
|
} = useInfiniteAppList({
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -111,6 +112,7 @@ const AppNav = () => {
|
|||||||
createText={t('menus.newApp', { ns: 'common' })}
|
createText={t('menus.newApp', { ns: 'common' })}
|
||||||
onCreate={openModal}
|
onCreate={openModal}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
|
isLoadingMore={isFetchingNextPage}
|
||||||
/>
|
/>
|
||||||
<CreateAppModal
|
<CreateAppModal
|
||||||
show={showNewAppDialog}
|
show={showNewAppDialog}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const DatasetNav = () => {
|
|||||||
data: datasetList,
|
data: datasetList,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
} = useDatasetList({
|
} = useDatasetList({
|
||||||
initialPage: 1,
|
initialPage: 1,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
@ -93,6 +94,7 @@ const DatasetNav = () => {
|
|||||||
createText={t('menus.newDataset', { ns: 'common' })}
|
createText={t('menus.newDataset', { ns: 'common' })}
|
||||||
onCreate={() => router.push(createRoute)}
|
onCreate={() => router.push(createRoute)}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
|
isLoadingMore={isFetchingNextPage}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ const Nav = ({
|
|||||||
createText,
|
createText,
|
||||||
onCreate,
|
onCreate,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
|
isLoadingMore,
|
||||||
isApp,
|
isApp,
|
||||||
}: INavProps) => {
|
}: INavProps) => {
|
||||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||||
@ -81,6 +82,7 @@ const Nav = ({
|
|||||||
createText={createText}
|
createText={createText}
|
||||||
onCreate={onCreate}
|
onCreate={onCreate}
|
||||||
onLoadMore={onLoadMore}
|
onLoadMore={onLoadMore}
|
||||||
|
isLoadingMore={isLoadingMore}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
|||||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
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 { useAppContext } from '@/context/app-context'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
|
||||||
@ -34,9 +35,10 @@ export type INavSelectorProps = {
|
|||||||
isApp?: boolean
|
isApp?: boolean
|
||||||
onCreate: (state: string) => void
|
onCreate: (state: string) => void
|
||||||
onLoadMore?: () => 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 { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||||
@ -106,6 +108,11 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="flex justify-center py-2">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isApp && isCurrentWorkspaceEditor && (
|
{!isApp && isCurrentWorkspaceEditor && (
|
||||||
<MenuItem as="div" className="w-full p-1">
|
<MenuItem as="div" className="w-full p-1">
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const ListWrapper = ({
|
|||||||
marketplaceCollections,
|
marketplaceCollections,
|
||||||
marketplaceCollectionPluginsMap,
|
marketplaceCollectionPluginsMap,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isFetchingNextPage,
|
||||||
page,
|
page,
|
||||||
} = useMarketplaceData()
|
} = useMarketplaceData()
|
||||||
|
|
||||||
@ -53,6 +54,11 @@ const ListWrapper = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
isFetchingNextPage && (
|
||||||
|
<Loading className="my-3" />
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function useMarketplaceData() {
|
|||||||
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
|
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
|
||||||
|
|
||||||
const pluginsQuery = useMarketplacePlugins(queryParams)
|
const pluginsQuery = useMarketplacePlugins(queryParams)
|
||||||
const { hasNextPage, fetchNextPage, isFetching } = pluginsQuery
|
const { hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = pluginsQuery
|
||||||
|
|
||||||
const handlePageChange = useCallback(() => {
|
const handlePageChange = useCallback(() => {
|
||||||
if (hasNextPage && !isFetching)
|
if (hasNextPage && !isFetching)
|
||||||
@ -50,5 +50,6 @@ export function useMarketplaceData() {
|
|||||||
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
|
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
|
||||||
page: pluginsQuery.data?.pages.length || 1,
|
page: pluginsQuery.data?.pages.length || 1,
|
||||||
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
|
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
|
||||||
|
isFetchingNextPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import { useDebounceFn } from 'ahooks'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
import { renderI18nObject } from '@/i18n-config'
|
import { renderI18nObject } from '@/i18n-config'
|
||||||
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||||
import Loading from '../../base/loading'
|
|
||||||
import { PluginSource } from '../types'
|
import { PluginSource } from '../types'
|
||||||
import { usePluginPageContext } from './context'
|
import { usePluginPageContext } from './context'
|
||||||
import Empty from './empty'
|
import Empty from './empty'
|
||||||
@ -107,12 +107,17 @@ const PluginsPanel = () => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<List pluginList={filteredList || []} />
|
<List pluginList={filteredList || []} />
|
||||||
</div>
|
</div>
|
||||||
{!isLastPage && !isFetching && (
|
{!isLastPage && (
|
||||||
<Button onClick={loadNextPage}>
|
<div className="flex justify-center py-4">
|
||||||
{t('common.loadMore', { ns: 'workflow' })}
|
{isFetching
|
||||||
</Button>
|
? <Loading className="size-8" />
|
||||||
|
: (
|
||||||
|
<Button onClick={loadNextPage}>
|
||||||
|
{t('common.loadMore', { ns: 'workflow' })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isFetching && <div className="system-md-semibold text-text-secondary">{t('detail.loading', { ns: 'appLog' })}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user