diff --git a/web/app/components/snippet-list/__tests__/index.spec.tsx b/web/app/components/snippet-list/__tests__/index.spec.tsx
index dca361764eb..9fad5bc2a4f 100644
--- a/web/app/components/snippet-list/__tests__/index.spec.tsx
+++ b/web/app/components/snippet-list/__tests__/index.spec.tsx
@@ -262,6 +262,7 @@ describe('SnippetList', () => {
expect(screen.getByRole('link', { name: 'common.menus.apps' })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('heading', { name: 'workflow.tabs.snippets' })).toBeInTheDocument()
expect(screen.getByText('app.studio.filters.creators')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /workflow\.common\.published \/ snippet\.draft/i })).toBeInTheDocument()
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
expect(screen.getByPlaceholderText('workflow.tabs.searchSnippets')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'snippet.create' })).toBeInTheDocument()
@@ -287,6 +288,42 @@ describe('SnippetList', () => {
})
})
+ it('does not pass published state to the snippets list query by default', () => {
+ renderList()
+
+ expect(mockUseInfiniteSnippetList).toHaveBeenCalledWith(expect.not.objectContaining({
+ is_published: expect.any(Boolean),
+ }), {
+ enabled: true,
+ })
+ })
+
+ it('passes published state when selecting the published filter', () => {
+ renderList()
+
+ fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.published \/ snippet\.draft/i }))
+ fireEvent.click(screen.getByRole('menuitemradio', { name: /workflow\.common\.published/i }))
+
+ expect(mockUseInfiniteSnippetList).toHaveBeenLastCalledWith(expect.objectContaining({
+ is_published: true,
+ }), {
+ enabled: true,
+ })
+ })
+
+ it('passes draft state when selecting the draft filter', () => {
+ renderList()
+
+ fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.published \/ snippet\.draft/i }))
+ fireEvent.click(screen.getByRole('menuitemradio', { name: /snippet\.draft/i }))
+
+ expect(mockUseInfiniteSnippetList).toHaveBeenLastCalledWith(expect.objectContaining({
+ is_published: false,
+ }), {
+ enabled: true,
+ })
+ })
+
it('updates the search query state from the search input', () => {
renderList()
diff --git a/web/app/components/snippet-list/components/__tests__/snippet-publish-status-filter.spec.tsx b/web/app/components/snippet-list/components/__tests__/snippet-publish-status-filter.spec.tsx
new file mode 100644
index 00000000000..c9ffbf9df0f
--- /dev/null
+++ b/web/app/components/snippet-list/components/__tests__/snippet-publish-status-filter.spec.tsx
@@ -0,0 +1,26 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import SnippetPublishStatusFilter from '../snippet-publish-status-filter'
+
+describe('SnippetPublishStatusFilter', () => {
+ it('should render the default published and draft filter label', () => {
+ render()
+
+ expect(screen.getByRole('button', { name: /workflow\.common\.published \/ snippet\.draft/i })).toBeInTheDocument()
+ })
+
+ it('should emit the selected publish status from the dropdown', () => {
+ const onChange = vi.fn()
+ render()
+
+ fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.published \/ snippet\.draft/i }))
+ fireEvent.click(screen.getByRole('menuitemradio', { name: /workflow\.common\.published/i }))
+
+ expect(onChange).toHaveBeenCalledWith('published')
+ })
+
+ it('should render the selected draft status label', () => {
+ render()
+
+ expect(screen.getByRole('button', { name: /snippet\.draft/i })).toBeInTheDocument()
+ })
+})
diff --git a/web/app/components/snippet-list/components/snippet-publish-status-filter.tsx b/web/app/components/snippet-list/components/snippet-publish-status-filter.tsx
new file mode 100644
index 00000000000..4828a555ab9
--- /dev/null
+++ b/web/app/components/snippet-list/components/snippet-publish-status-filter.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import { cn } from '@langgenius/dify-ui/cn'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuRadioItemIndicator,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+
+export type SnippetPublishStatus = 'all' | 'published' | 'draft'
+
+type SnippetPublishStatusFilterProps = {
+ value: SnippetPublishStatus
+ onChange: (value: SnippetPublishStatus) => void
+}
+
+const chipClassName = 'flex h-8 items-center rounded-lg border-[0.5px] px-2 text-[13px] leading-4 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-state-accent-solid'
+const snippetPublishStatusValues: SnippetPublishStatus[] = ['all', 'published', 'draft']
+
+const isSnippetPublishStatus = (value: string): value is SnippetPublishStatus => {
+ return snippetPublishStatusValues.includes(value as SnippetPublishStatus)
+}
+
+const SnippetPublishStatusFilter = ({
+ value,
+ onChange,
+}: SnippetPublishStatusFilterProps) => {
+ const { t } = useTranslation()
+
+ const options = useMemo(() => ([
+ { value: 'all', text: t('types.all', { ns: 'app' }) },
+ { value: 'published', text: t('common.published', { ns: 'workflow' }) },
+ { value: 'draft', text: t('draft', { ns: 'snippet' }) },
+ ] satisfies Array<{ value: SnippetPublishStatus, text: string }>), [t])
+
+ const activeOption = options.find(option => option.value === value)
+ const isSelected = value !== 'all'
+ const defaultLabel = `${t('common.published', { ns: 'workflow' })} / ${t('draft', { ns: 'snippet' })}`
+ const triggerLabel = isSelected ? activeOption?.text : defaultLabel
+
+ return (
+
+
+ )}
+ >
+ {triggerLabel}
+
+
+
+ isSnippetPublishStatus(nextValue) && onChange(nextValue)}>
+ {options.map(option => (
+
+ {option.text}
+
+
+ ))}
+
+
+
+ )
+}
+
+export default SnippetPublishStatusFilter
diff --git a/web/app/components/snippet-list/index.tsx b/web/app/components/snippet-list/index.tsx
index e62216a7c4a..8437a02a923 100644
--- a/web/app/components/snippet-list/index.tsx
+++ b/web/app/components/snippet-list/index.tsx
@@ -1,5 +1,6 @@
'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'
@@ -18,6 +19,7 @@ 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'
@@ -27,6 +29,14 @@ const TagManagementModal = dynamic(() => import('@/features/tag-management/compo
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
}
@@ -59,16 +69,22 @@ const SnippetList = () => {
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(() => ({
- page: 1,
- limit: 30,
- keyword: debouncedKeywords,
- ...(tagIDs.length ? { tag_ids: tagIDs } : {}),
- ...(creatorIDs.length ? { creator_ids: creatorIDs } : {}),
- }), [creatorIDs, debouncedKeywords, tagIDs])
+ 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 {
@@ -144,6 +160,10 @@ const SnippetList = () => {
value={creatorIDs}
onChange={setCreatorIDs}
/>
+
setShowTagManagementModal(true)} />