From 019ef74bf2406d534bccd8ac24f0fa5c52df494b Mon Sep 17 00:00:00 2001 From: twwu Date: Fri, 16 May 2025 10:50:31 +0800 Subject: [PATCH] refactor: replace Container with List, update DatasetCard z-index, and implement useDatasetList for data fetching --- web/app/(commonLayout)/datasets/page.tsx | 8 +- .../components/datasets/list/container.tsx | 146 ------------------ .../datasets/list/dataset-card/index.tsx | 68 ++++---- web/app/components/datasets/list/datasets.tsx | 104 +++++-------- web/app/components/datasets/list/index.tsx | 2 +- .../datasets/list/new-dataset-card/index.tsx | 11 +- web/models/datasets.ts | 8 + web/service/knowledge/use-dataset.ts | 32 ++++ 8 files changed, 121 insertions(+), 258 deletions(-) delete mode 100644 web/app/components/datasets/list/container.tsx create mode 100644 web/service/knowledge/use-dataset.ts diff --git a/web/app/(commonLayout)/datasets/page.tsx b/web/app/(commonLayout)/datasets/page.tsx index 95c2d6fc5c..8388b69468 100644 --- a/web/app/(commonLayout)/datasets/page.tsx +++ b/web/app/(commonLayout)/datasets/page.tsx @@ -1,7 +1,7 @@ -import Container from '../../components/datasets/list/container' +import List from '../../components/datasets/list' -const AppList = async () => { - return +const DatasetList = async () => { + return } -export default AppList +export default DatasetList diff --git a/web/app/components/datasets/list/container.tsx b/web/app/components/datasets/list/container.tsx deleted file mode 100644 index 7b70f78c57..0000000000 --- a/web/app/components/datasets/list/container.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client' - -// Libraries -import { useEffect, useMemo, useRef, useState } from 'react' -import { useRouter } from 'next/navigation' -import { useTranslation } from 'react-i18next' -import { useBoolean, useDebounceFn } from 'ahooks' -import { useQuery } from '@tanstack/react-query' - -// Components -import ExternalAPIPanel from '../external-api/external-api-panel' -import Datasets from './datasets' -import DatasetFooter from './dataset-footer' -import ApiServer from '../../develop/ApiServer' -import Doc from './doc' -import SegmentedControl from '@/app/components/base/segmented-control' -import { RiBook2Line, RiTerminalBoxLine } from '@remixicon/react' -import TagManagementModal from '@/app/components/base/tag-management' -import TagFilter from '@/app/components/base/tag-management/filter' -import Button from '@/app/components/base/button' -import Input from '@/app/components/base/input' -import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development' -import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' - -// Services -import { fetchDatasetApiBaseUrl } from '@/service/datasets' - -// Hooks -import { useTabSearchParams } from '@/hooks/use-tab-searchparams' -import { useStore as useTagStore } from '@/app/components/base/tag-management/store' -import { useAppContext } from '@/context/app-context' -import { useExternalApiPanel } from '@/context/external-api-panel-context' - -const Container = () => { - const { t } = useTranslation() - const router = useRouter() - const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext() - const showTagManagementModal = useTagStore(s => s.showTagManagementModal) - const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel() - const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false) - - document.title = `${t('dataset.knowledge')} - Dify` - - const options = useMemo(() => { - return [ - { value: 'dataset', text: t('dataset.datasets'), Icon: RiBook2Line }, - ...(currentWorkspace.role === 'dataset_operator' ? [] : [{ - value: 'api', text: t('dataset.datasetsApi'), Icon: RiTerminalBoxLine, - }]), - ] - }, [currentWorkspace.role, t]) - - const [activeTab, setActiveTab] = useTabSearchParams({ - defaultTab: 'dataset', - }) - const containerRef = useRef(null) - const { data } = useQuery( - { - queryKey: ['datasetApiBaseInfo'], - queryFn: () => fetchDatasetApiBaseUrl('/datasets/api-base-info'), - enabled: activeTab !== 'dataset', - }, - ) - - const [keywords, setKeywords] = useState('') - const [searchKeywords, setSearchKeywords] = useState('') - const { run: handleSearch } = useDebounceFn(() => { - setSearchKeywords(keywords) - }, { wait: 500 }) - const handleKeywordsChange = (value: string) => { - setKeywords(value) - handleSearch() - } - const [tagFilterValue, setTagFilterValue] = useState([]) - const [tagIDs, setTagIDs] = useState([]) - const { run: handleTagsUpdate } = useDebounceFn(() => { - setTagIDs(tagFilterValue) - }, { wait: 500 }) - const handleTagsChange = (value: string[]) => { - setTagFilterValue(value) - handleTagsUpdate() - } - - useEffect(() => { - if (currentWorkspace.role === 'normal') - return router.replace('/apps') - }, [currentWorkspace, router]) - - return ( -
-
- setActiveTab(newActiveTab as string)} - options={options} - size='large' - activeClassName='text-text-primary' - /> - {activeTab === 'dataset' && ( -
- {isCurrentWorkspaceOwner && } - - handleKeywordsChange(e.target.value)} - onClear={() => handleKeywordsChange('')} - /> -
- -
- )} - {activeTab === 'api' && data && } -
- {activeTab === 'dataset' && ( - <> - - - {showTagManagementModal && ( - - )} - - )} - {activeTab === 'api' && data && } - - {showExternalApiPanel && setShowExternalApiPanel(false)} />} -
- ) -} - -export default Container diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 15190df4a3..d83dc5b2e1 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -133,7 +133,7 @@ const DatasetCard = ({ background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background} imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined} /> -
+
@@ -189,7 +189,7 @@ const DatasetCard = ({ {/* Tag Mask */}
@@ -202,49 +202,49 @@ const DatasetCard = ({ >
- - {documentCount} + + {documentCount}
{!isExternalProvider && (
- - {dataset.app_count} + + {dataset.app_count}
)} / {`${t('dataset.updated')} ${formatTimeFromNow(dataset.updated_at)}`}
-
- { - setShowRenameModal(true) - }} - detectIsUsedByApp={detectIsUsedByApp} - /> - } - className={'z-20 min-w-[186px]'} - popupClassName={'rounded-xl bg-none shadow-none ring-0 min-w-[186px]'} - position='br' - trigger='click' - btnElement={ -
- -
- } - btnClassName={open => - cn( - 'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border', - open ? 'border-components-actionbar-border bg-state-base-hover' : '', - ) - } - /> -
+
+ { + setShowRenameModal(true) + }} + detectIsUsedByApp={detectIsUsedByApp} + /> + } + className={'z-20 min-w-[186px]'} + popupClassName={'rounded-xl bg-none shadow-none ring-0 min-w-[186px]'} + position='br' + trigger='click' + btnElement={ +
+ +
+ } + btnClassName={open => + cn( + 'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border', + open ? 'border-components-actionbar-border bg-state-base-hover' : '', + ) + } + /> +
{showRenameModal && ( { - if (!pageIndex || previousPageData.has_more) { - const params: FetchDatasetsParams = { - url: 'datasets', - params: { - page: pageIndex + 1, - limit: 30, - include_all: includeAll, - }, - } - if (tags.length) - params.params.tag_ids = tags - if (keyword) - params.params.keyword = keyword - return params - } - return null -} +import { useSelector as useAppContextWithSelector } from '@/context/app-context' +import { useDatasetList, useResetDatasetList } from '@/service/knowledge/use-dataset' type Props = { containerRef: React.RefObject @@ -43,53 +15,57 @@ type Props = { } const Datasets = ({ - containerRef, tags, keywords, includeAll, }: Props) => { const { t } = useTranslation() - const { isCurrentWorkspaceEditor } = useAppContext() - const { data, isLoading, setSize, mutate } = useSWRInfinite( - (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll), - fetchDatasets, - { revalidateFirstPage: false, revalidateAll: true }, - ) + const isCurrentWorkspaceEditor = useAppContextWithSelector(state => state.isCurrentWorkspaceEditor) + const { + data, + fetchNextPage, + hasNextPage, + isFetching, + } = useDatasetList({ + initialPage: 1, + tag_ids: tags, + limit: 20, + include_all: includeAll, + keyword: keywords, + }) + const resetDatasetList = useResetDatasetList() const loadingStateRef = useRef(false) - const anchorRef = useRef(null) + const anchorRef = useRef(null) + const observerRef = useRef() useEffect(() => { - loadingStateRef.current = isLoading + loadingStateRef.current = isFetching document.title = `${t('dataset.knowledge')} - Dify` - }, [isLoading, t]) - - const onScroll = useMemo(() => { - return debounce(() => { - if (!loadingStateRef.current && containerRef.current && anchorRef.current) { - const { scrollTop, clientHeight } = containerRef.current - const anchorOffset = anchorRef.current.offsetTop - if (anchorOffset - scrollTop - clientHeight < 100) - setSize(size => size + 1) - } - }, 50) - }, [containerRef, setSize]) + }, [isFetching, t]) useEffect(() => { - const currentContainer = containerRef.current - currentContainer?.addEventListener('scroll', onScroll) - return () => { - currentContainer?.removeEventListener('scroll', onScroll) - onScroll.cancel() + if (anchorRef.current) { + observerRef.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasNextPage) + fetchNextPage() + }, { + rootMargin: '100px', + }) + observerRef.current.observe(anchorRef.current) } - }, [onScroll, containerRef]) + return () => observerRef.current?.disconnect() + }, [anchorRef, data, hasNextPage, fetchNextPage]) return ( - + <> + +
+ ) } diff --git a/web/app/components/datasets/list/index.tsx b/web/app/components/datasets/list/index.tsx index 61f3d523e9..fc95f64e5c 100644 --- a/web/app/components/datasets/list/index.tsx +++ b/web/app/components/datasets/list/index.tsx @@ -88,7 +88,7 @@ const List = () => { return (
-
+
setActiveTab(newActiveTab as string)} diff --git a/web/app/components/datasets/list/new-dataset-card/index.tsx b/web/app/components/datasets/list/new-dataset-card/index.tsx index 7d6d7e3d2f..541c9d38ab 100644 --- a/web/app/components/datasets/list/new-dataset-card/index.tsx +++ b/web/app/components/datasets/list/new-dataset-card/index.tsx @@ -9,21 +9,14 @@ import { import Link from './link' import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development' -type CreateAppCardProps = { - ref: React.RefObject -} - -const CreateAppCard = ({ - ref, - ..._ -}: CreateAppCardProps) => { +const CreateAppCard = () => { const { t } = useTranslation() return (
- +
diff --git a/web/models/datasets.ts b/web/models/datasets.ts index 6b0a00182a..bbe6f2dc24 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -175,6 +175,14 @@ export type FetchDatasetsParams = { } } +export type DatasetListRequest = { + initialPage: number + tag_ids: string[] + limit: number + include_all: boolean + keyword: string +} + export type DataSetListResponse = { data: DataSet[] has_more: boolean diff --git a/web/service/knowledge/use-dataset.ts b/web/service/knowledge/use-dataset.ts new file mode 100644 index 0000000000..8fe4447a7a --- /dev/null +++ b/web/service/knowledge/use-dataset.ts @@ -0,0 +1,32 @@ +import { useInfiniteQuery } from '@tanstack/react-query' +import type { DataSetListResponse, DatasetListRequest } from '@/models/datasets' +import { get } from '../base' +import { useReset } from '../use-base' +import qs from 'qs' + +const NAME_SPACE = 'dataset' + +const DatasetListKey = [NAME_SPACE, 'list'] + +export const useDatasetList = (params: DatasetListRequest) => { + const { initialPage, tag_ids, limit, include_all, keyword } = params + return useInfiniteQuery({ + queryKey: [...DatasetListKey, initialPage, tag_ids, limit, include_all, keyword], + queryFn: ({ pageParam = 1 }) => { + const urlParams = qs.stringify({ + tag_ids, + limit, + include_all, + keyword, + page: pageParam, + }, { indices: false }) + return get(`/datasets?${urlParams}`) + }, + getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : null, + initialPageParam: initialPage, + }) +} + +export const useResetDatasetList = () => { + return useReset([...DatasetListKey]) +}