mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
fix(web): preserve link semantics in button-styled navigation (#41312)
This commit is contained in:
parent
638dfd77a4
commit
c8e29d9a7d
@ -17,15 +17,14 @@ describe('Button', () => {
|
||||
await expect.element(screen.getByRole('button')).toHaveAttribute('type', 'submit')
|
||||
})
|
||||
|
||||
it('renders custom element via render prop', async () => {
|
||||
it('renders a non-native button via render prop', async () => {
|
||||
const screen = await render(
|
||||
<Button nativeButton={false} render={<a href="/test" />}>
|
||||
Link
|
||||
<Button nativeButton={false} render={<div />}>
|
||||
Custom button
|
||||
</Button>,
|
||||
)
|
||||
const button = screen.getByRole('button', { name: 'Link' }).element()
|
||||
expect(button.tagName).toBe('A')
|
||||
expect(button).toHaveAttribute('href', '/test')
|
||||
const button = screen.getByRole('button', { name: 'Custom button' }).element()
|
||||
expect(button.tagName).toBe('DIV')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Field, FieldDescription, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
@ -12,6 +12,7 @@ import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { validPassword } from '@/config'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { changeWebAppPasswordWithToken } from '@/service/common'
|
||||
|
||||
@ -182,16 +183,13 @@ const ChangePasswordForm = () => {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setLeftTime(undefined)
|
||||
router.replace(getSignInUrl())
|
||||
}}
|
||||
<Link
|
||||
href={getSignInUrl()}
|
||||
replace
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-full')}
|
||||
>
|
||||
{t(($) => $.passwordChanged, { ns: 'login' })} ({Math.round(countdown / 1000)}){' '}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DifyLogo } from '@/app/components/base/logo/dify-logo'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import Avatar from './avatar'
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
|
||||
const goToHome = useCallback(() => {
|
||||
router.push('/')
|
||||
}, [router])
|
||||
const logoLabel =
|
||||
systemFeatures.branding.enabled && systemFeatures.branding.application_title
|
||||
? systemFeatures.branding.application_title
|
||||
@ -46,11 +40,11 @@ const Header = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<Button className="px-3 py-2 system-sm-medium" onClick={goToHome}>
|
||||
<Link href="/" className={cn(buttonVariants(), 'px-3 py-2 system-sm-medium')}>
|
||||
<span aria-hidden className="i-custom-vender-main-nav-home size-4" />
|
||||
<p>{t(($) => $['mainNav.home'], { ns: 'common' })}</p>
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="h-4 w-px bg-divider-regular" />
|
||||
<Avatar />
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import {
|
||||
RiAccountCircleLine,
|
||||
@ -18,6 +19,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
@ -221,9 +223,14 @@ export default function OAuthAuthorize() {
|
||||
|
||||
<div className="flex flex-col items-center gap-2 pt-4">
|
||||
{!isLoggedIn ? (
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onLoginSwitchClick}>
|
||||
<Link
|
||||
href={`/signin?redirect_url=${encodeURIComponent(
|
||||
buildReturnUrl('/account/oauth/authorize', `?${searchParams.toString()}`),
|
||||
)}`}
|
||||
className={cn(buttonVariants({ variant: 'primary', size: 'large' }), 'w-full')}
|
||||
>
|
||||
{t(($) => $.login, { ns: 'oauth' })}
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
@ -236,9 +243,9 @@ export default function OAuthAuthorize() {
|
||||
>
|
||||
{t(($) => $.continue, { ns: 'oauth' })}
|
||||
</Button>
|
||||
<Button size="large" className="w-full" onClick={() => router.push('/apps')}>
|
||||
<Link href="/apps" className={cn(buttonVariants({ size: 'large' }), 'w-full')}>
|
||||
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -64,9 +64,12 @@ const ActivateForm = () => {
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button variant="primary" className="w-full text-sm!">
|
||||
<a href="https://dify.ai">{t(($) => $.explore, { ns: 'login' })}</a>
|
||||
</Button>
|
||||
<a
|
||||
href="https://dify.ai"
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-full text-sm!')}
|
||||
>
|
||||
{t(($) => $.explore, { ns: 'login' })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -339,7 +339,7 @@ describe('environment access point cards', () => {
|
||||
}),
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'environment-api-keys' })).toBeInTheDocument()
|
||||
const apiReferenceLink = screen.getByRole('button', { name: /apiInfo\.doc/ })
|
||||
const apiReferenceLink = screen.getByRole('link', { name: /apiInfo\.doc/ })
|
||||
expect(apiReferenceLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://docs.example.test/en/api-reference/guides/workflow',
|
||||
@ -369,8 +369,7 @@ describe('environment access point cards', () => {
|
||||
|
||||
await screen.findByText(api.base_url)
|
||||
expect(await screen.findByRole('button', { name: 'environment-api-keys' })).toBeEnabled()
|
||||
const apiReferenceLink = screen.getByRole('button', { name: /apiInfo\.doc/ })
|
||||
expect(apiReferenceLink).not.toHaveAttribute('aria-disabled')
|
||||
const apiReferenceLink = screen.getByRole('link', { name: /apiInfo\.doc/ })
|
||||
expect(apiReferenceLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://docs.example.test/en/api-reference/guides/workflow',
|
||||
@ -395,9 +394,6 @@ describe('environment access point cards', () => {
|
||||
expect(card).not.toHaveAttribute('aria-busy')
|
||||
expect(screen.queryByText('common.loading')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'environment-api-keys' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: /apiInfo\.doc/ })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: /apiInfo\.doc/ })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -57,7 +57,7 @@ describe('ServiceApiAccessPointCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const apiReferenceLink = screen.getByRole('button', { name: /apiInfo\.doc/ })
|
||||
const apiReferenceLink = screen.getByRole('link', { name: /apiInfo\.doc/ })
|
||||
|
||||
expect(apiReferenceLink).toHaveAttribute('href', `https://docs.example.test/en${path}`)
|
||||
expect(apiReferenceLink).toHaveAttribute('target', '_blank')
|
||||
@ -93,8 +93,7 @@ describe('ServiceApiAccessPointCard', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'api-secret-keys' })).toBeEnabled()
|
||||
const apiReferenceLink = screen.getByRole('button', { name: /apiInfo\.doc/ })
|
||||
expect(apiReferenceLink).not.toHaveAttribute('aria-disabled')
|
||||
const apiReferenceLink = screen.getByRole('link', { name: /apiInfo\.doc/ })
|
||||
expect(apiReferenceLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://docs.example.test/en/api-reference/guides/workflow',
|
||||
@ -126,9 +125,6 @@ describe('ServiceApiAccessPointCard', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'api-secret-keys' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: /apiInfo\.doc/ })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: /apiInfo\.doc/ })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import type { AccessPoint } from '@/app/components/app/deploy/access-point'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useMemo } from 'react'
|
||||
@ -85,16 +86,23 @@ export function BuiltInAccessPoints({ appId, highlightedAccessPoint }: BuiltInAc
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
disabled={!capabilities.canReleaseAndVersion}
|
||||
render={<Link href={`/app/${appId}/workflow`} />}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{t(($) => $['studio.accessPoint.goToPublish'], { ns: 'deployments' })}
|
||||
<span aria-hidden className="i-ri-arrow-right-line size-4" />
|
||||
</Button>
|
||||
{capabilities.canReleaseAndVersion ? (
|
||||
<Link
|
||||
href={`/app/${appId}/workflow`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'primary', size: 'medium' }),
|
||||
'flex items-center gap-1',
|
||||
)}
|
||||
>
|
||||
{t(($) => $['studio.accessPoint.goToPublish'], { ns: 'deployments' })}
|
||||
<span aria-hidden className="i-ri-arrow-right-line size-4" />
|
||||
</Link>
|
||||
) : (
|
||||
<Button variant="primary" size="medium" disabled className="flex items-center gap-1">
|
||||
{t(($) => $['studio.accessPoint.goToPublish'], { ns: 'deployments' })}
|
||||
<span aria-hidden className="i-ri-arrow-right-line size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CopyFeedback } from '@/app/components/base/copy-feedback'
|
||||
@ -111,11 +112,10 @@ export function AccessPointUrl({
|
||||
href={openUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={buttonVariants({
|
||||
variant: 'secondary',
|
||||
size: 'small',
|
||||
className: 'h-6 gap-1 px-1.5',
|
||||
})}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'secondary', size: 'small' }),
|
||||
'h-6 gap-1 px-1.5',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-external-link-line size-3.5" />
|
||||
{openLabel}
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { AccessPointStatus } from './access-point-status'
|
||||
import type { AppModeEnum } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import Link from '@/next/link'
|
||||
@ -56,23 +57,24 @@ export function ServiceApiCardView({
|
||||
actions={
|
||||
<>
|
||||
<ApiSecretKeyButton {...apiKeyButtonProps} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={!available || !apiReferenceUrl}
|
||||
nativeButton={false}
|
||||
render={
|
||||
apiReferenceUrl ? (
|
||||
<Link href={apiReferenceUrl} target="_blank" rel="noopener noreferrer" />
|
||||
) : (
|
||||
<span />
|
||||
)
|
||||
}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<span aria-hidden className="i-ri-book-open-line size-4" />
|
||||
{t(($) => $['overview.apiInfo.doc'], { ns: 'appOverview' })}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</Button>
|
||||
{available && apiReferenceUrl ? (
|
||||
<Link
|
||||
href={apiReferenceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants({ variant: 'secondary' }), 'flex items-center gap-1')}
|
||||
>
|
||||
<span aria-hidden className="i-ri-book-open-line size-4" />
|
||||
{t(($) => $['overview.apiInfo.doc'], { ns: 'appOverview' })}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</Link>
|
||||
) : (
|
||||
<Button variant="secondary" disabled className="flex items-center gap-1">
|
||||
<span aria-hidden className="i-ri-book-open-line size-4" />
|
||||
{t(($) => $['overview.apiInfo.doc'], { ns: 'appOverview' })}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { DeploymentVersion } from '../../version'
|
||||
import type { DeploymentDialogRequest } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { DialogClose, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
@ -159,14 +159,13 @@ function VersionList({
|
||||
{t(($) => $['studio.accessPoint.noPublishedTitle'])}
|
||||
</p>
|
||||
{publishHref && (
|
||||
<Button
|
||||
size="medium"
|
||||
render={<Link href={publishHref} />}
|
||||
className="flex items-center gap-1"
|
||||
<Link
|
||||
href={publishHref}
|
||||
className={cn(buttonVariants({ size: 'medium' }), 'flex items-center gap-1')}
|
||||
>
|
||||
{t(($) => $['studio.accessPoint.goToPublish'])}
|
||||
<span aria-hidden className="i-ri-arrow-right-line size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { InSiteMessageActionItem } from '../index'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import InSiteMessage from '../index'
|
||||
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
@ -9,19 +10,8 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
}))
|
||||
|
||||
describe('InSiteMessage', () => {
|
||||
const originalLocation = window.location
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('open', vi.fn())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
configurable: true,
|
||||
})
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
const renderComponent = (
|
||||
@ -59,7 +49,7 @@ describe('InSiteMessage', () => {
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close' })
|
||||
const outlineButton = screen.getByRole('button', { name: 'Outline' })
|
||||
const learnMoreButton = screen.getByRole('button', { name: 'Learn more' })
|
||||
const learnMoreLink = screen.getByRole('link', { name: 'Learn more' })
|
||||
const panel = closeButton.closest('div.fixed')
|
||||
const titleElement = panel?.querySelector('.title-3xl-bold')
|
||||
const subtitleElement = panel?.querySelector('.body-md-regular')
|
||||
@ -71,7 +61,7 @@ describe('InSiteMessage', () => {
|
||||
expect(screen.getByText('Main content')).toBeInTheDocument()
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
expect(outlineButton).toHaveClass('bg-components-button-secondary-bg')
|
||||
expect(learnMoreButton).toBeInTheDocument()
|
||||
expect(learnMoreLink).toHaveAttribute('href', 'https://example.com')
|
||||
})
|
||||
|
||||
it('should fallback to default header background when headerBgUrl is empty string', () => {
|
||||
@ -103,7 +93,8 @@ describe('InSiteMessage', () => {
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open a new tab when link action data is a string', () => {
|
||||
it('should render a new-tab link and report its activation', () => {
|
||||
const onAction = vi.fn()
|
||||
const linkAction: InSiteMessageActionItem = {
|
||||
action: 'link',
|
||||
action_name: 'confirm',
|
||||
@ -112,26 +103,22 @@ describe('InSiteMessage', () => {
|
||||
data: 'https://example.com',
|
||||
}
|
||||
|
||||
renderComponent([linkAction])
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open link' }))
|
||||
renderComponent([linkAction], { onAction })
|
||||
const link = screen.getByRole('link', { name: 'Open link' })
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://example.com',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
expect(link).toHaveAttribute('href', 'https://example.com')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
fireEvent.click(link)
|
||||
|
||||
expect(onAction).toHaveBeenCalledWith(linkAction)
|
||||
expect(vi.mocked(trackEvent)).toHaveBeenCalledWith('in_site_message_action', {
|
||||
notification_id: 'test-notification-id',
|
||||
action: 'confirm',
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate with location.assign when link action target is _self', () => {
|
||||
const assignSpy = vi.fn()
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...originalLocation,
|
||||
assign: assignSpy,
|
||||
},
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
it('should render a same-tab link when target is _self', () => {
|
||||
const linkAction: InSiteMessageActionItem = {
|
||||
action: 'link',
|
||||
action_name: 'confirm',
|
||||
@ -141,10 +128,10 @@ describe('InSiteMessage', () => {
|
||||
}
|
||||
|
||||
renderComponent([linkAction])
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open self' }))
|
||||
const link = screen.getByRole('link', { name: 'Open self' })
|
||||
|
||||
expect(assignSpy).toHaveBeenCalledWith('https://example.com/self')
|
||||
expect(window.open).not.toHaveBeenCalled()
|
||||
expect(link).toHaveAttribute('href', 'https://example.com/self')
|
||||
expect(link).toHaveAttribute('target', '_self')
|
||||
})
|
||||
|
||||
it('should not trigger navigation when link data is invalid', () => {
|
||||
@ -159,7 +146,7 @@ describe('InSiteMessage', () => {
|
||||
renderComponent([linkAction])
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Broken link' }))
|
||||
|
||||
expect(window.open).not.toHaveBeenCalled()
|
||||
expect(screen.queryByRole('link', { name: 'Broken link' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -137,12 +137,15 @@ describe('InSiteMessageNotification', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Parsed body main')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByRole('button', { name: 'Visit docs' })).toBeInTheDocument()
|
||||
const docsLink = screen.getByRole('link', { name: 'Visit docs' })
|
||||
expect(docsLink).toHaveAttribute('href', 'https://example.com/docs')
|
||||
expect(docsLink).toHaveAttribute('target', '_blank')
|
||||
expect(docsLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
expect(screen.getByRole('button', { name: 'Outline close' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Dismiss now' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Invalid' })).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Visit docs' }))
|
||||
fireEvent.click(docsLink)
|
||||
expect(mockNotificationDismiss).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dismiss now' }))
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
@ -90,21 +90,7 @@ function InSiteMessage({
|
||||
})
|
||||
onAction?.(item)
|
||||
|
||||
if (item.action === 'close') {
|
||||
setVisible(false)
|
||||
return
|
||||
}
|
||||
|
||||
const linkData = normalizeLinkData(item.data)
|
||||
if (!linkData) return
|
||||
|
||||
const target = linkData.target ?? '_blank'
|
||||
if (target === '_self') {
|
||||
window.location.assign(linkData.href)
|
||||
return
|
||||
}
|
||||
|
||||
window.open(linkData.href, target, linkData.rel || 'noopener,noreferrer')
|
||||
if (item.action === 'close') setVisible(false)
|
||||
}
|
||||
|
||||
if (!visible) return null
|
||||
@ -129,17 +115,42 @@ function InSiteMessage({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 p-4">
|
||||
{actions.map((item) => (
|
||||
<Button
|
||||
key={`${item.type}-${item.action}-${item.text}`}
|
||||
variant={resolveButtonVariant(item.type)}
|
||||
size="medium"
|
||||
className={cn(item.type === 'default' && 'text-text-secondary')}
|
||||
onClick={() => handleAction(item)}
|
||||
>
|
||||
{item.text}
|
||||
</Button>
|
||||
))}
|
||||
{actions.map((item) => {
|
||||
const variant = resolveButtonVariant(item.type)
|
||||
const className = cn(
|
||||
buttonVariants({ variant, size: 'medium' }),
|
||||
item.type === 'default' && 'text-text-secondary',
|
||||
)
|
||||
const linkData = item.action === 'link' ? normalizeLinkData(item.data) : null
|
||||
|
||||
if (linkData) {
|
||||
const target = linkData.target ?? '_blank'
|
||||
return (
|
||||
<a
|
||||
key={`${item.type}-${item.action}-${item.text}`}
|
||||
href={linkData.href}
|
||||
target={target}
|
||||
rel={linkData.rel || (target === '_blank' ? 'noopener noreferrer' : undefined)}
|
||||
className={className}
|
||||
onClick={() => handleAction(item)}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={`${item.type}-${item.action}-${item.text}`}
|
||||
variant={variant}
|
||||
size="medium"
|
||||
className={cn(item.type === 'default' && 'text-text-secondary')}
|
||||
onClick={() => handleAction(item)}
|
||||
>
|
||||
{item.text}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@ -102,22 +103,18 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">
|
||||
{t(($) => $[`${prefixCustomize}.way1.step1Tip`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a
|
||||
href={`https://github.com/langgenius/${repository}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t(($) => $[`${prefixCustomize}.way1.step1Operation`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
/>
|
||||
}
|
||||
<a
|
||||
href={`https://github.com/langgenius/${repository}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t(($) => $[`${prefixCustomize}.way1.step1Operation`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className={buttonVariants()}
|
||||
>
|
||||
<GithubIcon className="text-text-secondary" />
|
||||
{t(($) => $[`${prefixCustomize}.way1.step1Operation`], { ns: 'appOverview' })}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-4">
|
||||
@ -129,24 +126,20 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">
|
||||
{t(($) => $[`${prefixCustomize}.way1.step2Tip`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a
|
||||
href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t(($) => $[`${prefixCustomize}.way1.step2Operation`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
/>
|
||||
}
|
||||
<a
|
||||
href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t(($) => $[`${prefixCustomize}.way1.step2Operation`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className={buttonVariants()}
|
||||
>
|
||||
<div className="border-t-0 border-r-[7px] border-b-12 border-l-[7px] border-solid border-text-primary border-t-transparent border-r-transparent border-l-transparent"></div>
|
||||
<span>
|
||||
{t(($) => $[`${prefixCustomize}.way1.step2Operation`], { ns: 'appOverview' })}
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-4">
|
||||
@ -176,19 +169,14 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
<p className="my-2 system-sm-medium text-text-secondary">
|
||||
{t(($) => $[`${prefixCustomize}.way2.name`], { ns: 'appOverview' })}
|
||||
</p>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a
|
||||
href={apiDocLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t(($) => $[`${prefixCustomize}.way2.operation`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
/>
|
||||
}
|
||||
className="mt-2"
|
||||
<a
|
||||
href={apiDocLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t(($) => $[`${prefixCustomize}.way2.operation`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className={cn(buttonVariants(), 'mt-2')}
|
||||
>
|
||||
<span className="text-sm text-text-secondary">
|
||||
{t(($) => $[`${prefixCustomize}.way2.operation`], { ns: 'appOverview' })}
|
||||
@ -197,7 +185,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
aria-hidden="true"
|
||||
className="i-heroicons-arrow-top-right-on-square size-4 shrink-0 text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -5,10 +5,8 @@ import { FeaturesProvider } from '../../../context'
|
||||
import AnnotationReply from '../index'
|
||||
|
||||
const originalConsoleError = console.error
|
||||
const mockPush = vi.fn()
|
||||
let mockPathname = '/app/test-app-id/configuration'
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
usePathname: () => mockPathname,
|
||||
}))
|
||||
|
||||
@ -273,7 +271,7 @@ describe('AnnotationReply', () => {
|
||||
expect(mockSetIsShowAnnotationConfigInit).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should navigate to annotations page when cache management is clicked', () => {
|
||||
it('should link to the app annotations page from cache management', () => {
|
||||
renderWithProvider(
|
||||
{},
|
||||
{
|
||||
@ -290,9 +288,10 @@ describe('AnnotationReply', () => {
|
||||
|
||||
const card = screen.getByText(/feature\.annotation\.title/).closest('[class]')!
|
||||
fireEvent.mouseEnter(card)
|
||||
fireEvent.click(screen.getByText(/feature\.annotation\.cacheManagement/))
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/app/test-app-id/annotations')
|
||||
expect(
|
||||
screen.getByRole('link', { name: /feature\.annotation\.cacheManagement/ }),
|
||||
).toHaveAttribute('href', '/app/test-app-id/annotations')
|
||||
})
|
||||
|
||||
it('should fallback appId to empty string when pathname does not match', () => {
|
||||
@ -313,9 +312,10 @@ describe('AnnotationReply', () => {
|
||||
|
||||
const card = screen.getByText(/feature\.annotation\.title/).closest('[class]')!
|
||||
fireEvent.mouseEnter(card)
|
||||
fireEvent.click(screen.getByText(/feature\.annotation\.cacheManagement/))
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/app//annotations')
|
||||
expect(
|
||||
screen.getByRole('link', { name: /feature\.annotation\.cacheManagement/ }),
|
||||
).toHaveAttribute('href', '/app//annotations')
|
||||
})
|
||||
|
||||
it('should show config param modal when isShowAnnotationConfigInit is true', async () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import type { AnnotationReplyConfig } from '@/models/debug'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiEqualizer2Line, RiExternalLinkLine } from '@remixicon/react'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
@ -13,7 +14,8 @@ import FeatureCard from '@/app/components/base/features/new-feature-panel/featur
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/features'
|
||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||
import { ANNOTATION_DEFAULT } from '@/config'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import Link from '@/next/link'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
|
||||
type Props = Readonly<{
|
||||
disabled?: boolean
|
||||
@ -22,7 +24,6 @@ type Props = Readonly<{
|
||||
|
||||
const AnnotationReply = ({ disabled, onChange }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const matched = /\/app\/([^/]+)/.exec(pathname)
|
||||
const appId = matched?.length && matched[1] ? matched[1] : ''
|
||||
@ -126,15 +127,13 @@ const AnnotationReply = ({ disabled, onChange }: Props) => {
|
||||
<RiEqualizer2Line className="size-4" />
|
||||
{t(($) => $['operation.params'], { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-44.5"
|
||||
onClick={() => {
|
||||
router.push(`/app/${appId}/annotations`)
|
||||
}}
|
||||
<Link
|
||||
href={`/app/${appId}/annotations`}
|
||||
className={cn(buttonVariants(), 'w-44.5')}
|
||||
>
|
||||
<RiExternalLinkLine className="size-4" />
|
||||
{t(($) => $['feature.annotation.cacheManagement'], { ns: 'appDebug' })}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -52,21 +52,19 @@ describe('MarkdownButton (integration)', () => {
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Click me')
|
||||
})
|
||||
|
||||
it('opens new tab when link is valid and does not call onSend', async () => {
|
||||
it('renders a native link when the URL is valid', () => {
|
||||
isValidUrlSpy.mockReturnValue(true)
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
const user = userEvent.setup()
|
||||
|
||||
const node = createButtonNode({ dataLink: 'https://example.com' }, 'Go')
|
||||
|
||||
renderWithCtx(node)
|
||||
await user.click(screen.getByRole('button'))
|
||||
const link = screen.getByRole('link', { name: 'Go' })
|
||||
|
||||
expect(isValidUrlSpy).toHaveBeenCalledWith('https://example.com')
|
||||
expect(openSpy).toHaveBeenCalledWith('https://example.com', '_blank')
|
||||
expect(link).toHaveAttribute('href', 'https://example.com')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
expect(onSendSpy).not.toHaveBeenCalled()
|
||||
|
||||
openSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('calls onSend when link is invalid but message exists', async () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { ExtraProps } from 'streamdown'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
import { getMarkdownButtonAppearance } from './button-appearance'
|
||||
@ -22,16 +22,28 @@ const MarkdownButton = ({ node }: MarkdownButtonProps) => {
|
||||
const link = getStringProperty(node?.properties.dataLink)
|
||||
const firstChild = node?.children[0]
|
||||
const label = firstChild?.type === 'text' ? firstChild.value : ''
|
||||
const validLink = link && isValidUrl(link) ? link : undefined
|
||||
|
||||
const className = 'h-auto! min-h-8 px-3! whitespace-normal select-none'
|
||||
|
||||
if (validLink) {
|
||||
return (
|
||||
<a
|
||||
href={validLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants(appearance), className)}
|
||||
>
|
||||
<span className="text-[13px]">{label}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...appearance}
|
||||
className={cn('h-auto! min-h-8 px-3! whitespace-normal select-none')}
|
||||
className={className}
|
||||
onClick={() => {
|
||||
if (link && isValidUrl(link)) {
|
||||
window.open(link, '_blank')
|
||||
return
|
||||
}
|
||||
if (!message) return
|
||||
onSend?.(message)
|
||||
}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { MeterTone } from '@langgenius/dify-ui/meter'
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Meter, MeterIndicator, MeterTrack } from '@langgenius/dify-ui/meter'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
@ -61,19 +61,18 @@ const AppsFull: FC<{ loc: string; className?: string }> = ({ loc, className }) =
|
||||
<UpgradeBtn isShort loc={loc} />
|
||||
)}
|
||||
{plan.type !== 'sandbox' && plan.type !== 'professional' && (
|
||||
<Button variant="secondary-accent">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={mailToSupport(
|
||||
accountProfile.email,
|
||||
plan.type,
|
||||
accountProfile.currentVersion ?? '',
|
||||
)}
|
||||
>
|
||||
{t(($) => $['apps.contactUs'], { ns: 'billing' })}
|
||||
</a>
|
||||
</Button>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={mailToSupport(
|
||||
accountProfile.email,
|
||||
plan.type,
|
||||
accountProfile.currentVersion ?? '',
|
||||
)}
|
||||
className={buttonVariants({ variant: 'secondary-accent' })}
|
||||
>
|
||||
{t(($) => $['apps.contactUs'], { ns: 'billing' })}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -10,10 +11,15 @@ const Header = () => {
|
||||
return (
|
||||
<div className="relative flex px-16 pt-5 pb-2 system-md-semibold text-text-primary">
|
||||
<span>{t(($) => $['creation.backToKnowledge'], { ns: 'datasetPipeline' })}</span>
|
||||
<Link className="absolute bottom-0 left-5" href="/datasets" replace>
|
||||
<Button variant="secondary-accent" className="size-9 rounded-full p-0">
|
||||
<RiArrowLeftLine className="size-5" />
|
||||
</Button>
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'secondary-accent' }),
|
||||
'absolute bottom-0 left-5 size-9 rounded-full p-0',
|
||||
)}
|
||||
href="/datasets"
|
||||
replace
|
||||
>
|
||||
<RiArrowLeftLine className="size-5" />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,7 +3,6 @@ import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import EmbeddingProcess from '../index'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
const mockInvalidDocumentList = vi.fn()
|
||||
let mockEnableBilling = false
|
||||
let mockPlanType = 'sandbox'
|
||||
@ -17,10 +16,6 @@ let mockPollingState: {
|
||||
isEmbeddingCompleted: false,
|
||||
}
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: ({
|
||||
children,
|
||||
@ -184,14 +179,15 @@ describe('EmbeddingProcess', () => {
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('invalidates the document list before navigating to it', async () => {
|
||||
it('links to the document list and invalidates its cache on activation', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<EmbeddingProcess datasetId="dataset-1" batchId="batch-1" />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'datasetCreation.stepThree.navTo' }))
|
||||
const link = screen.getByRole('link', { name: 'datasetCreation.stepThree.navTo' })
|
||||
expect(link).toHaveAttribute('href', '/datasets/dataset-1/documents')
|
||||
await user.click(link)
|
||||
|
||||
expect(mockInvalidDocumentList).toHaveBeenCalledOnce()
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents')
|
||||
})
|
||||
|
||||
it('links to the dataset API reference', () => {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FullDocumentDetail } from '@/models/datasets'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowRightLine, RiLoader2Fill, RiTerminalBoxLine } from '@remixicon/react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -10,7 +11,6 @@ import VectorSpaceAdmissionAlert from '@/app/components/datasets/common/vector-s
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useProcessRule } from '@/service/knowledge/use-dataset'
|
||||
import { useInvalidDocumentList } from '@/service/knowledge/use-document'
|
||||
import IndexingProgressItem from './indexing-progress-item'
|
||||
@ -50,22 +50,30 @@ const StatusHeader: FC<{ isEmbedding: boolean; isCompleted: boolean }> = ({
|
||||
// Action buttons component
|
||||
const ActionButtons: FC<{
|
||||
apiReferenceUrl: string
|
||||
onNavToDocuments: () => void
|
||||
}> = ({ apiReferenceUrl, onNavToDocuments }) => {
|
||||
documentsHref: string
|
||||
onNavigateToDocuments: () => void
|
||||
}> = ({ apiReferenceUrl, documentsHref, onNavigateToDocuments }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex items-center gap-x-2 py-2">
|
||||
<Link href={apiReferenceUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Button className="w-fit">
|
||||
<RiTerminalBoxLine className="size-4" />
|
||||
<span>Access the API</span>
|
||||
</Button>
|
||||
<Link
|
||||
href={apiReferenceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants(), 'w-fit')}
|
||||
>
|
||||
<RiTerminalBoxLine className="size-4" />
|
||||
<span>Access the API</span>
|
||||
</Link>
|
||||
<Button className="w-fit" variant="primary" onClick={onNavToDocuments}>
|
||||
<Link
|
||||
href={documentsHref}
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-fit')}
|
||||
onClick={onNavigateToDocuments}
|
||||
>
|
||||
<span>{t(($) => $['stepThree.navTo'], { ns: 'datasetCreation' })}</span>
|
||||
<RiArrowRightLine className="size-4 stroke-current stroke-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -78,7 +86,6 @@ const EmbeddingProcess: FC<EmbeddingProcessProps> = ({
|
||||
retrievalMethod,
|
||||
}) => {
|
||||
const { enableBilling, plan } = useProviderContext()
|
||||
const router = useRouter()
|
||||
const invalidDocumentList = useInvalidDocumentList()
|
||||
const apiReferenceUrl = useDatasetApiAccessUrl()
|
||||
|
||||
@ -95,10 +102,7 @@ const EmbeddingProcess: FC<EmbeddingProcessProps> = ({
|
||||
// Document lookup utilities - memoized for performance
|
||||
const documentLookup = useMemo(() => createDocumentLookup(documents), [documents])
|
||||
|
||||
const handleNavToDocuments = () => {
|
||||
invalidDocumentList()
|
||||
router.push(`/datasets/${datasetId}/documents`)
|
||||
}
|
||||
const documentsHref = `/datasets/${datasetId}/documents`
|
||||
|
||||
const showUpgradeBanner = enableBilling && plan.type !== 'team'
|
||||
const showVectorSpaceUpgrade =
|
||||
@ -145,7 +149,11 @@ const EmbeddingProcess: FC<EmbeddingProcessProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionButtons apiReferenceUrl={apiReferenceUrl} onNavToDocuments={handleNavToDocuments} />
|
||||
<ActionButtons
|
||||
apiReferenceUrl={apiReferenceUrl}
|
||||
documentsHref={documentsHref}
|
||||
onNavigateToDocuments={invalidDocumentList}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
@ -63,10 +64,12 @@ const Actions = ({
|
||||
</>
|
||||
)}
|
||||
<div className="flex grow items-center justify-end gap-x-2">
|
||||
<Link href={`/datasets/${datasetId}/documents`} replace>
|
||||
<Button variant="ghost" className="px-3 py-2">
|
||||
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
||||
</Button>
|
||||
<Link
|
||||
href={`/datasets/${datasetId}/documents`}
|
||||
replace
|
||||
className={cn(buttonVariants({ variant: 'ghost' }), 'px-3 py-2')}
|
||||
>
|
||||
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
||||
</Link>
|
||||
<Button disabled={disabled} variant="primary" onClick={handleNextStep}>
|
||||
<span>{t(($) => $['stepOne.button'], { ns: 'datasetCreation' })}</span>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Step } from './step-indicator'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import Effect from '@/app/components/base/effect'
|
||||
@ -27,13 +28,15 @@ const LeftHeader = ({ steps, title, currentStep }: LeftHeaderProps) => {
|
||||
</div>
|
||||
<div className="system-md-semibold text-text-primary">{steps[currentStep - 1]?.label}</div>
|
||||
{currentStep !== steps.length && (
|
||||
<Link href={`/datasets/${datasetId}/documents`} replace>
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
className="absolute top-3.5 -left-11 size-9 rounded-full p-0"
|
||||
>
|
||||
<RiArrowLeftLine className="size-5" />
|
||||
</Button>
|
||||
<Link
|
||||
href={`/datasets/${datasetId}/documents`}
|
||||
replace
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'secondary-accent' }),
|
||||
'absolute top-3.5 -left-11 size-9 rounded-full p-0',
|
||||
)}
|
||||
>
|
||||
<RiArrowLeftLine className="size-5" />
|
||||
</Link>
|
||||
)}
|
||||
<Effect className="-top-8.5 left-8 opacity-20" />
|
||||
|
||||
@ -10,13 +10,6 @@ import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import EmbeddingProcess from '../index'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock next/link
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: function MockLink({
|
||||
@ -650,29 +643,15 @@ describe('EmbeddingProcess', () => {
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
// Tests for button clicks and navigation
|
||||
it('should navigate to document list when nav button is clicked', async () => {
|
||||
it('should link to the document list and invalidate its cache on activation', () => {
|
||||
const props = createDefaultProps({ datasetId: 'my-dataset-123' })
|
||||
|
||||
render(<EmbeddingProcess {...props} />)
|
||||
const navButton = screen.getByText('datasetCreation.stepThree.navTo')
|
||||
fireEvent.click(navButton)
|
||||
const link = screen.getByRole('link', { name: 'datasetCreation.stepThree.navTo' })
|
||||
expect(link).toHaveAttribute('href', '/datasets/my-dataset-123/documents')
|
||||
fireEvent.click(link)
|
||||
|
||||
expect(mockInvalidDocumentList).toHaveBeenCalled()
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/my-dataset-123/documents')
|
||||
})
|
||||
|
||||
it('should call invalidDocumentList before navigation', () => {
|
||||
const props = createDefaultProps()
|
||||
const callOrder: string[] = []
|
||||
mockInvalidDocumentList.mockImplementation(() => callOrder.push('invalidate'))
|
||||
mockPush.mockImplementation(() => callOrder.push('push'))
|
||||
|
||||
render(<EmbeddingProcess {...props} />)
|
||||
const navButton = screen.getByText('datasetCreation.stepThree.navTo')
|
||||
fireEvent.click(navButton)
|
||||
|
||||
expect(callOrder).toEqual(['invalidate', 'push'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import type { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import type { IndexingStatusResponse } from '@/models/datasets'
|
||||
import type { InitialDocumentDetail } from '@/models/pipeline'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
@ -26,7 +26,6 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
import { DatasourceType } from '@/models/pipeline'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useIndexingStatusBatch, useProcessRule } from '@/service/knowledge/use-dataset'
|
||||
import { useInvalidDocumentList } from '@/service/knowledge/use-document'
|
||||
import RuleDetail from './rule-detail'
|
||||
@ -47,7 +46,6 @@ const EmbeddingProcess = ({
|
||||
retrievalMethod,
|
||||
}: EmbeddingProcessProps) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { enableBilling, plan } = useProviderContext()
|
||||
const [indexingStatusBatchDetail, setIndexingStatusDetail] = useState<IndexingStatusResponse[]>(
|
||||
[],
|
||||
@ -86,10 +84,7 @@ const EmbeddingProcess = ({
|
||||
const { data: ruleDetail } = useProcessRule(firstDocument!.id)
|
||||
|
||||
const invalidDocumentList = useInvalidDocumentList()
|
||||
const navToDocumentList = () => {
|
||||
invalidDocumentList()
|
||||
router.push(`/datasets/${datasetId}/documents`)
|
||||
}
|
||||
const documentsHref = `/datasets/${datasetId}/documents`
|
||||
const apiReferenceUrl = useDatasetApiAccessUrl()
|
||||
|
||||
const isEmbeddingWaiting = useMemo(() => {
|
||||
@ -255,16 +250,23 @@ const EmbeddingProcess = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center gap-x-2 py-2">
|
||||
<Link href={apiReferenceUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Button className="w-fit">
|
||||
<RiTerminalBoxLine className="size-4" />
|
||||
<span>Access the API</span>
|
||||
</Button>
|
||||
<Link
|
||||
href={apiReferenceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants(), 'w-fit')}
|
||||
>
|
||||
<RiTerminalBoxLine className="size-4" />
|
||||
<span>Access the API</span>
|
||||
</Link>
|
||||
<Button className="w-fit" variant="primary" onClick={navToDocumentList}>
|
||||
<Link
|
||||
href={documentsHref}
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-fit')}
|
||||
onClick={invalidDocumentList}
|
||||
>
|
||||
<span>{t(($) => $['stepThree.navTo'], { ns: 'datasetCreation' })}</span>
|
||||
<RiArrowRightLine className="size-4 stroke-current stroke-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -368,16 +368,12 @@ describe('ExternalKnowledgeBaseConnector', () => {
|
||||
expect((descriptionInput as HTMLTextAreaElement).value).toBe('My Description')
|
||||
})
|
||||
|
||||
it('should handle cancel button click', async () => {
|
||||
const user = userEvent.setup()
|
||||
it('should link back to the dataset list from cancel', () => {
|
||||
render(<ExternalKnowledgeBaseConnector />)
|
||||
|
||||
const cancelButton = screen
|
||||
.getByText('dataset.externalKnowledgeForm.cancel')
|
||||
.closest('button')
|
||||
await user.click(cancelButton!)
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'dataset.externalKnowledgeForm.cancel' }),
|
||||
).toHaveAttribute('href', '/datasets')
|
||||
})
|
||||
|
||||
it('should handle back button click', async () => {
|
||||
|
||||
@ -15,6 +15,14 @@ vi.mock('@/next/navigation', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: ({ children, replace, ...props }: React.ComponentProps<'a'> & { replace?: boolean }) => (
|
||||
<a {...props} data-replace={replace || undefined}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock useDocLink hook
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path?: string) =>
|
||||
@ -363,16 +371,12 @@ describe('ExternalKnowledgeBaseCreate', () => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||
})
|
||||
|
||||
it('should navigate back when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
it('should link back to the dataset list from cancel', () => {
|
||||
renderComponent()
|
||||
|
||||
const cancelButton = screen
|
||||
.getByText('dataset.externalKnowledgeForm.cancel')
|
||||
.closest('button')
|
||||
await user.click(cancelButton!)
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||
const link = screen.getByRole('link', { name: 'dataset.externalKnowledgeForm.cancel' })
|
||||
expect(link).toHaveAttribute('href', '/datasets')
|
||||
expect(link).toHaveAttribute('data-replace', 'true')
|
||||
})
|
||||
|
||||
it('should call onConnect with complete form data when connect is clicked', async () => {
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import type { CreateKnowledgeBaseReq } from './declarations'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import ExternalApiSelection from './ExternalApiSelection'
|
||||
import InfoPanel from './InfoPanel'
|
||||
@ -124,11 +125,11 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 self-stretch py-2">
|
||||
<Button variant="secondary" onClick={navBackHandle}>
|
||||
<Link href="/datasets" replace className={buttonVariants({ variant: 'secondary' })}>
|
||||
<div className="system-sm-medium text-components-button-secondary-text">
|
||||
{t(($) => $['externalKnowledgeForm.cancel'], { ns: 'dataset' })}
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { PopoverClose } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -68,17 +69,17 @@ export function ServiceApiCard({ apiBaseUrl, canManageApiKey, onOpenApiKeyModal
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="text-text-tertiary"
|
||||
render={<Link href={apiReferenceUrl} target="_blank" rel="noopener noreferrer" />}
|
||||
<Link
|
||||
href={apiReferenceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants({ variant: 'ghost', size: 'small' }), 'text-text-tertiary')}
|
||||
>
|
||||
<span aria-hidden className="i-ri-book-open-line size-3.5 shrink-0" />
|
||||
<span className="system-xs-medium">
|
||||
{t(($) => $['serviceApi.card.apiReference'], { ns: 'dataset' })}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
@ -125,21 +126,22 @@ const Installed: FC<Props> = ({
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="min-w-18"
|
||||
render={categoryTarget ? <Link href={categoryTarget.path} /> : undefined}
|
||||
onClick={handleClose}
|
||||
>
|
||||
{categoryTarget ? (
|
||||
{categoryTarget ? (
|
||||
<Link
|
||||
href={categoryTarget.path}
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'min-w-18')}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<>
|
||||
<span>{t(($) => $['installModal.viewDetails'], { ns: 'plugin' })}</span>
|
||||
<span className="i-ri-arrow-right-up-line size-4 shrink-0" aria-hidden="true" />
|
||||
</>
|
||||
) : (
|
||||
t(($) => $['operation.close'], { ns: 'common' })
|
||||
)}
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Button variant="primary" className="min-w-18" onClick={handleClose}>
|
||||
{t(($) => $['operation.close'], { ns: 'common' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -107,7 +107,7 @@ describe('CardWrapper', () => {
|
||||
screen.getByRole('button', { name: 'plugin.detailPanel.operation.install' }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'plugin.detailPanel.operation.detail' }),
|
||||
screen.getByRole('link', { name: 'plugin.detailPanel.operation.detail' }),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -122,18 +122,13 @@ describe('CardWrapper', () => {
|
||||
expect(screen.queryByTestId('install-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens marketplace detail from the detail action', () => {
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
|
||||
it('links the detail action to the marketplace', () => {
|
||||
renderCardWrapper({ showInstallButton: true })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.detail' }))
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
'/marketplace/dify/plugin-a?language=en-US&theme=system',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
const link = screen.getByRole('link', { name: 'plugin.detailPanel.operation.detail' })
|
||||
expect(link).toHaveAttribute('href', '/marketplace/dify/plugin-a?language=en-US&theme=system')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('opens and closes install modal from install action', () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTheme } from 'next-themes'
|
||||
import * as React from 'react'
|
||||
@ -50,13 +51,6 @@ const CardWrapperComponent = ({
|
||||
() => plugin.tags.map((tag) => getTagLabel(tag.name)),
|
||||
[plugin.tags, getTagLabel],
|
||||
)
|
||||
const handleOpenMarketplaceDetail = () => {
|
||||
window.open(
|
||||
getPluginLinkInMarketplace(plugin, marketplaceLinkParams),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
}
|
||||
const showInstallAction = !!showInstallButton && canInstallPlugin
|
||||
|
||||
if (showInstallAction) {
|
||||
@ -85,13 +79,15 @@ const CardWrapperComponent = ({
|
||||
? t(($) => $['task.installed'], { ns: 'plugin' })
|
||||
: t(($) => $['detailPanel.operation.install'], { ns: 'plugin' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-0 flex-1 shadow-xs backdrop-blur-[5px]"
|
||||
onClick={handleOpenMarketplaceDetail}
|
||||
<a
|
||||
href={getPluginLinkInMarketplace(plugin, marketplaceLinkParams)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants(), 'min-w-0 flex-1 shadow-xs backdrop-blur-[5px]')}
|
||||
>
|
||||
{t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-4" />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
{isShowInstallFromMarketplace && (
|
||||
<InstallFromMarketplace
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Plugin } from './types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTheme } from 'next-themes'
|
||||
@ -79,16 +79,15 @@ const ProviderCardComponent: FC<Props> = ({ className, payload }) => {
|
||||
{t(($) => $['detailPanel.operation.install'], { ns: 'plugin' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button className="grow" variant="secondary">
|
||||
<a
|
||||
href={getPluginLinkInMarketplace(payload, marketplaceLinkParams)}
|
||||
target="_blank"
|
||||
className="flex items-center gap-0.5"
|
||||
>
|
||||
{t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })}
|
||||
<span className="i-ri-arrow-right-up-line size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<a
|
||||
href={getPluginLinkInMarketplace(payload, marketplaceLinkParams)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants({ variant: 'secondary' }), 'grow gap-0.5')}
|
||||
>
|
||||
{t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })}
|
||||
<span className="i-ri-arrow-right-up-line size-4" />
|
||||
</a>
|
||||
</div>
|
||||
{isShowInstallFromMarketplace && (
|
||||
<InstallFromMarketplace
|
||||
|
||||
@ -37,18 +37,12 @@ const triggerHotkey = (hotkey: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'test-dataset-id' }),
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: ({ children, href, ...props }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
default: ({ children, ...props }: React.ComponentProps<'a'>) => <a {...props}>{children}</a>,
|
||||
}))
|
||||
|
||||
const mockHandleSyncWorkflowDraft = vi.fn()
|
||||
@ -433,17 +427,6 @@ describe('publisher', () => {
|
||||
expect(addDocumentsButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable action buttons when published', () => {
|
||||
mockPublishedAt.mockReturnValue(1700000000)
|
||||
|
||||
renderWithQueryClient(<Popup />)
|
||||
|
||||
const addDocumentsButton = screen
|
||||
.getAllByRole('button')
|
||||
.find((btn) => btn.textContent?.includes('pipeline.common.goToAddDocuments'))
|
||||
expect(addDocumentsButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should show premium badge when publish as template is not allowed', () => {
|
||||
mockPublishedAt.mockReturnValue(1700000000)
|
||||
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false)
|
||||
@ -506,18 +489,13 @@ describe('publisher', () => {
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should navigate to add documents when go to add documents is clicked', async () => {
|
||||
it('should link to add documents when the pipeline is published', () => {
|
||||
mockPublishedAt.mockReturnValue(1700000000)
|
||||
renderWithQueryClient(<Popup />)
|
||||
|
||||
const addDocumentsButton = screen
|
||||
.getAllByRole('button')
|
||||
.find((btn) => btn.textContent?.includes('pipeline.common.goToAddDocuments'))
|
||||
fireEvent.click(addDocumentsButton!)
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
'/datasets/test-dataset-id/documents/create-from-pipeline',
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'pipeline.common.goToAddDocuments' }),
|
||||
).toHaveAttribute('href', '/datasets/test-dataset-id/documents/create-from-pipeline')
|
||||
})
|
||||
|
||||
it('should show pricing modal when publish as template is clicked without permission', async () => {
|
||||
@ -913,9 +891,12 @@ describe('publisher', () => {
|
||||
|
||||
renderWithQueryClient(<Popup />)
|
||||
|
||||
const apiLink = screen.getByRole('link')
|
||||
const apiLink = screen.getByRole('link', {
|
||||
name: 'workflow.common.accessAPIReference',
|
||||
})
|
||||
expect(apiLink).toHaveAttribute('href', 'https://api.dify.ai/v1/datasets/test-dataset-id')
|
||||
expect(apiLink).toHaveAttribute('target', '_blank')
|
||||
expect(apiLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -39,7 +39,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
promise: toastMocks.promise,
|
||||
}),
|
||||
}))
|
||||
const mockPush = vi.fn()
|
||||
const mockHandleCheckBeforePublish = vi.fn().mockResolvedValue(true)
|
||||
const mockSetPublishedAt = vi.fn()
|
||||
const mockMutateDatasetRes = vi.fn()
|
||||
@ -60,13 +59,10 @@ let mockWorkspacePermissionKeys: string[] = []
|
||||
const mockUseBoolean = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'ds-123' }),
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
default: ({ children, ...props }: React.ComponentProps<'a'>) => <a {...props}>{children}</a>,
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
@ -275,18 +271,6 @@ describe('Popup', () => {
|
||||
expect(container.querySelectorAll('kbd')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should render "Go to Add Documents" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('pipeline.common.goToAddDocuments')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "API Reference" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('workflow.common.accessAPIReference')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "Publish As" button', () => {
|
||||
const { container } = render(<Popup />)
|
||||
|
||||
@ -312,12 +296,21 @@ describe('Popup', () => {
|
||||
})
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate to add documents page', () => {
|
||||
it('should link to the add documents page', () => {
|
||||
render(<Popup />)
|
||||
|
||||
fireEvent.click(screen.getByText('pipeline.common.goToAddDocuments'))
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'pipeline.common.goToAddDocuments' }),
|
||||
).toHaveAttribute('href', '/datasets/ds-123/documents/create-from-pipeline')
|
||||
})
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/ds-123/documents/create-from-pipeline')
|
||||
it('should open the API reference safely in a new tab', () => {
|
||||
render(<Popup />)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'workflow.common.accessAPIReference' })
|
||||
expect(link).toHaveAttribute('href', '/api/datasets/ds-123')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@ -37,7 +37,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import Link from '@/next/link'
|
||||
import { useParams, useRouter } from '@/next/navigation'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { publishedPipelineInfoQueryKeyPrefix } from '@/service/use-pipeline'
|
||||
@ -68,7 +68,6 @@ export function Popup({
|
||||
select: ({ deployment_edition }) => deployment_edition,
|
||||
})
|
||||
const { datasetId } = useParams()
|
||||
const { push } = useRouter()
|
||||
const publishedAt = useStore((s) => s.publishedAt)
|
||||
const draftUpdatedAt = useStore((s) => s.draftUpdatedAt)
|
||||
const pipelineId = useStore((s) => s.pipelineId)
|
||||
@ -195,11 +194,6 @@ export function Popup({
|
||||
ignoreInputs: true,
|
||||
preventDefault: true,
|
||||
})
|
||||
const goToAddDocuments = useCallback(() => {
|
||||
if (isAddDocumentsDisabled) return
|
||||
|
||||
push(`/datasets/${datasetId}/documents/create-from-pipeline`)
|
||||
}, [datasetId, isAddDocumentsDisabled, push])
|
||||
const handleClickPublishAsKnowledgePipeline = useCallback(() => {
|
||||
onRequestClose?.()
|
||||
if (!isAllowPublishAsCustomKnowledgePipelineTemplate) {
|
||||
@ -263,23 +257,54 @@ export function Popup({
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-t-[0.5px] border-t-divider-regular p-4 pt-3">
|
||||
<Button
|
||||
className="mb-1 w-full hover:bg-state-accent-hover hover:text-text-accent"
|
||||
variant="tertiary"
|
||||
onClick={goToAddDocuments}
|
||||
disabled={isAddDocumentsDisabled}
|
||||
>
|
||||
<div className="flex grow items-center">
|
||||
<RiPlayCircleLine className="mr-2 size-4" />
|
||||
{t(($) => $['common.goToAddDocuments'], { ns: 'pipeline' })}
|
||||
</div>
|
||||
<RiArrowRightUpLine className="size-4 shrink-0" />
|
||||
</Button>
|
||||
<Link href={apiReferenceUrl} target="_blank" rel="noopener noreferrer">
|
||||
{isAddDocumentsDisabled ? (
|
||||
<Button
|
||||
className="mb-1 w-full hover:bg-state-accent-hover hover:text-text-accent"
|
||||
variant="tertiary"
|
||||
disabled
|
||||
>
|
||||
<div className="flex grow items-center">
|
||||
<RiPlayCircleLine className="mr-2 size-4" />
|
||||
{t(($) => $['common.goToAddDocuments'], { ns: 'pipeline' })}
|
||||
</div>
|
||||
<RiArrowRightUpLine className="size-4 shrink-0" />
|
||||
</Button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/datasets/${datasetId}/documents/create-from-pipeline`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'tertiary' }),
|
||||
'mb-1 w-full hover:bg-state-accent-hover hover:text-text-accent',
|
||||
)}
|
||||
>
|
||||
<div className="flex grow items-center">
|
||||
<RiPlayCircleLine className="mr-2 size-4" />
|
||||
{t(($) => $['common.goToAddDocuments'], { ns: 'pipeline' })}
|
||||
</div>
|
||||
<RiArrowRightUpLine className="size-4 shrink-0" />
|
||||
</Link>
|
||||
)}
|
||||
{publishedAt ? (
|
||||
<Link
|
||||
href={apiReferenceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'tertiary' }),
|
||||
'w-full hover:bg-state-accent-hover hover:text-text-accent',
|
||||
)}
|
||||
>
|
||||
<div className="flex grow items-center">
|
||||
<RiTerminalBoxLine className="mr-2 size-4" />
|
||||
{t(($) => $['common.accessAPIReference'], { ns: 'workflow' })}
|
||||
</div>
|
||||
<RiArrowRightUpLine className="size-4 shrink-0" />
|
||||
</Link>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full hover:bg-state-accent-hover hover:text-text-accent"
|
||||
variant="tertiary"
|
||||
disabled={!publishedAt}
|
||||
disabled
|
||||
>
|
||||
<div className="flex grow items-center">
|
||||
<RiTerminalBoxLine className="mr-2 size-4" />
|
||||
@ -287,7 +312,7 @@ export function Popup({
|
||||
</div>
|
||||
<RiArrowRightUpLine className="size-4 shrink-0" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Divider className="my-2" />
|
||||
<Button
|
||||
className="w-full hover:bg-state-accent-hover hover:text-text-accent"
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
@ -336,24 +336,21 @@ const ProviderDetail = ({ collection, onHide, onRefreshData }: Props) => {
|
||||
!isDetailLoading &&
|
||||
customCollection && (
|
||||
<>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
variant="primary"
|
||||
className={cn('my-3 h-8 min-w-0 flex-1 rounded-lg py-2')}
|
||||
render={
|
||||
<a
|
||||
href={`${basePath}/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
aria-label={t(($) => $.openInStudio, { ns: 'tools' })}
|
||||
/>
|
||||
}
|
||||
<a
|
||||
href={`${basePath}/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
aria-label={t(($) => $.openInStudio, { ns: 'tools' })}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'primary' }),
|
||||
'my-3 h-8 min-w-0 flex-1 rounded-lg py-2',
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 truncate system-sm-medium">
|
||||
{t(($) => $.openInStudio, { ns: 'tools' })}
|
||||
</span>
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-4 shrink-0" />
|
||||
</Button>
|
||||
</a>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={cn('my-3 h-8 min-w-0 flex-1 rounded-lg py-2')}
|
||||
|
||||
@ -41,7 +41,7 @@ export function EditInConsoleLink({
|
||||
if (canManageAgents) {
|
||||
return (
|
||||
<Link
|
||||
className={cn(buttonVariants({ className: layoutClassName }))}
|
||||
className={cn(buttonVariants(), layoutClassName)}
|
||||
href={getAgentDetailPath(agentId, 'configure')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@ -233,12 +233,10 @@ const Right = ({ nodeId, currentNodeVar, handleOpenMenu, isValueFetching }: Prop
|
||||
href={fullContent?.download_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'small',
|
||||
className:
|
||||
'size-6 rounded-lg p-0 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
})}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost', size: 'small' }),
|
||||
'size-6 rounded-lg p-0 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-file-download-fill size-4" />
|
||||
</a>
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
@ -36,6 +36,7 @@ import ModelParameterModal from '@/app/components/header/account-setting/model-p
|
||||
import WorkflowPreview from '@/app/components/workflow/workflow-preview'
|
||||
import { WORKFLOW_GENERATION_TIMEOUT_MS } from '@/config'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { generateWorkflow, generateWorkflowStream } from '@/service/workflow-generator'
|
||||
@ -684,16 +685,13 @@ function WorkflowGeneratorModal() {
|
||||
{t(($) => $['workflowGenerator.regenerate'])}
|
||||
</Button>
|
||||
{genErrorHasUnknownTool && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
closeGenerator()
|
||||
router.push('/tools')
|
||||
}}
|
||||
<Link
|
||||
href="/tools"
|
||||
className={buttonVariants({ size: 'small', variant: 'secondary' })}
|
||||
onClick={closeGenerator}
|
||||
>
|
||||
{t(($) => $['workflowGenerator.errors.installTools'])}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,59 +3,44 @@ import { describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
|
||||
import Chooser from '../chooser'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/signin/utils/post-login-redirect', () => ({
|
||||
setPostLoginRedirect: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('Chooser', () => {
|
||||
it('renders account button', () => {
|
||||
it('renders an account sign-in link', () => {
|
||||
render(<Chooser userCode="ABCD-3456" ssoAvailable={false} />)
|
||||
expect(
|
||||
screen.getByRole('button', { name: /deviceFlow.chooser.signInAccount/i }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /deviceFlow.chooser.signInAccount/i })).toHaveAttribute(
|
||||
'href',
|
||||
'/signin',
|
||||
)
|
||||
})
|
||||
|
||||
it('hides SSO button when ssoAvailable is false', () => {
|
||||
render(<Chooser userCode="ABCD-3456" ssoAvailable={false} />)
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /deviceFlow.chooser.signInSSO/i }),
|
||||
screen.queryByRole('link', { name: /deviceFlow.chooser.signInSSO/i }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows SSO button when ssoAvailable is true', () => {
|
||||
render(<Chooser userCode="ABCD-3456" ssoAvailable={true} />)
|
||||
expect(
|
||||
screen.getByRole('button', { name: /deviceFlow.chooser.signInSSO/i }),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('sets post-login redirect and navigates to /signin on account button click', () => {
|
||||
it('sets the post-login redirect when the account link is activated', () => {
|
||||
render(<Chooser userCode="ABCD-3456" ssoAvailable={false} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /deviceFlow.chooser.signInAccount/i }))
|
||||
fireEvent.click(screen.getByRole('link', { name: /deviceFlow.chooser.signInAccount/i }))
|
||||
expect(vi.mocked(setPostLoginRedirect)).toHaveBeenCalledWith('/device?user_code=ABCD-3456')
|
||||
expect(mockPush).toHaveBeenCalledWith('/signin')
|
||||
})
|
||||
|
||||
it('encodes userCode in post-login redirect', () => {
|
||||
// Uses a code with a space to exercise encodeURIComponent
|
||||
render(<Chooser userCode="AB CD" ssoAvailable={false} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /deviceFlow.chooser.signInAccount/i }))
|
||||
fireEvent.click(screen.getByRole('link', { name: /deviceFlow.chooser.signInAccount/i }))
|
||||
expect(vi.mocked(setPostLoginRedirect)).toHaveBeenCalledWith('/device?user_code=AB%20CD')
|
||||
})
|
||||
|
||||
it('navigates to SSO initiate URL on SSO button click', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { href: '' },
|
||||
})
|
||||
it('links to the SSO initiate URL', () => {
|
||||
render(<Chooser userCode="ABCD-3456" ssoAvailable={true} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /deviceFlow.chooser.signInSSO/i }))
|
||||
expect(window.location.href).toBe('/openapi/v1/oauth/device/sso-initiate?user_code=ABCD-3456')
|
||||
expect(screen.getByRole('link', { name: /deviceFlow.chooser.signInSSO/i })).toHaveAttribute(
|
||||
'href',
|
||||
'/openapi/v1/oauth/device/sso-initiate?user_code=ABCD-3456',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import Link from '@/next/link'
|
||||
|
||||
type Props = {
|
||||
userCode: string
|
||||
@ -26,28 +27,26 @@ type Props = {
|
||||
*/
|
||||
const Chooser: FC<Props> = ({ userCode, ssoAvailable }) => {
|
||||
const { t } = useTranslation('deviceFlow')
|
||||
const router = useRouter()
|
||||
|
||||
const onAccount = () => {
|
||||
setPostLoginRedirect(`/device?user_code=${encodeURIComponent(userCode)}`)
|
||||
router.push('/signin')
|
||||
}
|
||||
|
||||
const onSSO = () => {
|
||||
window.location.href = `/openapi/v1/oauth/device/sso-initiate?user_code=${encodeURIComponent(userCode)}`
|
||||
}
|
||||
const deviceReturnPath = `/device?user_code=${encodeURIComponent(userCode)}`
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onAccount}>
|
||||
<Link
|
||||
href="/signin"
|
||||
className={cn(buttonVariants({ variant: 'primary', size: 'large' }), 'w-full')}
|
||||
onClick={() => setPostLoginRedirect(deviceReturnPath)}
|
||||
>
|
||||
<span className="i-ri-user-3-line h-4 w-4" />
|
||||
{t(($) => $['chooser.signInAccount'])}
|
||||
</Button>
|
||||
</Link>
|
||||
{ssoAvailable && (
|
||||
<Button variant="secondary" size="large" className="w-full" onClick={onSSO}>
|
||||
<a
|
||||
href={`/openapi/v1/oauth/device/sso-initiate?user_code=${encodeURIComponent(userCode)}`}
|
||||
className={cn(buttonVariants({ variant: 'secondary', size: 'large' }), 'w-full')}
|
||||
>
|
||||
<span className="i-ri-shield-line h-4 w-4" />
|
||||
{t(($) => $['chooser.signInSSO'])}
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
@ -8,6 +9,7 @@ import Divider from '@/app/components/base/divider'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { usePathname, useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deviceLookup } from '@/service/device-flow'
|
||||
@ -214,9 +216,9 @@ export default function DevicePage() {
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary">{t(($) => $['success.subtitle'])}</p>
|
||||
<Divider className="my-3" />
|
||||
<Button variant="ghost" className="w-full" onClick={() => router.push('/')}>
|
||||
<Link href="/" className={cn(buttonVariants({ variant: 'ghost' }), 'w-full')}>
|
||||
{t(($) => $['success.goToConsole'])}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useCallback, useState } from 'react'
|
||||
@ -91,9 +91,12 @@ const ChangePasswordForm = () => {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button variant="primary" className="w-full text-sm!">
|
||||
<a href="https://dify.ai">{t(($) => $.explore, { ns: 'login' })}</a>
|
||||
</Button>
|
||||
<a
|
||||
href="https://dify.ai"
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-full text-sm!')}
|
||||
>
|
||||
{t(($) => $.explore, { ns: 'login' })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -171,9 +174,12 @@ const ChangePasswordForm = () => {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button variant="primary" className="w-full">
|
||||
<a href={`${basePath}/signin`}>{t(($) => $.passwordChanged, { ns: 'login' })}</a>
|
||||
</Button>
|
||||
<a
|
||||
href={`${basePath}/signin`}
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-full')}
|
||||
>
|
||||
{t(($) => $.passwordChanged, { ns: 'login' })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -8,12 +8,6 @@ import {
|
||||
} from '@/service/common'
|
||||
import ForgotPasswordForm from './ForgotPasswordForm'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
fetchSetupStatus: vi.fn(),
|
||||
fetchInitValidateStatus: vi.fn(),
|
||||
@ -65,7 +59,7 @@ describe('ForgotPasswordForm', () => {
|
||||
expect(mockSendForgotPasswordEmail).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should send reset email and navigate after confirmation', async () => {
|
||||
it('should send the reset email and show a sign-in link after confirmation', async () => {
|
||||
mockSendForgotPasswordEmail.mockResolvedValue({ result: 'success', data: 'ok' } as any)
|
||||
|
||||
render(<ForgotPasswordForm />)
|
||||
@ -83,12 +77,12 @@ describe('ForgotPasswordForm', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /login\.backToSignIn/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /login\.backToSignIn/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/signin',
|
||||
)
|
||||
})
|
||||
expect(mockUseDocumentTitle).toHaveBeenLastCalledWith('login.resetLinkSent')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /login\.backToSignIn/ }))
|
||||
expect(mockPush).toHaveBeenCalledWith('/signin')
|
||||
})
|
||||
|
||||
it('should submit when form is submitted', async () => {
|
||||
@ -139,7 +133,7 @@ describe('ForgotPasswordForm', () => {
|
||||
resolveRequest?.({ result: 'success', data: 'ok' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /login\.backToSignIn/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /login\.backToSignIn/ })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -159,7 +153,6 @@ describe('ForgotPasswordForm', () => {
|
||||
})
|
||||
|
||||
expect(screen.getByRole('button', { name: /login\.sendResetLink/ })).toBeInTheDocument()
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { InitValidateStatusResponse } from '@/models/common'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -9,7 +10,7 @@ import * as z from 'zod'
|
||||
import { formContext, useAppForm } from '@/app/components/base/form'
|
||||
import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import Link from '@/next/link'
|
||||
import {
|
||||
fetchInitValidateStatus,
|
||||
fetchSetupStatus,
|
||||
@ -27,7 +28,6 @@ const accountFormSchema = z.object({
|
||||
|
||||
const ForgotPasswordForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isEmailSent, setIsEmailSent] = useState(false)
|
||||
const documentTitle = loading
|
||||
@ -59,14 +59,10 @@ const ForgotPasswordForm = () => {
|
||||
const isSubmitting = useStore(form.store, (state) => state.isSubmitting)
|
||||
const emailErrors = useStore(form.store, (state) => state.fieldMeta.email?.errors)
|
||||
|
||||
const handleSendResetPasswordClick = async () => {
|
||||
const handleSendResetPasswordClick = () => {
|
||||
if (isSubmitting) return
|
||||
|
||||
if (isEmailSent) {
|
||||
router.push('/signin')
|
||||
} else {
|
||||
form.handleSubmit()
|
||||
}
|
||||
form.handleSubmit()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -134,16 +130,23 @@ const ForgotPasswordForm = () => {
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSendResetPasswordClick}
|
||||
>
|
||||
{isEmailSent
|
||||
? t(($) => $.backToSignIn, { ns: 'login' })
|
||||
: t(($) => $.sendResetLink, { ns: 'login' })}
|
||||
</Button>
|
||||
{isEmailSent ? (
|
||||
<Link
|
||||
href="/signin"
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-full')}
|
||||
>
|
||||
{t(($) => $.backToSignIn, { ns: 'login' })}
|
||||
</Link>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSendResetPasswordClick}
|
||||
>
|
||||
{t(($) => $.sendResetLink, { ns: 'login' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</formContext.Provider>
|
||||
|
||||
@ -21,6 +21,14 @@ vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/link', () => ({
|
||||
default: ({ children, replace, ...props }: React.ComponentProps<'a'> & { replace?: boolean }) => (
|
||||
<a {...props} data-replace={replace || undefined}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
changePasswordWithToken: vi.fn(),
|
||||
}))
|
||||
@ -59,7 +67,7 @@ const completePasswordChange = async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'login.changePasswordBtn' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /login\.passwordChanged/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /login\.passwordChanged/ })).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('login.passwordChangedTip')
|
||||
expect(mockUseDocumentTitle).toHaveBeenLastCalledWith('login.passwordChangedTip')
|
||||
@ -117,9 +125,9 @@ describe('Reset Password Set Password Page', () => {
|
||||
setSearchParams({ token: 'reset-token', redirect_url: redirectUrl })
|
||||
await completePasswordChange()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /login\.passwordChanged/ }))
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith(encodedSigninUrl)
|
||||
const link = screen.getByRole('link', { name: /login\.passwordChanged/ })
|
||||
expect(link).toHaveAttribute('href', encodedSigninUrl)
|
||||
expect(link).toHaveAttribute('data-replace', 'true')
|
||||
})
|
||||
|
||||
it('should preserve redirect_url when the countdown returns to sign in automatically', async () => {
|
||||
@ -140,17 +148,19 @@ describe('Reset Password Set Password Page', () => {
|
||||
})
|
||||
await completePasswordChange()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /login\.passwordChanged/ }))
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith('/activate?token=invite-token')
|
||||
expect(screen.getByRole('link', { name: /login\.passwordChanged/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/activate?token=invite-token',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return to plain sign in when no redirect target is present', async () => {
|
||||
await completePasswordChange()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /login\.passwordChanged/ }))
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith('/signin')
|
||||
expect(screen.getByRole('link', { name: /login\.passwordChanged/ })).toHaveAttribute(
|
||||
'href',
|
||||
'/signin',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Field, FieldDescription, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
@ -12,6 +12,7 @@ import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { validPassword } from '@/config'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { changePasswordWithToken } from '@/service/common'
|
||||
|
||||
@ -195,16 +196,13 @@ const ChangePasswordForm = () => {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setLeftTime(undefined)
|
||||
router.replace(getSignInUrl())
|
||||
}}
|
||||
<Link
|
||||
href={getSignInUrl()}
|
||||
replace
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-full')}
|
||||
>
|
||||
{t(($) => $.passwordChanged, { ns: 'login' })} ({Math.round(countdown / 1000)}){' '}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -27,17 +27,11 @@ export default function SocialAuth() {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
className={buttonVariants({ className: 'w-full' })}
|
||||
href={getOAuthLink('/oauth/login/github')}
|
||||
>
|
||||
<a className={cn(buttonVariants(), 'w-full')} href={getOAuthLink('/oauth/login/github')}>
|
||||
<span aria-hidden="true" className={cn(style.githubIcon, 'size-5')} />
|
||||
<span className="truncate leading-normal">{t(($) => $.withGitHub, { ns: 'login' })}</span>
|
||||
</a>
|
||||
<a
|
||||
className={buttonVariants({ className: 'w-full' })}
|
||||
href={getOAuthLink('/oauth/login/google')}
|
||||
>
|
||||
<a className={cn(buttonVariants(), 'w-full')} href={getOAuthLink('/oauth/login/google')}>
|
||||
<span aria-hidden="true" className={cn(style.googleIcon, 'size-5')} />
|
||||
<span className="truncate leading-normal">{t(($) => $.withGoogle, { ns: 'login' })}</span>
|
||||
</a>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import {
|
||||
Select,
|
||||
@ -197,9 +198,12 @@ export default function InviteSettingsPage() {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mx-auto mt-6 w-full">
|
||||
<Button variant="primary" className="w-full text-sm!">
|
||||
<a href="https://dify.ai">{t(($) => $.explore, { ns: 'login' })}</a>
|
||||
</Button>
|
||||
<a
|
||||
href="https://dify.ai"
|
||||
className={cn(buttonVariants({ variant: 'primary' }), 'w-full text-sm!')}
|
||||
>
|
||||
{t(($) => $.explore, { ns: 'login' })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -443,7 +443,7 @@ describe('Agent access surface cards', () => {
|
||||
expect(dialog).toHaveTextContent(/NEXT_PUBLIC_APP_ID=\s*'app-1'/)
|
||||
expect(dialog).toHaveTextContent(/NEXT_PUBLIC_API_URL=\s*'https:\/\/api\.example\.test\/v1'/)
|
||||
expect(
|
||||
within(dialog).getByRole('button', {
|
||||
within(dialog).getByRole('link', {
|
||||
name: /appOverview\.overview\.appInfo\.customize\.way1\.step1Operation/,
|
||||
}),
|
||||
).toHaveAttribute('href', 'https://github.com/langgenius/webapp-conversation')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user