mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 13:01:16 +08:00
feat(web): add snippet publish status filter
This commit is contained in:
parent
6928c1b4ab
commit
52383ce627
@ -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()
|
||||
|
||||
|
||||
@ -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(<SnippetPublishStatusFilter value="all" onChange={vi.fn()} />)
|
||||
|
||||
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(<SnippetPublishStatusFilter value="all" onChange={onChange} />)
|
||||
|
||||
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(<SnippetPublishStatusFilter value="draft" onChange={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /snippet\.draft/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
chipClassName,
|
||||
isSelected
|
||||
? 'border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-state-base-hover'
|
||||
: 'border-transparent bg-components-input-bg-normal text-text-tertiary hover:bg-components-input-bg-hover',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<span className="px-1 text-text-tertiary">{triggerLabel}</span>
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-tertiary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-start" popupClassName="w-[220px]">
|
||||
<DropdownMenuRadioGroup value={value} onValueChange={nextValue => isSnippetPublishStatus(nextValue) && onChange(nextValue)}>
|
||||
{options.map(option => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value} closeOnClick>
|
||||
<span>{option.text}</span>
|
||||
<DropdownMenuRadioItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default SnippetPublishStatusFilter
|
||||
@ -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<HTMLDivElement>(null)
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const [showTagManagementModal, setShowTagManagementModal] = useState(false)
|
||||
const [publishStatus, setPublishStatus] = useState<SnippetPublishStatus>('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}
|
||||
/>
|
||||
<SnippetPublishStatusFilter
|
||||
value={publishStatus}
|
||||
onChange={setPublishStatus}
|
||||
/>
|
||||
<TagFilter type="snippet" value={tagIDs} onChange={setTagIDs} onOpenTagManagement={() => setShowTagManagementModal(true)} />
|
||||
<div className="relative w-50">
|
||||
<span aria-hidden className="pointer-events-none absolute top-1/2 left-2 i-ri-search-line size-4 -translate-y-1/2 text-components-input-text-placeholder" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user