mirror of
https://github.com/langgenius/dify.git
synced 2026-05-02 08:28:03 +08:00
feat(skill): guard template cards against duplicate skill addition
Add useExistingSkillNames hook that derives root folder names from the cached asset tree via TanStack Query select, then use it to show an "Added" state on hover for already-present skills and block re-upload.
This commit is contained in:
@ -35,3 +35,23 @@ export function useSkillAssetNodeMap() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the set of root-level folder names in the skill asset tree.
|
||||
* Useful for checking whether a skill template has already been added.
|
||||
*/
|
||||
export function useExistingSkillNames() {
|
||||
const appId = useSkillAppId()
|
||||
return useGetAppAssetTree(appId, {
|
||||
select: (data: AppAssetTreeResponse): Set<string> => {
|
||||
if (!data?.children)
|
||||
return new Set()
|
||||
const names = new Set<string>()
|
||||
for (const node of data.children) {
|
||||
if (node.node_type === 'folder')
|
||||
names.add(node.name)
|
||||
}
|
||||
return names
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useBatchUpload } from '@/service/use-app-asset'
|
||||
import { useExistingSkillNames } from '../hooks/use-skill-asset-tree'
|
||||
import { useSkillTreeUpdateEmitter } from '../hooks/use-skill-tree-collaboration'
|
||||
import CategoryTabs from './category-tabs'
|
||||
import SectionHeader from './section-header'
|
||||
@ -30,9 +31,13 @@ const SkillTemplatesSection = () => {
|
||||
const emitTreeUpdateRef = useRef(emitTreeUpdate)
|
||||
emitTreeUpdateRef.current = emitTreeUpdate
|
||||
|
||||
const { data: existingNames } = useExistingSkillNames()
|
||||
const existingNamesRef = useRef(existingNames)
|
||||
existingNamesRef.current = existingNames
|
||||
|
||||
const handleUse = useCallback(async (summary: SkillTemplateSummary) => {
|
||||
const entry = SKILL_TEMPLATES.find(e => e.id === summary.id)
|
||||
if (!entry || !appId)
|
||||
if (!entry || !appId || existingNamesRef.current?.has(summary.name))
|
||||
return
|
||||
|
||||
setLoadingId(summary.id)
|
||||
@ -94,6 +99,7 @@ const SkillTemplatesSection = () => {
|
||||
<TemplateCard
|
||||
key={entry.id}
|
||||
template={entry}
|
||||
added={existingNames?.has(entry.name) ?? false}
|
||||
disabled={loadingId !== null}
|
||||
loading={loadingId === entry.id}
|
||||
onUse={handleUse}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { SkillTemplateSummary } from './templates/types'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { RiAddLine, RiCheckLine } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
@ -10,12 +10,13 @@ import Button from '@/app/components/base/button'
|
||||
|
||||
type TemplateCardProps = {
|
||||
template: SkillTemplateSummary
|
||||
added?: boolean
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
onUse: (template: SkillTemplateSummary) => void
|
||||
}
|
||||
|
||||
const TemplateCard = ({ template, disabled, loading, onUse }: TemplateCardProps) => {
|
||||
const TemplateCard = ({ template, added, disabled, loading, onUse }: TemplateCardProps) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
|
||||
return (
|
||||
@ -51,17 +52,31 @@ const TemplateCard = ({ template, disabled, loading, onUse }: TemplateCardProps)
|
||||
)
|
||||
: <div className="h-[18px]" />}
|
||||
<div className="pointer-events-none absolute inset-0 flex items-end px-4 pb-4 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
className="w-full"
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
onClick={() => onUse(template)}
|
||||
>
|
||||
<RiAddLine className="mr-0.5 h-4 w-4" />
|
||||
{t('skill.startTab.useThisSkill')}
|
||||
</Button>
|
||||
{added
|
||||
? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="w-full"
|
||||
disabled
|
||||
>
|
||||
<RiCheckLine className="mr-0.5 h-4 w-4" />
|
||||
{t('skill.startTab.skillAdded')}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
className="w-full"
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
onClick={() => onUse(template)}
|
||||
>
|
||||
<RiAddLine className="mr-0.5 h-4 w-4" />
|
||||
{t('skill.startTab.useThisSkill')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user