refactor(web): simplify github install focus (#36314)

This commit is contained in:
yyh
2026-05-18 14:45:12 +08:00
committed by GitHub
parent 28818f2e2a
commit c407f40e0d
7 changed files with 81 additions and 430 deletions

View File

@@ -101,25 +101,6 @@ vi.mock('../../hooks/use-hide-logic', () => ({
}))
// Mock child components
vi.mock('../steps/setURL', () => ({
default: ({ repoUrl, onChange, onNext, onCancel }: {
repoUrl: string
onChange: (value: string) => void
onNext: () => void
onCancel: () => void
}) => (
<div data-testid="set-url-step">
<input
data-testid="repo-url-input"
value={repoUrl}
onChange={e => onChange(e.target.value)}
/>
<button data-testid="next-btn" onClick={onNext}>Next</button>
<button data-testid="cancel-btn" onClick={onCancel}>Cancel</button>
</div>
),
}))
vi.mock('../steps/selectPackage', () => ({
default: ({
repoUrl,
@@ -236,6 +217,10 @@ vi.mock('../../base/installed', () => ({
),
}))
const getRepoUrlInput = () => screen.getByLabelText('plugin.installFromGitHub.gitHubRepo')
const getNextButton = () => screen.getByRole('button', { name: 'plugin.installModal.next' })
const getCancelButton = () => screen.getByRole('button', { name: 'plugin.installModal.cancel' })
describe('InstallFromGitHub', () => {
const defaultProps = {
onClose: vi.fn(),
@@ -261,8 +246,8 @@ describe('InstallFromGitHub', () => {
it('should render modal with correct initial state for new installation', () => {
render(<InstallFromGitHub {...defaultProps} />)
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
expect(screen.getByTestId('repo-url-input')).toHaveValue('')
expect(getRepoUrlInput()).toBeInTheDocument()
expect(getRepoUrlInput()).toHaveValue('')
})
it('should render modal with selectPackage step when updatePayload is provided', () => {
@@ -312,7 +297,7 @@ describe('InstallFromGitHub', () => {
it('should update repoUrl when user types in input', () => {
render(<InstallFromGitHub {...defaultProps} />)
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/test/repo' } })
expect(input).toHaveValue('https://github.com/test/repo')
@@ -321,10 +306,10 @@ describe('InstallFromGitHub', () => {
it('should transition from setUrl to selectPackage on successful URL submit', async () => {
render(<InstallFromGitHub {...defaultProps} />)
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
const nextBtn = screen.getByTestId('next-btn')
const nextBtn = getNextButton()
fireEvent.click(nextBtn)
await waitFor(() => {
@@ -443,10 +428,10 @@ describe('InstallFromGitHub', () => {
it('should show error toast for invalid GitHub URL', async () => {
render(<InstallFromGitHub {...defaultProps} />)
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'invalid-url' } })
const nextBtn = screen.getByTestId('next-btn')
const nextBtn = getNextButton()
fireEvent.click(nextBtn)
await waitFor(() => {
@@ -462,10 +447,10 @@ describe('InstallFromGitHub', () => {
render(<InstallFromGitHub {...defaultProps} />)
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
const nextBtn = screen.getByTestId('next-btn')
const nextBtn = getNextButton()
fireEvent.click(nextBtn)
await waitFor(() => {
@@ -481,10 +466,10 @@ describe('InstallFromGitHub', () => {
render(<InstallFromGitHub {...defaultProps} />)
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
const nextBtn = screen.getByTestId('next-btn')
const nextBtn = getNextButton()
fireEvent.click(nextBtn)
await waitFor(() => {
@@ -504,9 +489,9 @@ describe('InstallFromGitHub', () => {
render(<InstallFromGitHub {...defaultProps} />)
// Navigate to selectPackage
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
fireEvent.click(screen.getByTestId('next-btn'))
fireEvent.click(getNextButton())
await waitFor(() => {
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
@@ -516,7 +501,7 @@ describe('InstallFromGitHub', () => {
fireEvent.click(screen.getByTestId('back-btn'))
await waitFor(() => {
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
expect(getRepoUrlInput()).toBeInTheDocument()
})
})
@@ -546,7 +531,7 @@ describe('InstallFromGitHub', () => {
it('should call onClose when cancel button is clicked', () => {
render(<InstallFromGitHub {...defaultProps} />)
fireEvent.click(screen.getByTestId('cancel-btn'))
fireEvent.click(getCancelButton())
expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
})
@@ -783,10 +768,10 @@ describe('InstallFromGitHub', () => {
it('should handle URL without trailing slash', async () => {
render(<InstallFromGitHub {...defaultProps} />)
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
fireEvent.click(screen.getByTestId('next-btn'))
fireEvent.click(getNextButton())
await waitFor(() => {
expect(mockFetchReleases).toHaveBeenCalledWith('owner', 'repo')
@@ -797,11 +782,11 @@ describe('InstallFromGitHub', () => {
render(<InstallFromGitHub {...defaultProps} />)
// Set URL
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/test/myrepo' } })
// Navigate to selectPackage
fireEvent.click(screen.getByTestId('next-btn'))
fireEvent.click(getNextButton())
await waitFor(() => {
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
@@ -980,12 +965,12 @@ describe('InstallFromGitHub', () => {
render(<InstallFromGitHub {...defaultProps} />)
// Start from setUrl step
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
expect(getRepoUrlInput()).toBeInTheDocument()
// Enter URL
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
fireEvent.click(screen.getByTestId('next-btn'))
fireEvent.click(getNextButton())
await waitFor(() => {
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
@@ -1086,7 +1071,7 @@ describe('InstallFromGitHub', () => {
render(<InstallFromGitHub {...defaultProps} />)
// Verify we're on setUrl step
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
expect(getRepoUrlInput()).toBeInTheDocument()
// The setUrl step doesn't expose onBack in the real component,
// but our mock doesn't have it either - this is correct behavior
@@ -1097,9 +1082,9 @@ describe('InstallFromGitHub', () => {
render(<InstallFromGitHub {...defaultProps} />)
// Navigate to selectPackage
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
fireEvent.click(screen.getByTestId('next-btn'))
fireEvent.click(getNextButton())
await waitFor(() => {
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
@@ -1123,11 +1108,11 @@ describe('InstallFromGitHub', () => {
fireEvent.click(screen.getByTestId('back-btn'))
await waitFor(() => {
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
expect(getRepoUrlInput()).toBeInTheDocument()
})
// Verify URL is preserved after back navigation
expect(screen.getByTestId('repo-url-input')).toHaveValue('https://github.com/owner/repo')
expect(getRepoUrlInput()).toHaveValue('https://github.com/owner/repo')
})
})
})
@@ -1368,114 +1353,6 @@ describe('Install Plugin Utils', () => {
// Steps Components Tests
// ================================
// SetURL Component Tests
describe('SetURL Component', () => {
// Import the real component for testing
const SetURL = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
// Re-mock the SetURL component with a more testable version
vi.doMock('./steps/setURL', () => ({
default: SetURL,
}))
})
describe('Rendering', () => {
it('should render label with correct text', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
// The mocked component should be rendered
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
})
it('should render input field with placeholder', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
const input = screen.getByTestId('repo-url-input')
expect(input).toBeInTheDocument()
})
it('should render cancel and next buttons', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
expect(screen.getByTestId('cancel-btn')).toBeInTheDocument()
expect(screen.getByTestId('next-btn')).toBeInTheDocument()
})
})
describe('Props', () => {
it('should display repoUrl value in input', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
const input = screen.getByTestId('repo-url-input')
fireEvent.change(input, { target: { value: 'https://github.com/test/repo' } })
expect(input).toHaveValue('https://github.com/test/repo')
})
it('should call onChange when input value changes', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
const input = screen.getByTestId('repo-url-input')
fireEvent.change(input, { target: { value: 'new-value' } })
expect(input).toHaveValue('new-value')
})
})
describe('User Interactions', () => {
it('should call onNext when next button is clicked', async () => {
mockFetchReleases.mockResolvedValue(createMockReleases())
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
const input = screen.getByTestId('repo-url-input')
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
fireEvent.click(screen.getByTestId('next-btn'))
await waitFor(() => {
expect(mockFetchReleases).toHaveBeenCalled()
})
})
it('should call onCancel when cancel button is clicked', () => {
const onClose = vi.fn()
render(<InstallFromGitHub onClose={onClose} onSuccess={vi.fn()} />)
fireEvent.click(screen.getByTestId('cancel-btn'))
expect(onClose).toHaveBeenCalledTimes(1)
})
})
describe('Edge Cases', () => {
it('should handle empty URL input', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
const input = screen.getByTestId('repo-url-input')
expect(input).toHaveValue('')
})
it('should handle URL with whitespace only', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
const input = screen.getByTestId('repo-url-input')
fireEvent.change(input, { target: { value: ' ' } })
// With whitespace only, next should still be submittable but validation will fail
fireEvent.click(screen.getByTestId('next-btn'))
// Should show error for invalid URL
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
message: 'plugin.error.inValidGitHubUrl',
})
})
})
})
// SelectPackage Component Tests
describe('SelectPackage Component', () => {
beforeEach(() => {
@@ -1513,9 +1390,9 @@ describe('SelectPackage Component', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
// Navigate to selectPackage step
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
fireEvent.click(screen.getByTestId('next-btn'))
fireEvent.click(getNextButton())
await waitFor(() => {
expect(screen.getByTestId('back-btn')).toBeInTheDocument()
@@ -1590,9 +1467,9 @@ describe('SelectPackage Component', () => {
render(<InstallFromGitHub onClose={vi.fn()} onSuccess={vi.fn()} />)
// Navigate to selectPackage
const input = screen.getByTestId('repo-url-input')
const input = getRepoUrlInput()
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
fireEvent.click(screen.getByTestId('next-btn'))
fireEvent.click(getNextButton())
await waitFor(() => {
expect(screen.getByTestId('select-package-step')).toBeInTheDocument()
@@ -1601,7 +1478,7 @@ describe('SelectPackage Component', () => {
fireEvent.click(screen.getByTestId('back-btn'))
await waitFor(() => {
expect(screen.getByTestId('set-url-step')).toBeInTheDocument()
expect(getRepoUrlInput()).toBeInTheDocument()
})
})

View File

@@ -2,6 +2,7 @@
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
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 { toast } from '@langgenius/dify-ui/toast'
@@ -17,7 +18,6 @@ import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
import Loaded from './steps/loaded'
import SelectPackage from './steps/selectPackage'
import SetURL from './steps/setURL'
const i18nPrefix = 'installFromGitHub'
@@ -194,12 +194,41 @@ 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 && (
<SetURL
repoUrl={state.repoUrl}
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
onNext={handleUrlSubmit}
onCancel={onClose}
/>
<>
<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"
/>
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
<Button
variant="secondary"
className="min-w-18"
onClick={onClose}
>
{t('installModal.cancel', { ns: 'plugin' })}
</Button>
<Button
variant="primary"
className="min-w-18"
onClick={handleUrlSubmit}
disabled={!state.repoUrl.trim()}
>
{t('installModal.next', { ns: 'plugin' })}
</Button>
</div>
</>
)}
{state.step === InstallStepFromGitHub.selectPackage && (
<SelectPackage
@@ -216,11 +245,11 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
onBack={handleBack}
/>
)}
{state.step === InstallStepFromGitHub.readyToInstall && (
{state.step === InstallStepFromGitHub.readyToInstall && manifest && uniqueIdentifier && (
<Loaded
updatePayload={updatePayload!}
uniqueIdentifier={uniqueIdentifier!}
payload={manifest as any}
uniqueIdentifier={uniqueIdentifier}
payload={manifest}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
selectedPackage={state.selectedPackage}

View File

@@ -1,189 +0,0 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SetURL from '../setURL'
describe('SetURL', () => {
const defaultProps = {
repoUrl: '',
onChange: vi.fn(),
onNext: vi.fn(),
onCancel: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
// ================================
// Rendering Tests
// ================================
describe('Rendering', () => {
it('should render label with GitHub repo text', () => {
render(<SetURL {...defaultProps} />)
expect(screen.getByText('plugin.installFromGitHub.gitHubRepo')).toBeInTheDocument()
})
it('should render input field with correct attributes', () => {
render(<SetURL {...defaultProps} />)
const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument()
expect(input).toHaveAttribute('type', 'url')
expect(input).toHaveAttribute('id', 'repoUrl')
expect(input).toHaveAttribute('name', 'repoUrl')
expect(input).toHaveAttribute('placeholder', 'Please enter GitHub repo URL')
})
it('should render cancel button', () => {
render(<SetURL {...defaultProps} />)
expect(screen.getByRole('button', { name: 'plugin.installModal.cancel' })).toBeInTheDocument()
})
it('should render next button', () => {
render(<SetURL {...defaultProps} />)
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeInTheDocument()
})
it('should associate label with input field', () => {
render(<SetURL {...defaultProps} />)
const input = screen.getByLabelText('plugin.installFromGitHub.gitHubRepo')
expect(input).toBeInTheDocument()
})
it('should auto-focus the input on mount', async () => {
render(<SetURL {...defaultProps} />)
const input = screen.getByRole('textbox')
await waitFor(() => {
expect(input).toHaveFocus()
})
})
})
// ================================
// Props Tests
// ================================
describe('Props', () => {
it('should display repoUrl value in input', () => {
render(<SetURL {...defaultProps} repoUrl="https://github.com/test/repo" />)
expect(screen.getByRole('textbox')).toHaveValue('https://github.com/test/repo')
})
it('should display empty string when repoUrl is empty', () => {
render(<SetURL {...defaultProps} repoUrl="" />)
expect(screen.getByRole('textbox')).toHaveValue('')
})
})
// ================================
// User Interactions Tests
// ================================
describe('User Interactions', () => {
it('should call onChange when input value changes', () => {
const onChange = vi.fn()
render(<SetURL {...defaultProps} onChange={onChange} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'https://github.com/owner/repo' } })
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenCalledWith('https://github.com/owner/repo')
})
it('should call onCancel when cancel button is clicked', () => {
const onCancel = vi.fn()
render(<SetURL {...defaultProps} onCancel={onCancel} />)
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.cancel' }))
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('should call onNext when next button is clicked', () => {
const onNext = vi.fn()
render(<SetURL {...defaultProps} repoUrl="https://github.com/test/repo" onNext={onNext} />)
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
expect(onNext).toHaveBeenCalledTimes(1)
})
})
// ================================
// Button State Tests
// ================================
describe('Button State', () => {
it('should disable next button when repoUrl is empty', () => {
render(<SetURL {...defaultProps} repoUrl="" />)
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
})
it('should disable next button when repoUrl is only whitespace', () => {
render(<SetURL {...defaultProps} repoUrl=" " />)
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).toBeDisabled()
})
it('should enable next button when repoUrl has content', () => {
render(<SetURL {...defaultProps} repoUrl="https://github.com/test/repo" />)
expect(screen.getByRole('button', { name: 'plugin.installModal.next' })).not.toBeDisabled()
})
it('should not disable cancel button regardless of repoUrl', () => {
render(<SetURL {...defaultProps} repoUrl="" />)
expect(screen.getByRole('button', { name: 'plugin.installModal.cancel' })).not.toBeDisabled()
})
})
// ================================
// Edge Cases Tests
// ================================
describe('Edge Cases', () => {
it('should handle URL with special characters', () => {
const onChange = vi.fn()
render(<SetURL {...defaultProps} onChange={onChange} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'https://github.com/test-org/repo_name-123' } })
expect(onChange).toHaveBeenCalledWith('https://github.com/test-org/repo_name-123')
})
it('should handle very long URLs', () => {
const longUrl = `https://github.com/${'a'.repeat(100)}/${'b'.repeat(100)}`
render(<SetURL {...defaultProps} repoUrl={longUrl} />)
expect(screen.getByRole('textbox')).toHaveValue(longUrl)
})
it('should handle onChange with empty string', () => {
const onChange = vi.fn()
render(<SetURL {...defaultProps} repoUrl="some-value" onChange={onChange} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '' } })
expect(onChange).toHaveBeenCalledWith('')
})
it('should preserve callback references on rerender', () => {
const onNext = vi.fn()
const { rerender } = render(<SetURL {...defaultProps} repoUrl="https://github.com/a/b" onNext={onNext} />)
rerender(<SetURL {...defaultProps} repoUrl="https://github.com/a/b" onNext={onNext} />)
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.next' }))
expect(onNext).toHaveBeenCalledTimes(1)
})
})
})

View File

@@ -1,69 +0,0 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
type SetURLProps = {
repoUrl: string
onChange: (value: string) => void
onNext: () => void
onCancel: () => void
}
const SetURL: React.FC<SetURLProps> = ({ repoUrl, onChange, onNext, onCancel }) => {
const { t } = useTranslation()
const inputRef = React.useRef<HTMLInputElement>(null)
// Focus the input after the dropdown's focus-return animation settles.
// Using rAF avoids racing the DropdownMenu FloatingFocusManager that returns
// focus to the trigger on close.
React.useEffect(() => {
const frame = requestAnimationFrame(() => {
inputRef.current?.focus()
})
return () => cancelAnimationFrame(frame)
}, [])
return (
<>
<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
ref={inputRef}
type="url"
id="repoUrl"
name="repoUrl"
value={repoUrl}
onChange={e => onChange(e.target.value)}
className="shadows-shadow-xs flex grow items-center gap-[2px] 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"
/>
<div className="mt-4 flex items-center justify-end gap-2 self-stretch">
<Button
variant="secondary"
className="min-w-[72px]"
onClick={onCancel}
>
{t('installModal.cancel', { ns: 'plugin' })}
</Button>
<Button
variant="primary"
className="min-w-[72px]"
onClick={onNext}
disabled={!repoUrl.trim()}
>
{t('installModal.next', { ns: 'plugin' })}
</Button>
</div>
</>
)
}
export default SetURL