feat(dify-ui): add shared form primitives (#36334)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh
2026-05-19 13:38:57 +08:00
committed by GitHub
parent 7f392b6950
commit 04d62867af
51 changed files with 1824 additions and 557 deletions

View File

@@ -317,6 +317,20 @@ describe('InstallFromGitHub', () => {
})
})
it('should submit the repo URL form from the set URL step', async () => {
render(<InstallFromGitHub {...defaultProps} />)
const input = getRepoUrlInput()
const form = input.closest('form')
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
expect(form).toBeInTheDocument()
expect(getNextButton()).toHaveAttribute('type', 'submit')
fireEvent.submit(form!)
expect(await screen.findByRole('button', { name: 'Select Version' })).toBeInTheDocument()
})
it('should update selectedVersion when version is selected', async () => {
render(<InstallFromGitHub {...defaultProps} updatePayload={createUpdatePayload()} />)

View File

@@ -5,6 +5,8 @@ import type { InstallState } from '@/app/components/plugins/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useState } from 'react'
@@ -194,23 +196,24 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
: (
<div className={`flex min-h-0 flex-1 flex-col items-start justify-center self-stretch overflow-y-auto px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
{state.step === InstallStepFromGitHub.setUrl && (
<>
<label
htmlFor="repoUrl"
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
>
<span className="system-sm-semibold">{t('installFromGitHub.gitHubRepo', { ns: 'plugin' })}</span>
</label>
<input
autoFocus
type="url"
id="repoUrl"
name="repoUrl"
value={state.repoUrl}
onChange={e => setState(prevState => ({ ...prevState, repoUrl: e.target.value }))}
className="flex grow items-center gap-0.5 self-stretch overflow-hidden rounded-lg border border-components-input-border-active bg-components-input-bg-active p-2 system-sm-regular text-ellipsis text-components-input-text-filled outline-hidden"
placeholder="Please enter GitHub repo URL"
/>
<Form
onFormSubmit={handleUrlSubmit}
className="flex flex-col items-start gap-4 self-stretch"
>
<FieldRoot name="repoUrl" className="gap-4 self-stretch">
<FieldLabel className="flex w-full flex-col items-start justify-center p-0 text-text-secondary">
<span className="system-sm-semibold">{t('installFromGitHub.gitHubRepo', { ns: 'plugin' })}</span>
</FieldLabel>
<FieldControl
autoFocus
type="text"
inputMode="url"
value={state.repoUrl}
onValueChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
className="flex grow items-center gap-0.5 self-stretch overflow-hidden rounded-lg border-components-input-border-active bg-components-input-bg-active p-2 text-ellipsis"
placeholder="Please enter GitHub repo URL"
/>
</FieldRoot>
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
<Button
variant="secondary"
@@ -221,14 +224,14 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
</Button>
<Button
variant="primary"
type="submit"
className="min-w-18"
onClick={handleUrlSubmit}
disabled={!state.repoUrl.trim()}
>
{t('installModal.next', { ns: 'plugin' })}
</Button>
</div>
</>
</Form>
)}
{state.step === InstallStepFromGitHub.selectPackage && (
<SelectPackage

View File

@@ -2,6 +2,7 @@
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
import { Button } from '@langgenius/dify-ui/button'
import { FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
@@ -77,79 +78,77 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
return (
<>
<label
htmlFor="version"
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
>
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectVersion`, { ns: 'plugin' })}</span>
</label>
<Select
value={selectedVersionOption ? String(selectedVersionOption.value) : null}
onValueChange={(value) => {
if (!value)
return
const selectedItem = versions.find(item => String(item.value) === value)
if (selectedItem)
onSelectVersion(selectedItem)
}}
>
<SelectTrigger className="h-9 text-components-input-text-filled">
<div className="flex items-center justify-between gap-2">
<span className="truncate">
{selectedVersionOption?.name ?? t(`${i18nPrefix}.selectVersionPlaceholder`, { ns: 'plugin' }) ?? ''}
</span>
{!!(updatePayload?.originalPackageInfo.version && selectedVersionOption && selectedVersionOption.value !== updatePayload.originalPackageInfo.version) && (
<Badge>
{updatePayload.originalPackageInfo.version}
{' '}
{'->'}
{' '}
{selectedVersionOption.value}
</Badge>
)}
</div>
</SelectTrigger>
<SelectContent popupClassName="w-[512px]">
{versions.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItemText>{item.name}</SelectItemText>
{item.value === updatePayload?.originalPackageInfo.version && (
<Badge uppercase={true} className="ml-1 shrink-0">INSTALLED</Badge>
<FieldRoot name="version" className="gap-4 self-stretch">
<FieldLabel className="flex w-full flex-col items-start justify-center p-0 text-text-secondary">
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectVersion`, { ns: 'plugin' })}</span>
</FieldLabel>
<Select
value={selectedVersionOption ? String(selectedVersionOption.value) : null}
onValueChange={(value) => {
if (!value)
return
const selectedItem = versions.find(item => String(item.value) === value)
if (selectedItem)
onSelectVersion(selectedItem)
}}
>
<SelectTrigger className="h-9 text-components-input-text-filled">
<div className="flex items-center justify-between gap-2">
<span className="truncate">
{selectedVersionOption?.name ?? t(`${i18nPrefix}.selectVersionPlaceholder`, { ns: 'plugin' }) ?? ''}
</span>
{!!(updatePayload?.originalPackageInfo.version && selectedVersionOption && selectedVersionOption.value !== updatePayload.originalPackageInfo.version) && (
<Badge>
{updatePayload.originalPackageInfo.version}
{' '}
{'->'}
{' '}
{selectedVersionOption.value}
</Badge>
)}
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
<label
htmlFor="package"
className="flex flex-col items-start justify-center self-stretch text-text-secondary"
>
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectPackage`, { ns: 'plugin' })}</span>
</label>
<Select
value={selectedPackageOption ? String(selectedPackageOption.value) : null}
readOnly={!selectedVersion}
onValueChange={(value) => {
if (!value)
return
const selectedItem = packages.find(item => String(item.value) === value)
if (selectedItem)
onSelectPackage(selectedItem)
}}
>
<SelectTrigger className="h-9 text-components-input-text-filled">
{selectedPackageOption?.name ?? t(`${i18nPrefix}.selectPackagePlaceholder`, { ns: 'plugin' }) ?? ''}
</SelectTrigger>
<SelectContent popupClassName="w-[512px]">
{packages.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</SelectTrigger>
<SelectContent popupClassName="w-[512px]">
{versions.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItemText>{item.name}</SelectItemText>
{item.value === updatePayload?.originalPackageInfo.version && (
<Badge uppercase={true} className="ml-1 shrink-0">INSTALLED</Badge>
)}
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</FieldRoot>
<FieldRoot name="package" className="gap-4 self-stretch">
<FieldLabel className="flex w-full flex-col items-start justify-center p-0 text-text-secondary">
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectPackage`, { ns: 'plugin' })}</span>
</FieldLabel>
<Select
value={selectedPackageOption ? String(selectedPackageOption.value) : null}
readOnly={!selectedVersion}
onValueChange={(value) => {
if (!value)
return
const selectedItem = packages.find(item => String(item.value) === value)
if (selectedItem)
onSelectPackage(selectedItem)
}}
>
<SelectTrigger className="h-9 text-components-input-text-filled">
{selectedPackageOption?.name ?? t(`${i18nPrefix}.selectPackagePlaceholder`, { ns: 'plugin' }) ?? ''}
</SelectTrigger>
<SelectContent popupClassName="w-[512px]">
{packages.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</FieldRoot>
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
{!isEdit
&& (