mirror of
https://github.com/langgenius/dify.git
synced 2026-02-22 19:15:47 +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>
|
||||
|
||||
@ -1078,6 +1078,7 @@
|
||||
"skill.startTab.importSkill": "Import Skill",
|
||||
"skill.startTab.importSkillDesc": "Import skill from skill.zip file",
|
||||
"skill.startTab.searchPlaceholder": "Search…",
|
||||
"skill.startTab.skillAdded": "Added",
|
||||
"skill.startTab.templatesComingSoon": "Templates coming soon…",
|
||||
"skill.startTab.templatesDesc": "Choose a template to bootstrap your agent's capabilities",
|
||||
"skill.startTab.templatesTitle": "Skill Templates",
|
||||
|
||||
@ -1070,6 +1070,7 @@
|
||||
"skill.startTab.importSkill": "导入 Skill",
|
||||
"skill.startTab.importSkillDesc": "从 skill.zip 文件导入",
|
||||
"skill.startTab.searchPlaceholder": "搜索…",
|
||||
"skill.startTab.skillAdded": "已添加",
|
||||
"skill.startTab.templatesComingSoon": "模板即将推出…",
|
||||
"skill.startTab.templatesDesc": "选择模板来快速构建你的 Agent 能力",
|
||||
"skill.startTab.templatesTitle": "Skill 模板",
|
||||
|
||||
Reference in New Issue
Block a user