feat: snippet siderbar update (#38371)

This commit is contained in:
KVOJJJin 2026-07-03 15:18:13 +08:00 committed by GitHub
parent 5622e8f7ea
commit 7b7956f3e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 110 additions and 40 deletions

View File

@ -77,8 +77,9 @@ export function AppListHeaderFilters({
<div className="ml-auto flex max-w-full min-w-0 flex-wrap items-center justify-end gap-2">
<Link
href="/snippets"
className="flex h-8 items-center rounded-lg px-3 text-sm font-semibold whitespace-nowrap text-text-secondary outline-hidden hover:bg-state-base-hover hover:text-text-primary focus-visible:ring-2 focus-visible:ring-state-accent-solid"
className="inline-flex h-8 cursor-pointer items-center justify-center gap-1 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 text-[13px] leading-4 font-medium whitespace-nowrap text-components-button-secondary-text shadow-xs outline-hidden backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
<span aria-hidden className="i-ri-braces-line size-4 shrink-0" />
{t('studio.viewSnippets', { ns: 'app' })}
</Link>
{showCreateButton && (

View File

@ -0,0 +1,11 @@
import { render, screen } from '@testing-library/react'
import { SnippetCollapsedPreview } from '../snippet-collapsed-preview'
describe('SnippetCollapsedPreview', () => {
it('should render collapsed route navigation and input field count', () => {
render(<SnippetCollapsedPreview inputFieldCount={2} snippetId="snippet-1" />)
expect(screen.getByRole('link', { name: 'snippet.sectionOrchestrate' })).toHaveAttribute('href', '/snippets/snippet-1/orchestrate')
expect(screen.getByLabelText('2 snippet.inputVariables')).toHaveTextContent('2')
})
})

View File

@ -152,6 +152,19 @@ describe('SnippetSidebarContent', () => {
expect(capturedVarListProps?.readonly).toBe(true)
})
it('should render the orchestrate route menu item', () => {
render(
<SnippetSidebarContent
snippet={snippet}
fields={fields}
readonly={false}
onFieldsChange={vi.fn()}
/>,
)
expect(screen.getByRole('link', { name: 'snippet.sectionOrchestrate' })).toHaveAttribute('href', '/snippets/snippet-1/orchestrate')
})
it('should add a new input field from the config variable modal', () => {
const onFieldsChange = vi.fn()
render(

View File

@ -1,27 +1,62 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import NavLink from '@/app/components/app-sidebar/nav-link'
import { SnippetPlaceholderIcon } from './snippet-placeholder-icon'
const NodeTreeIcon = ({ className }: { className?: string }) => (
<span className={cn('i-ri-node-tree', className)} />
)
export function SnippetCollapsedPreview({
inputFieldCount,
snippetId,
}: {
inputFieldCount: number
snippetId?: string
}) {
const { t } = useTranslation()
const sectionLabel = t('sectionOrchestrate', { ns: 'snippet' })
return (
<div
className="flex min-h-0 grow flex-col items-center px-2 pt-4"
className="flex min-h-0 grow flex-col items-center px-2"
aria-label="Snippet collapsed preview"
>
<SnippetPlaceholderIcon />
<div className="mb-3.5 flex w-full shrink-0 justify-center px-3.5 pt-0.5 pb-0.75">
<div className="my-0 h-px w-6.75 shrink-0 bg-divider-subtle" data-testid="divider"></div>
</div>
<SnippetPlaceholderIcon className="size-9" />
<div className="my-4 h-px w-8 rounded-full bg-divider-subtle" aria-hidden="true" />
{snippetId
? (
<NavLink
mode="collapse"
name={sectionLabel}
href={`/snippets/${snippetId}/orchestrate`}
active
iconMap={{ selected: NodeTreeIcon, normal: NodeTreeIcon }}
/>
)
: (
<div
aria-label={sectionLabel}
className="flex size-8 items-center justify-center rounded-lg border-t-[0.75px] border-r-[0.25px] border-b-[0.25px] border-l-[0.75px] border-effects-highlight-lightmode-off bg-components-menu-item-bg-active p-1.5 text-text-accent-light-mode-only"
>
<div className="flex size-5 items-center justify-center">
<NodeTreeIcon className="size-4.5 shrink-0" />
</div>
</div>
)}
<div
className="relative flex size-8 items-center justify-center rounded-lg border border-divider-subtle bg-background-default-subtle text-text-accent shadow-xs"
aria-label={`${inputFieldCount} input fields`}
className={cn(
'mt-4 flex min-w-6 items-center justify-center rounded-full bg-background-default-subtle px-2 text-2xs leading-4 font-normal text-text-tertiary',
inputFieldCount > 99 ? 'h-5' : 'size-5',
)}
aria-label={`${inputFieldCount} ${t('inputVariables', { ns: 'snippet' })}`}
>
<span aria-hidden="true" className="i-custom-vender-solid-development-variable-02 size-5" />
<span className="absolute -right-1.5 -bottom-1.5 flex size-4 items-center justify-center rounded-full border-2 border-components-panel-bg bg-state-accent-solid text-2xs leading-none text-text-primary-on-surface shadow-xs">
{inputFieldCount}
</span>
{inputFieldCount}
</div>
</div>
)

View File

@ -19,7 +19,7 @@ export function SnippetDetailSection({ expand }: SnippetDetailSectionProps) {
const snippetInputFields = useSnippetDraftStore(state => state.inputFields)
if (!expand)
return <SnippetCollapsedPreview inputFieldCount={snippetInputFields.length} />
return <SnippetCollapsedPreview inputFieldCount={snippetInputFields.length} snippetId={snippetNavigation.snippet?.id} />
if (!snippetNavigation.snippet || !snippetNavigation.onFieldsChange)
return null

View File

@ -14,17 +14,12 @@ export function SnippetPlaceholderIcon({
return (
<div
className={cn(
'flex size-10 items-center justify-center rounded-[10px] border border-divider-subtle bg-background-default-subtle text-text-tertiary shadow-xs',
'flex size-10 shrink-0 items-center justify-center rounded-[10px] border border-divider-subtle bg-state-accent-hover text-text-accent shadow-xs',
className,
)}
aria-hidden="true"
>
<span className={cn('relative block size-8', graphicClassName)}>
<span className="absolute top-1/2 left-1/2 h-4 w-0.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-util-colors-blue-blue-500" />
<span className="absolute top-0.5 left-0.5 size-2.5 rounded-xs bg-util-colors-blue-blue-300 shadow-xs" />
<span className="absolute top-1/2 right-0.5 size-2.5 -translate-y-1/2 rounded-xs bg-util-colors-blue-blue-600 shadow-xs" />
<span className="absolute bottom-0.5 left-0.5 size-2.5 rounded-xs bg-util-colors-indigo-indigo-400 shadow-xs" />
</span>
<span className={cn('i-ri-braces-line size-6', graphicClassName)} />
</div>
)
}

View File

@ -7,6 +7,7 @@ import { toast } from '@langgenius/dify-ui/toast'
import { isEqual } from 'es-toolkit/predicate'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NavLink from '@/app/components/app-sidebar/nav-link'
import SnippetInfoDropdown from '@/app/components/app-sidebar/snippet-info/dropdown'
import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal'
import Field from '@/app/components/workflow/nodes/_base/components/field'
@ -36,6 +37,10 @@ const toSnippetInputField = (field: InputVar): SnippetInputField => ({
type: field.type as unknown as SnippetInputField['type'],
})
const NodeTreeIcon = ({ className }: { className?: string }) => (
<span className={cn('i-ri-node-tree', className)} />
)
export const SnippetSidebarContent = ({
snippet,
fields,
@ -109,6 +114,16 @@ export const SnippetSidebarContent = ({
)}
</div>
<nav className="shrink-0 px-3 pt-4">
<NavLink
mode="expand"
name={t('sectionOrchestrate', { ns: 'snippet' })}
href={`/snippets/${snippet.id}/orchestrate`}
active
iconMap={{ selected: NodeTreeIcon, normal: NodeTreeIcon }}
/>
</nav>
<div className="flex min-h-0 grow flex-col px-3 pt-6">
<Field
title={t('inputVariables', { ns: 'snippet' })}

View File

@ -224,7 +224,7 @@
"studio.starFailed": "فشل تحديث النجمة",
"studio.starred": "المميزة بنجمة",
"studio.unstarApp": "إزالة النجمة من التطبيق",
"studio.viewSnippets": "عرض المقتطفات",
"studio.viewSnippets": "المقتطفات",
"switch": "التبديل إلى Workflow Orchestrate",
"switchLabel": "نسخة التطبيق التي سيتم إنشاؤها",
"switchStart": "بدء التبديل",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Stern konnte nicht aktualisiert werden",
"studio.starred": "Markiert",
"studio.unstarApp": "Markierung der App entfernen",
"studio.viewSnippets": "Snippets anzeigen",
"studio.viewSnippets": "Snippets",
"switch": "Zu Workflow-Orchestrierung wechseln",
"switchLabel": "Die zu erstellende App-Kopie",
"switchStart": "Wechsel starten",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Failed to update star",
"studio.starred": "Starred",
"studio.unstarApp": "Unstar app",
"studio.viewSnippets": "View Snippets",
"studio.viewSnippets": "Snippets",
"switch": "Switch to Workflow Orchestrate",
"switchLabel": "The app copy to be created",
"switchStart": "Start switch",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "No se pudo actualizar la estrella",
"studio.starred": "Destacadas",
"studio.unstarApp": "Quitar estrella de la aplicación",
"studio.viewSnippets": "Ver fragmentos",
"studio.viewSnippets": "Fragmentos",
"switch": "Cambiar a Orquestación de Flujo de Trabajo",
"switchLabel": "La copia de la app a crear",
"switchStart": "Iniciar cambio",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "به‌روزرسانی ستاره ناموفق بود",
"studio.starred": "ستاره‌دار",
"studio.unstarApp": "برداشتن ستاره برنامه",
"studio.viewSnippets": "مشاهده قطعه‌ها",
"studio.viewSnippets": "قطعه‌ها",
"switch": "تغییر به سازماندهی گردش کار",
"switchLabel": "نسخه برنامه که ایجاد می‌شود",
"switchStart": "شروع تغییر",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Échec de la mise à jour du favori",
"studio.starred": "Favoris",
"studio.unstarApp": "Retirer des favoris",
"studio.viewSnippets": "Voir les extraits",
"studio.viewSnippets": "Extraits",
"switch": "Passer à l'orchestration de flux de travail",
"switchLabel": "La copie de l'application à créer",
"switchStart": "Commencer la commutation",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "स्टार अपडेट करने में विफल",
"studio.starred": "स्टार किए गए",
"studio.unstarApp": "ऐप से स्टार हटाएं",
"studio.viewSnippets": "स्निपेट देखें",
"studio.viewSnippets": "स्निपेट",
"switch": "वर्कफ़्लो ऑर्केस्ट्रेट पर स्विच करें",
"switchLabel": "बनाई जाने वाली ऐप कॉपी",
"switchStart": "स्विच शुरू करें",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Gagal memperbarui bintang",
"studio.starred": "Berbintang",
"studio.unstarApp": "Hapus bintang aplikasi",
"studio.viewSnippets": "Lihat snippet",
"studio.viewSnippets": "Snippet",
"switch": "Beralih ke Workflow Orchestrate",
"switchLabel": "Salinan aplikasi yang akan dibuat",
"switchStart": "Sakelar mulai",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Impossibile aggiornare la stella",
"studio.starred": "Preferite",
"studio.unstarApp": "Rimuovi app dai preferiti",
"studio.viewSnippets": "Visualizza snippet",
"studio.viewSnippets": "Snippet",
"switch": "Passa a Orchestrazione del flusso di lavoro",
"switchLabel": "La copia dell'app da creare",
"switchStart": "Inizia il passaggio",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "スターの更新に失敗しました",
"studio.starred": "スター付き",
"studio.unstarApp": "アプリのスターを外す",
"studio.viewSnippets": "スニペットを表示",
"studio.viewSnippets": "スニペット",
"switch": "ワークフロー オーケストレートに切り替える",
"switchLabel": "作成されるアプリのコピー",
"switchStart": "切り替えを開始する",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "별표 업데이트 실패",
"studio.starred": "별표 표시됨",
"studio.unstarApp": "앱 별표 해제",
"studio.viewSnippets": "스니펫 보기",
"studio.viewSnippets": "스니펫",
"switch": "워크플로우 오케스트레이션으로 전환하기",
"switchLabel": "생성될 앱의 복사본",
"switchStart": "전환 시작하기",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Ster kon niet worden bijgewerkt",
"studio.starred": "Gemarkeerd",
"studio.unstarApp": "Markering van app verwijderen",
"studio.viewSnippets": "Snippets bekijken",
"studio.viewSnippets": "Snippets",
"switch": "Switch to Workflow Orchestrate",
"switchLabel": "The app copy to be created",
"switchStart": "Start switch",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Nie udało się zaktualizować gwiazdki",
"studio.starred": "Oznaczone gwiazdką",
"studio.unstarApp": "Usuń gwiazdkę aplikacji",
"studio.viewSnippets": "Wyświetl fragmenty",
"studio.viewSnippets": "Fragmenty",
"switch": "Przełącz na Orkiestrację Przepływu Pracy",
"switchLabel": "Kopia aplikacji do utworzenia",
"switchStart": "Rozpocznij przełączanie",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Falha ao atualizar favorito",
"studio.starred": "Favoritos",
"studio.unstarApp": "Remover app dos favoritos",
"studio.viewSnippets": "Ver snippets",
"studio.viewSnippets": "Snippets",
"switch": "Mudar para Orquestração de Fluxo de Trabalho",
"switchLabel": "A cópia do aplicativo a ser criada",
"switchStart": "Iniciar mudança",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Actualizarea stelei a eșuat",
"studio.starred": "Marcate cu stea",
"studio.unstarApp": "Elimină steaua aplicației",
"studio.viewSnippets": "Vezi fragmentele",
"studio.viewSnippets": "Fragmente",
"switch": "Comută la Orchestrare Flux de Lucru",
"switchLabel": "Copia aplicației care urmează să fie creată",
"switchStart": "Începe comutarea",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Не удалось обновить отметку",
"studio.starred": "Избранные",
"studio.unstarApp": "Удалить приложение из избранного",
"studio.viewSnippets": "Посмотреть фрагменты",
"studio.viewSnippets": "Фрагменты",
"switch": "Переключиться на Workflow",
"switchLabel": "Копия приложения, которая будет создана",
"switchStart": "Переключиться",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Posodobitev zvezdice ni uspela",
"studio.starred": "Označeno z zvezdico",
"studio.unstarApp": "Odstrani zvezdico aplikacije",
"studio.viewSnippets": "Prikaži izrezke",
"studio.viewSnippets": "Izrezki",
"switch": "Preklopi na Workflow Orchestrate",
"switchLabel": "Kopija aplikacije, ki bo ustvarjena",
"switchStart": "Začni preklop",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "อัปเดตดาวไม่สำเร็จ",
"studio.starred": "ติดดาวแล้ว",
"studio.unstarApp": "เลิกติดดาวแอป",
"studio.viewSnippets": "ดูสนิปเปต",
"studio.viewSnippets": "สนิปเปต",
"switch": "เปลี่ยนไปใช้ Workflow Orchestrate",
"switchLabel": "สําเนาโปรเจกต์ที่จะสร้าง",
"switchStart": "สวิตช์สตาร์ท",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Yıldız güncellenemedi",
"studio.starred": "Yıldızlı",
"studio.unstarApp": "Uygulamanın yıldızını kaldır",
"studio.viewSnippets": "Snippet'leri görüntüle",
"studio.viewSnippets": "Snippet'ler",
"switch": "İş Akışı Orkestrasyonuna Geç",
"switchLabel": "Oluşturulacak uygulama kopyası",
"switchStart": "Geçişi Başlat",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Не вдалося оновити зірку",
"studio.starred": "Позначені зіркою",
"studio.unstarApp": "Зняти зірку із застосунку",
"studio.viewSnippets": "Переглянути фрагменти",
"studio.viewSnippets": "Фрагменти",
"switch": "Перейти до оркестрації робочого процесу",
"switchLabel": "Копія додатка, яка буде створена",
"switchStart": "Почати перемикання",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "Không thể cập nhật sao",
"studio.starred": "Đã gắn sao",
"studio.unstarApp": "Bỏ gắn sao ứng dụng",
"studio.viewSnippets": "Xem snippets",
"studio.viewSnippets": "Snippets",
"switch": "Chuyển sang quản lý quy trình",
"switchLabel": "Bản sao ứng dụng sẽ được tạo",
"switchStart": "Bắt đầu chuyển",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "更新星标失败",
"studio.starred": "星标",
"studio.unstarApp": "取消星标应用",
"studio.viewSnippets": "查看 Snippets",
"studio.viewSnippets": "Snippets",
"switch": "迁移为工作流编排",
"switchLabel": "新应用创建为",
"switchStart": "开始迁移",

View File

@ -224,7 +224,7 @@
"studio.starFailed": "更新星標失敗",
"studio.starred": "星標",
"studio.unstarApp": "取消星標應用",
"studio.viewSnippets": "查看片段",
"studio.viewSnippets": "片段",
"switch": "遷移為工作流編排",
"switchLabel": "新應用建立為",
"switchStart": "開始遷移",