From e61d4ff85d4d2bb4aca332b96fb647ea5b891710 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 29 Jul 2026 17:15:51 +0800 Subject: [PATCH] fix(web): align agent skills interactions with design --- .../vender/agent-v2/building-blocks.svg | 3 + .../custom-vender/icons.json | 3 + .../custom-vender/info.json | 6 +- .../skills/__tests__/index.spec.tsx | 109 ++++++++++-- .../components/orchestrate/skills/index.tsx | 158 +++++++++++------- .../components/orchestrate/skills/item.tsx | 108 ++++++++---- 6 files changed, 275 insertions(+), 112 deletions(-) create mode 100644 packages/iconify-collections/assets/vender/agent-v2/building-blocks.svg diff --git a/packages/iconify-collections/assets/vender/agent-v2/building-blocks.svg b/packages/iconify-collections/assets/vender/agent-v2/building-blocks.svg new file mode 100644 index 00000000000..52c8c1904f6 --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/building-blocks.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json index b100ce8257a..1021d42aca7 100644 --- a/packages/iconify-collections/custom-vender/icons.json +++ b/packages/iconify-collections/custom-vender/icons.json @@ -7,6 +7,9 @@ "width": 15, "height": 15 }, + "agent-v2-building-blocks": { + "body": "" + }, "agent-v2-configure": { "body": "", "width": 14 diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json index fdc5ad8ce64..d1840a897aa 100644 --- a/packages/iconify-collections/custom-vender/info.json +++ b/packages/iconify-collections/custom-vender/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-vender", "name": "Dify Custom Vender", - "total": 331, + "total": 332, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", @@ -14,11 +14,11 @@ }, "samples": [ "agent-v2-access-point", + "agent-v2-building-blocks", "agent-v2-configure", "agent-v2-configure-active", "agent-v2-configure-build", - "agent-v2-configure-preview", - "agent-v2-end-user-auth" + "agent-v2-configure-preview" ], "palette": false } diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index 9085e8ffba5..d2a18ccc1f4 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -456,15 +456,49 @@ describe('AgentSkills', () => { await user.click(missingSkill) expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + + await user.click( + screen.getByRole('button', { + name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Missing Skill"}', + }), + ) + expect(screen.queryByText('common.operation.download')).not.toBeInTheDocument() + expect(screen.getByText('common.operation.delete')).toBeInTheDocument() }) it('should delete a configured skill by config name', async () => { + const user = userEvent.setup() const { container } = renderAgentSkills() - const removeButton = container.querySelector('[data-agent-skill-remove-button]') - expect(removeButton).not.toBeNull() + const embeddedBadge = screen.getByText( + 'agentV2.agentDetail.configure.skills.addMenu.upload.badge', + ) + expect(embeddedBadge).toBeInTheDocument() + expect( + screen.queryByText('agentV2.agentDetail.configure.skills.itemType'), + ).not.toBeInTheDocument() + expect( + screen + .getByRole('button', { name: 'Tender Analyzer' }) + .querySelector('.i-custom-vender-agent-v2-building-blocks'), + ).toHaveClass('text-text-secondary') - fireEvent.click(removeButton!) + await user.click( + screen.getByRole('button', { + name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Tender Analyzer"}', + }), + ) + expect(embeddedBadge).toHaveClass('opacity-0') + + const deleteAction = screen.getByText('common.operation.delete') + fireEvent.mouseEnter(deleteAction.closest('[data-agent-skill-remove-button]')!) + + expect(container.querySelector('[data-agent-skill-row]')).toHaveClass( + 'border-state-destructive-border!', + 'bg-state-destructive-hover!', + ) + + await user.click(deleteAction) await waitFor(() => { expect(mocks.deleteSkillMutationFn).toHaveBeenCalled() @@ -574,12 +608,20 @@ describe('AgentSkills', () => { await user.click( screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }), ) - await user.click( - screen.getByRole('button', { - name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i, - }), - ) - await user.click(await screen.findByRole('button', { name: /Refund approval/ })) + const workspaceMenuItem = screen.getByRole('button', { + name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i, + }) + expect( + workspaceMenuItem.querySelector('.i-custom-vender-agent-v2-building-blocks'), + ).toHaveClass('text-text-secondary') + await user.click(workspaceMenuItem) + + const workspaceSkillButton = await screen.findByRole('button', { name: /Refund approval/ }) + expect( + workspaceSkillButton.querySelector('.i-custom-vender-agent-v2-building-blocks'), + ).toHaveClass('text-text-secondary') + expect(screen.queryByText('💳')).not.toBeInTheDocument() + await user.click(workspaceSkillButton) await waitFor(() => { expect(mocks.replaceAgentSkillBindingsMutationFn.mock.calls[0]?.[0]).toEqual({ @@ -706,6 +748,7 @@ describe('AgentSkills', () => { data: [ createWorkspaceSkill(), createWorkspaceSkill({ + description: 'Draft skill description.', id: 'draft-skill', name: 'draft-skill', display_name: 'Draft skill', @@ -745,8 +788,16 @@ describe('AgentSkills', () => { const draftSkillButton = screen .getByText('agentV2.agentDetail.configure.skills.workspaceSelector.draft') .closest('button') - expect(addedSkillButton).toBeDisabled() - expect(draftSkillButton).toBeDisabled() + expect(addedSkillButton).not.toBeDisabled() + expect(addedSkillButton).toHaveAttribute('aria-disabled', 'true') + expect(draftSkillButton).not.toBeDisabled() + expect(draftSkillButton).toHaveAttribute('aria-disabled', 'true') + + await user.hover(draftSkillButton!) + expect(await screen.findByText('Draft skill description.')).toBeInTheDocument() + + await user.click(draftSkillButton!) + await user.click(addedSkillButton!) expect(mocks.replaceAgentSkillBindingsMutationFn).not.toHaveBeenCalled() }) @@ -832,14 +883,26 @@ describe('AgentSkills', () => { }), } }) - renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState }) + const { container } = renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState }) await user.click( await screen.findByRole('button', { name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Refund approval"}', }), ) - await user.click(await screen.findByText('agentV2.agentDetail.configure.skills.removeAction')) + expect(screen.getByText('refund-approval')).toHaveClass('opacity-0') + + const removeAction = await screen.findByText( + 'agentV2.agentDetail.configure.skills.removeAction', + ) + fireEvent.mouseEnter(removeAction.closest('[data-workspace-skill-remove-action]')!) + + expect(container.querySelector('[data-workspace-skill-row]')).toHaveClass( + 'border-state-destructive-border!', + 'bg-state-destructive-hover!', + ) + + await user.click(removeAction) await waitFor(() => { expect(mocks.replaceAgentSkillBindingsMutationFn.mock.calls[0]?.[0]).toEqual({ @@ -1072,9 +1135,10 @@ describe('AgentSkills', () => { await user.click( screen.getByRole('button', { - name: /common\.operation\.download.*Tender Analyzer/, + name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Tender Analyzer"}', }), ) + await user.click(screen.getByText('common.operation.download')) await waitFor(() => { expect(mocks.skillDownloadQueryOptions).toHaveBeenCalledWith( @@ -1098,6 +1162,20 @@ describe('AgentSkills', () => { }) }) + it('should expose only download from an embedded skill row in read-only mode', async () => { + const user = userEvent.setup() + renderAgentSkills({ readOnly: true }) + + await user.click( + screen.getByRole('button', { + name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Tender Analyzer"}', + }), + ) + + expect(screen.getByText('common.operation.download')).toBeInTheDocument() + expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument() + }) + it('should download a whole workflow skill package with node_id', async () => { const user = userEvent.setup() renderAgentSkills({ @@ -1114,9 +1192,10 @@ describe('AgentSkills', () => { await user.click( screen.getByRole('button', { - name: /common\.operation\.download.*Tender Analyzer/, + name: 'agentV2.agentDetail.configure.skills.moreActions:{"name":"Tender Analyzer"}', }), ) + await user.click(screen.getByText('common.operation.download')) await waitFor(() => { expect(mocks.skillDownloadQueryOptions).toHaveBeenCalledWith( diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx index 9b1bc9455d9..78f90acc796 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx @@ -13,6 +13,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' @@ -75,7 +76,7 @@ function AgentSkillAddMenuItem({ > @@ -92,20 +93,19 @@ function AgentSkillAddMenuItem({ ) } -function WorkspaceSkillIcon({ icon }: { icon?: string }) { +function WorkspaceSkillIcon() { return ( - - {icon ? ( - {icon} - ) : ( - - )} + + ) } function WorkspaceSkillRow({ - disabled, + unavailable, isAdded, isPending, onSelect, @@ -113,7 +113,7 @@ function WorkspaceSkillRow({ selected, skill, }: { - disabled: boolean + unavailable: boolean isAdded: boolean isPending: boolean onSelect: (skill: SkillResponse) => void @@ -122,30 +122,34 @@ function WorkspaceSkillRow({ skill: SkillResponse }) { const { t } = useTranslation('agentV2') + const cannotAdd = unavailable || isAdded || isPending return ( @@ -115,32 +134,57 @@ export function AgentSkillItem({ label={t(($) => $['agentDetail.configure.skills.missing'])} /> )} - {!skill.isMissing && ( - - )} - {!readOnly && ( - + $['agentDetail.configure.skills.moreActions'], { + name: skill.name, + })} + className={cn( + 'pointer-events-none absolute top-1/2 right-1 z-10 flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-text-tertiary opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:pointer-events-auto data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary data-popup-open:opacity-100', + isRemoveHighlighted && 'text-text-destructive!', + )} + onClick={(event) => event.stopPropagation()} + > + + + + {!skill.isMissing && ( + + + {tCommon(($) => $['operation.download'])} + + )} + {!skill.isMissing && !readOnly && } + {!readOnly && ( + setIsRemoveHighlighted(true)} + onBlur={() => setIsRemoveHighlighted(false)} + onMouseEnter={() => setIsRemoveHighlighted(true)} + onMouseLeave={() => setIsRemoveHighlighted(false)} + > + + {tCommon(($) => $['operation.delete'])} + + )} + + )} {isPreviewOpen && }