Files
dify/web/features/deployments/create-guide/source-step.tsx
2026-05-27 22:18:28 +08:00

159 lines
5.3 KiB
TypeScript

'use client'
import type { App } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import { toAppMode } from '../app-mode'
import { StepShell } from './layout'
const sourceAppSkeletonKeys = ['first-source-app', 'second-source-app', 'third-source-app']
function sourceAppSearchText(app: App) {
return `${app.name} ${app.id} ${app.mode}`.toLowerCase()
}
function SourceAppSkeleton() {
return (
<div className="divide-y divide-divider-subtle">
{sourceAppSkeletonKeys.map(key => (
<SkeletonRow key={key} className="h-14 px-3 py-2">
<SkeletonRectangle className="my-0 size-7 animate-pulse rounded-lg" />
<div className="flex min-w-0 grow flex-col gap-1">
<SkeletonRectangle className="my-0 h-3.5 w-2/3 animate-pulse" />
<SkeletonRectangle className="my-0 h-2.5 w-1/3 animate-pulse" />
</div>
</SkeletonRow>
))}
</div>
)
}
function SourceAppOption({ app, selected, onSelect }: {
app: App
selected: boolean
onSelect: () => void
}) {
const { t } = useTranslation('deployments')
const mode = toAppMode(app.mode)
return (
<label
className={cn(
'group flex min-h-14 cursor-pointer items-center gap-3 border-b border-l-2 border-b-divider-subtle px-3 py-2 transition-colors first:rounded-t-lg last:rounded-b-lg last:border-b-0',
selected
? 'border-l-state-accent-solid bg-background-default hover:bg-state-base-hover'
: 'border-l-transparent bg-background-default hover:bg-state-base-hover',
)}
>
<AppIcon
className="shrink-0"
size="xs"
iconType={app.icon_type}
icon={app.icon}
background={app.icon_background}
imageUrl={app.icon_url}
/>
<span className="flex min-w-0 grow flex-col gap-0.5">
<span className="truncate system-sm-medium text-text-primary">{app.name}</span>
<span className={cn('truncate system-xs-regular', selected ? 'text-text-secondary' : 'text-text-tertiary')}>{t(`appMode.${mode}`)}</span>
</span>
<input
type="radio"
name="source-app"
checked={selected}
onChange={onSelect}
className="sr-only"
/>
<span
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded-full',
selected ? 'bg-state-accent-active text-text-accent' : 'text-transparent',
)}
aria-hidden="true"
>
<span className="i-ri-check-line size-4" />
</span>
</label>
)
}
export function SourceStep({
apps,
selectedApp,
searchText,
isLoading,
onSearchTextChange,
onSelectApp,
}: {
apps: App[]
selectedApp?: App
searchText: string
isLoading: boolean
onSearchTextChange: (value: string) => void
onSelectApp: (app: App) => void
}) {
const { t } = useTranslation('deployments')
const effectiveSelectedAppId = selectedApp?.id ?? apps[0]?.id
const filteredApps = searchText.trim()
? apps.filter(app => sourceAppSearchText(app).includes(searchText.trim().toLowerCase()))
: apps
return (
<StepShell
title={t('createGuide.source.title')}
description={t('createGuide.source.description')}
descriptionClassName="lg:hidden"
hideHeader
>
<div className="flex flex-col gap-3">
<div className="relative">
<span className="pointer-events-none absolute top-1/2 left-2.5 i-ri-search-line size-4 -translate-y-1/2 text-text-tertiary" aria-hidden="true" />
<Input
id="create-guide-source-search"
aria-label={t('createGuide.source.sourceApp')}
value={searchText}
onChange={event => onSearchTextChange(event.target.value)}
placeholder={t('createGuide.source.searchPlaceholder')}
className="h-9 pr-8 pl-8"
/>
{searchText && (
<button
type="button"
aria-label={t('createGuide.source.clearSearch')}
onClick={() => onSearchTextChange('')}
className="absolute top-1/2 right-2.5 flex size-4 -translate-y-1/2 items-center justify-center text-text-quaternary hover:text-text-secondary"
>
<span className="i-ri-close-circle-fill size-4" aria-hidden="true" />
</button>
)}
</div>
<div className="max-h-64 overflow-y-auto rounded-lg border border-divider-subtle bg-background-default">
{isLoading
? <SourceAppSkeleton />
: filteredApps.length === 0
? (
<div className="px-4 py-10 text-center system-sm-regular text-text-tertiary">
{t('createGuide.source.empty')}
</div>
)
: (
<div>
{filteredApps.map(app => (
<SourceAppOption
key={app.id}
app={app}
selected={effectiveSelectedAppId === app.id}
onSelect={() => onSelectApp(app)}
/>
))}
</div>
)}
</div>
</div>
</StepShell>
)
}