'use client' import type { SnippetPublishStatus } from './components/snippet-publish-status-filter' import type { SnippetListItem } from '@/types/snippet' import { cn } from '@langgenius/dify-ui/cn' import { Input } from '@langgenius/dify-ui/input' import { useDebounce } from 'ahooks' import { useAtomValue } from 'jotai' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { currentWorkspaceLoadingAtom } from '@/context/workspace-state' import { TagFilter } from '@/features/tag-management/components/tag-filter' import useDocumentTitle from '@/hooks/use-document-title' import dynamic from '@/next/dynamic' import Link from '@/next/link' import { useInfiniteSnippetList } from '@/service/use-snippets' import CreatorsFilter from '../apps/creators-filter' import Empty from '../apps/empty' import { StudioListHeader } from '../apps/studio-list-header' import { canAccessSnippets } from '../snippets/utils/permission' import SnippetCard from './components/snippet-card' import SnippetCreateButton from './components/snippet-create-button' import SnippetPublishStatusFilter from './components/snippet-publish-status-filter' import { SNIPPET_LIST_SEARCH_DEBOUNCE_MS } from './constants' import { useSnippetsQueryState } from './hooks/use-snippets-query-state' const TagManagementModal = dynamic( () => import('@/features/tag-management/components/tag-management-modal').then( (mod) => mod.TagManagementModal, ), { ssr: false, }, ) const SNIPPET_CARD_SKELETON_KEYS = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth'] const toSnippetPublishedQuery = (publishStatus: SnippetPublishStatus) => { if (publishStatus === 'published') return true if (publishStatus === 'draft') return false return undefined } type SnippetCardSkeletonProps = { count: number } const SnippetCardSkeleton = ({ count }: SnippetCardSkeletonProps) => { return ( <> {SNIPPET_CARD_SKELETON_KEYS.slice(0, count).map((key) => (
))} ) } const SnippetList = () => { const { t } = useTranslation() const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) // oxlint-disable-next-line eslint-react/use-state -- custom URL query hook, not React.useState const { query: { tagIDs, keywords, creatorIDs }, setKeywords, setTagIDs, setCreatorIDs, } = useSnippetsQueryState() const debouncedKeywords = useDebounce(keywords, { wait: SNIPPET_LIST_SEARCH_DEBOUNCE_MS }) const containerRef = useRef(null) const anchorRef = useRef(null) const [showTagManagementModal, setShowTagManagementModal] = useState(false) const [publishStatus, setPublishStatus] = useState('all') useDocumentTitle(t(($) => $['tabs.snippets'], { ns: 'workflow' })) const snippetListQuery = useMemo(() => { const isPublished = toSnippetPublishedQuery(publishStatus) return { page: 1, limit: 30, keyword: debouncedKeywords, ...(tagIDs.length ? { tag_ids: tagIDs } : {}), ...(creatorIDs.length ? { creator_ids: creatorIDs } : {}), ...(typeof isPublished === 'boolean' ? { is_published: isPublished } : {}), } }, [creatorIDs, debouncedKeywords, publishStatus, tagIDs]) const canQuerySnippetList = canAccessSnippets(workspacePermissionKeys) const { data, isLoading, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage, error, refetch, } = useInfiniteSnippetList(snippetListQuery, { enabled: canQuerySnippetList, }) useEffect(() => { if (!canQuerySnippetList) return const hasMore = hasNextPage ?? true let observer: IntersectionObserver | undefined if (error) { if (observer) observer.disconnect() return } if (anchorRef.current && containerRef.current) { const containerHeight = containerRef.current.clientHeight const dynamicMargin = Math.max(100, Math.min(containerHeight * 0.2, 200)) observer = new IntersectionObserver( (entries) => { if (entries[0]!.isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore) fetchNextPage() }, { root: containerRef.current, rootMargin: `${dynamicMargin}px`, threshold: 0.1, }, ) observer.observe(anchorRef.current) } return () => observer?.disconnect() }, [canQuerySnippetList, error, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading]) const pages = useMemo( () => (canQuerySnippetList ? (data?.pages ?? []) : []), [canQuerySnippetList, data?.pages], ) const snippets = useMemo( () => pages.flatMap(({ data: pageSnippets }) => pageSnippets), [pages], ) const hasAnySnippet = (pages[0]?.total ?? 0) > 0 const showSkeleton = isLoadingCurrentWorkspace || (canQuerySnippetList && (isLoading || (isFetching && pages.length === 0))) return (
{t(($) => $['menus.apps'], { ns: 'common' })} /

{t(($) => $['tabs.snippets'], { ns: 'workflow' })}

} >
setShowTagManagementModal(true)} />
setKeywords(e.target.value)} placeholder={t(($) => $['tabs.searchSnippets'], { ns: 'workflow' })} /> {!!keywords && ( )}
{showSkeleton ? ( ) : hasAnySnippet ? ( snippets.map((snippet) => ( setShowTagManagementModal(true)} onRefresh={refetch} onTagsChange={refetch} /> )) ) : ( $['tabs.noSnippetsFound'], { ns: 'workflow' })} /> )} {isFetchingNextPage && }
{' '}
setShowTagManagementModal(false)} onTagsChange={refetch} />
) } export default SnippetList