chore(web): new lint setup (#30020)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou
2025-12-23 16:58:55 +08:00
committed by GitHub
parent 9701a2994b
commit f2842da397
3356 changed files with 85046 additions and 81278 deletions

View File

@ -1,8 +1,16 @@
import type { Mock } from 'vitest'
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { AppModeEnum } from '@/types/app'
import React from 'react'
import { AccessMode } from '@/models/access-control'
// Mock API services - import for direct manipulation
import * as appsService from '@/service/apps'
import * as exploreService from '@/service/explore'
import * as workflowService from '@/service/workflow'
import { AppModeEnum } from '@/types/app'
// Import component after mocks
import AppCard from './app-card'
// Mock next/navigation
const mockPush = vi.fn()
@ -54,11 +62,6 @@ vi.mock('@/context/global-public-context', () => ({
}),
}))
// Mock API services - import for direct manipulation
import * as appsService from '@/service/apps'
import * as workflowService from '@/service/workflow'
import * as exploreService from '@/service/explore'
vi.mock('@/service/apps', () => ({
deleteApp: vi.fn(() => Promise.resolve()),
updateAppInfo: vi.fn(() => Promise.resolve()),
@ -113,74 +116,59 @@ vi.mock('next/dynamic', () => {
if (fnString.includes('create-app-modal') || fnString.includes('explore/create-app-modal')) {
return function MockEditAppModal({ show, onHide, onConfirm }: any) {
if (!show) return null
return React.createElement('div', { 'data-testid': 'edit-app-modal' },
React.createElement('button', { 'onClick': onHide, 'data-testid': 'close-edit-modal' }, 'Close'),
React.createElement('button', {
'onClick': () => onConfirm?.({
name: 'Updated App',
icon_type: 'emoji',
icon: '🎯',
icon_background: '#FFEAD5',
description: 'Updated description',
use_icon_as_answer_icon: false,
max_active_requests: null,
}),
'data-testid': 'confirm-edit-modal',
}, 'Confirm'),
)
if (!show)
return null
return React.createElement('div', { 'data-testid': 'edit-app-modal' }, React.createElement('button', { 'onClick': onHide, 'data-testid': 'close-edit-modal' }, 'Close'), React.createElement('button', {
'onClick': () => onConfirm?.({
name: 'Updated App',
icon_type: 'emoji',
icon: '🎯',
icon_background: '#FFEAD5',
description: 'Updated description',
use_icon_as_answer_icon: false,
max_active_requests: null,
}),
'data-testid': 'confirm-edit-modal',
}, 'Confirm'))
}
}
if (fnString.includes('duplicate-modal')) {
return function MockDuplicateAppModal({ show, onHide, onConfirm }: any) {
if (!show) return null
return React.createElement('div', { 'data-testid': 'duplicate-modal' },
React.createElement('button', { 'onClick': onHide, 'data-testid': 'close-duplicate-modal' }, 'Close'),
React.createElement('button', {
'onClick': () => onConfirm?.({
name: 'Copied App',
icon_type: 'emoji',
icon: '📋',
icon_background: '#E4FBCC',
}),
'data-testid': 'confirm-duplicate-modal',
}, 'Confirm'),
)
if (!show)
return null
return React.createElement('div', { 'data-testid': 'duplicate-modal' }, React.createElement('button', { 'onClick': onHide, 'data-testid': 'close-duplicate-modal' }, 'Close'), React.createElement('button', {
'onClick': () => onConfirm?.({
name: 'Copied App',
icon_type: 'emoji',
icon: '📋',
icon_background: '#E4FBCC',
}),
'data-testid': 'confirm-duplicate-modal',
}, 'Confirm'))
}
}
if (fnString.includes('switch-app-modal')) {
return function MockSwitchAppModal({ show, onClose, onSuccess }: any) {
if (!show) return null
return React.createElement('div', { 'data-testid': 'switch-modal' },
React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-switch-modal' }, 'Close'),
React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'confirm-switch-modal' }, 'Switch'),
)
if (!show)
return null
return React.createElement('div', { 'data-testid': 'switch-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-switch-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'confirm-switch-modal' }, 'Switch'))
}
}
if (fnString.includes('base/confirm')) {
return function MockConfirm({ isShow, onCancel, onConfirm }: any) {
if (!isShow) return null
return React.createElement('div', { 'data-testid': 'confirm-dialog' },
React.createElement('button', { 'onClick': onCancel, 'data-testid': 'cancel-confirm' }, 'Cancel'),
React.createElement('button', { 'onClick': onConfirm, 'data-testid': 'confirm-confirm' }, 'Confirm'),
)
if (!isShow)
return null
return React.createElement('div', { 'data-testid': 'confirm-dialog' }, React.createElement('button', { 'onClick': onCancel, 'data-testid': 'cancel-confirm' }, 'Cancel'), React.createElement('button', { 'onClick': onConfirm, 'data-testid': 'confirm-confirm' }, 'Confirm'))
}
}
if (fnString.includes('dsl-export-confirm-modal')) {
return function MockDSLExportModal({ onClose, onConfirm }: any) {
return React.createElement('div', { 'data-testid': 'dsl-export-modal' },
React.createElement('button', { 'onClick': () => onClose?.(), 'data-testid': 'close-dsl-export' }, 'Close'),
React.createElement('button', { 'onClick': () => onConfirm?.(true), 'data-testid': 'confirm-dsl-export' }, 'Export with secrets'),
React.createElement('button', { 'onClick': () => onConfirm?.(false), 'data-testid': 'confirm-dsl-export-no-secrets' }, 'Export without secrets'),
)
return React.createElement('div', { 'data-testid': 'dsl-export-modal' }, React.createElement('button', { 'onClick': () => onClose?.(), 'data-testid': 'close-dsl-export' }, 'Close'), React.createElement('button', { 'onClick': () => onConfirm?.(true), 'data-testid': 'confirm-dsl-export' }, 'Export with secrets'), React.createElement('button', { 'onClick': () => onConfirm?.(false), 'data-testid': 'confirm-dsl-export-no-secrets' }, 'Export without secrets'))
}
}
if (fnString.includes('app-access-control')) {
return function MockAccessControl({ onClose, onConfirm }: any) {
return React.createElement('div', { 'data-testid': 'access-control-modal' },
React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-access-control' }, 'Close'),
React.createElement('button', { 'onClick': onConfirm, 'data-testid': 'confirm-access-control' }, 'Confirm'),
)
return React.createElement('div', { 'data-testid': 'access-control-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-access-control' }, 'Close'), React.createElement('button', { 'onClick': onConfirm, 'data-testid': 'confirm-access-control' }, 'Confirm'))
}
}
return () => null
@ -193,18 +181,13 @@ vi.mock('@/app/components/base/popover', () => {
const MockPopover = ({ htmlContent, btnElement, btnClassName }: any) => {
const [isOpen, setIsOpen] = React.useState(false)
const computedClassName = typeof btnClassName === 'function' ? btnClassName(isOpen) : ''
return React.createElement('div', { 'data-testid': 'custom-popover', 'className': computedClassName },
React.createElement('div', {
'onClick': () => setIsOpen(!isOpen),
'data-testid': 'popover-trigger',
}, btnElement),
isOpen && React.createElement('div', {
'data-testid': 'popover-content',
'onMouseLeave': () => setIsOpen(false),
},
typeof htmlContent === 'function' ? htmlContent({ open: isOpen, onClose: () => setIsOpen(false), onClick: () => setIsOpen(false) }) : htmlContent,
),
)
return React.createElement('div', { 'data-testid': 'custom-popover', 'className': computedClassName }, React.createElement('div', {
'onClick': () => setIsOpen(!isOpen),
'data-testid': 'popover-trigger',
}, btnElement), isOpen && React.createElement('div', {
'data-testid': 'popover-content',
'onMouseLeave': () => setIsOpen(false),
}, typeof htmlContent === 'function' ? htmlContent({ open: isOpen, onClose: () => setIsOpen(false), onClick: () => setIsOpen(false) }) : htmlContent))
}
return { __esModule: true, default: MockPopover }
})
@ -220,9 +203,7 @@ vi.mock('@/app/components/base/tag-management/selector', () => ({
__esModule: true,
default: ({ tags }: any) => {
const React = require('react')
return React.createElement('div', { 'aria-label': 'tag-selector' },
tags?.map((tag: any) => React.createElement('span', { key: tag.id }, tag.name)),
)
return React.createElement('div', { 'aria-label': 'tag-selector' }, tags?.map((tag: any) => React.createElement('span', { key: tag.id }, tag.name)))
},
}))
@ -231,9 +212,6 @@ vi.mock('@/app/components/app/type-selector', () => ({
AppTypeIcon: () => React.createElement('div', { 'data-testid': 'app-type-icon' }),
}))
// Import component after mocks
import AppCard from './app-card'
// ============================================================================
// Test Data Factories
// ============================================================================

View File

@ -1,38 +1,39 @@
'use client'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
import { cn } from '@/utils/classnames'
import { type App, AppModeEnum } from '@/types/app'
import Toast, { ToastContext } from '@/app/components/base/toast'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import AppIcon from '@/app/components/base/app-icon'
import { useAppContext } from '@/context/app-context'
import type { HtmlContentProps } from '@/app/components/base/popover'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
import { basePath } from '@/utils/var'
import { getRedirection } from '@/utils/app-redirection'
import { useProviderContext } from '@/context/provider-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import type { Tag } from '@/app/components/base/tag-management/constant'
import TagSelector from '@/app/components/base/tag-management/selector'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { fetchWorkflowDraft } from '@/service/workflow'
import { fetchInstalledAppList } from '@/service/explore'
import type { App } from '@/types/app'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/navigation'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import AppIcon from '@/app/components/base/app-icon'
import Divider from '@/app/components/base/divider'
import CustomPopover from '@/app/components/base/popover'
import TagSelector from '@/app/components/base/tag-management/selector'
import Toast, { ToastContext } from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { formatTime } from '@/utils/time'
import { useGetUserCanAccessApp } from '@/service/access-control'
import dynamic from 'next/dynamic'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import { fetchInstalledAppList } from '@/service/explore'
import { fetchWorkflowDraft } from '@/service/workflow'
import { AppModeEnum } from '@/types/app'
import { getRedirection } from '@/utils/app-redirection'
import { cn } from '@/utils/classnames'
import { formatTime } from '@/utils/time'
import { basePath } from '@/utils/var'
const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), {
ssr: false,
@ -266,62 +267,66 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
}
return (
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickSettings}>
<span className='system-sm-regular text-text-secondary'>{t('app.editApp')}</span>
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickSettings}>
<span className="system-sm-regular text-text-secondary">{t('app.editApp')}</span>
</button>
<Divider className="my-1" />
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickDuplicate}>
<span className='system-sm-regular text-text-secondary'>{t('app.duplicate')}</span>
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickDuplicate}>
<span className="system-sm-regular text-text-secondary">{t('app.duplicate')}</span>
</button>
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickExport}>
<span className='system-sm-regular text-text-secondary'>{t('app.export')}</span>
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickExport}>
<span className="system-sm-regular text-text-secondary">{t('app.export')}</span>
</button>
{(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && (
<>
<Divider className="my-1" />
<button
type="button"
className='mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover'
className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
onClick={onClickSwitch}
>
<span className='text-sm leading-5 text-text-secondary'>{t('app.switch')}</span>
<span className="text-sm leading-5 text-text-secondary">{t('app.switch')}</span>
</button>
</>
)}
{
!app.has_draft_trigger && (
(!systemFeatures.webapp_auth.enabled)
? <>
<Divider className="my-1" />
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
</>
? (
<>
<Divider className="my-1" />
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
<span className="system-sm-regular text-text-secondary">{t('app.openInExplore')}</span>
</button>
</>
)
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
<>
<Divider className="my-1" />
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
</>
)
<>
<Divider className="my-1" />
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
<span className="system-sm-regular text-text-secondary">{t('app.openInExplore')}</span>
</button>
</>
)
)
}
<Divider className="my-1" />
{
systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && <>
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickAccessControl}>
<span className='text-sm leading-5 text-text-secondary'>{t('app.accessControl')}</span>
</button>
<Divider className='my-1' />
</>
systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && (
<>
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickAccessControl}>
<span className="text-sm leading-5 text-text-secondary">{t('app.accessControl')}</span>
</button>
<Divider className="my-1" />
</>
)
}
<button
type="button"
className='group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover'
className="group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover"
onClick={onClickDelete}
>
<span className='system-sm-regular text-text-secondary group-hover:text-text-destructive'>
<span className="system-sm-regular text-text-secondary group-hover:text-text-destructive">
{t('common.operation.delete')}
</span>
</button>
@ -349,10 +354,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault()
getRedirection(isCurrentWorkspaceEditor, app, push)
}}
className='group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border-[1px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border-[1px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg"
>
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
<div className='relative shrink-0'>
<div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]">
<div className="relative shrink-0">
<AppIcon
size="large"
iconType={app.icon_type}
@ -360,52 +365,63 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
background={app.icon_background}
imageUrl={app.icon_url}
/>
<AppTypeIcon type={app.mode} wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm' className='h-3 w-3' />
<AppTypeIcon type={app.mode} wrapperClassName="absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm" className="h-3 w-3" />
</div>
<div className='w-0 grow py-[1px]'>
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className='truncate' title={app.name}>{app.name}</div>
<div className="w-0 grow py-[1px]">
<div className="flex items-center text-sm font-semibold leading-5 text-text-secondary">
<div className="truncate" title={app.name}>{app.name}</div>
</div>
<div className='flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary'>
<div className='truncate' title={app.author_name}>{app.author_name}</div>
<div className="flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary">
<div className="truncate" title={app.author_name}>{app.author_name}</div>
<div>·</div>
<div className='truncate' title={EditTimeText}>{EditTimeText}</div>
<div className="truncate" title={EditTimeText}>{EditTimeText}</div>
</div>
</div>
<div className='flex h-5 w-5 shrink-0 items-center justify-center'>
{app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}>
<RiGlobalLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>}
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}>
<RiLockLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>}
{app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}>
<RiBuildingLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>}
{app.access_mode === AccessMode.EXTERNAL_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.external')}>
<RiVerifiedBadgeLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>}
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
{app.access_mode === AccessMode.PUBLIC && (
<Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}>
<RiGlobalLine className="h-4 w-4 text-text-quaternary" />
</Tooltip>
)}
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && (
<Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}>
<RiLockLine className="h-4 w-4 text-text-quaternary" />
</Tooltip>
)}
{app.access_mode === AccessMode.ORGANIZATION && (
<Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}>
<RiBuildingLine className="h-4 w-4 text-text-quaternary" />
</Tooltip>
)}
{app.access_mode === AccessMode.EXTERNAL_MEMBERS && (
<Tooltip asChild={false} popupContent={t('app.accessItemsDescription.external')}>
<RiVerifiedBadgeLine className="h-4 w-4 text-text-quaternary" />
</Tooltip>
)}
</div>
</div>
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
<div className="title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary">
<div
className='line-clamp-2'
className="line-clamp-2"
title={app.description}
>
{app.description}
</div>
</div>
<div className='absolute bottom-1 left-0 right-0 flex h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1'>
<div className="absolute bottom-1 left-0 right-0 flex h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1">
{isCurrentWorkspaceEditor && (
<>
<div className={cn('flex w-0 grow items-center gap-1')} onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}>
<div className='mr-[41px] w-full grow group-hover:!mr-0'>
<div
className={cn('flex w-0 grow items-center gap-1')}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
>
<div className="mr-[41px] w-full grow group-hover:!mr-0">
<TagSelector
position='bl'
type='app'
position="bl"
type="app"
targetID={app.id}
value={tags.map(tag => tag.id)}
selectedTags={tags}
@ -414,31 +430,30 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
/>
</div>
</div>
<div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex' />
<div className='!hidden shrink-0 group-hover:!flex'>
<div className="mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex" />
<div className="!hidden shrink-0 group-hover:!flex">
<CustomPopover
htmlContent={<Operations />}
position="br"
trigger="click"
btnElement={
btnElement={(
<div
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-md'
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md"
>
<RiMoreFill className='h-4 w-4 text-text-tertiary' />
<RiMoreFill className="h-4 w-4 text-text-tertiary" />
</div>
}
)}
btnClassName={open =>
cn(
open ? '!bg-state-base-hover !shadow-none' : '!bg-transparent',
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-state-base-hover',
)
}
)}
popupClassName={
(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT)
? '!w-[256px] translate-x-[-224px]'
: '!w-[216px] translate-x-[-128px]'
}
className={'!z-20 h-fit'}
className="!z-20 h-fit"
/>
</div>
</>

View File

@ -1,5 +1,5 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import React from 'react'
import Empty from './empty'
describe('Empty', () => {

View File

@ -9,7 +9,7 @@ const DefaultCards = React.memo(() => {
renderArray.map((_, index) => (
<div
key={index}
className='inline-flex h-[160px] rounded-xl bg-background-default-lighter'
className="inline-flex h-[160px] rounded-xl bg-background-default-lighter"
/>
))
}
@ -23,8 +23,8 @@ const Empty = () => {
return (
<>
<DefaultCards />
<div className='pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
<span className='system-md-medium text-text-tertiary'>
<div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent">
<span className="system-md-medium text-text-tertiary">
{t('app.newApp.noAppsFound')}
</span>
</div>

View File

@ -1,5 +1,5 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import React from 'react'
import Footer from './footer'
describe('Footer', () => {

View File

@ -1,6 +1,6 @@
import React from 'react'
import Link from 'next/link'
import { RiDiscordFill, RiDiscussLine, RiGithubFill } from '@remixicon/react'
import Link from 'next/link'
import React from 'react'
import { useTranslation } from 'react-i18next'
type CustomLinkProps = {
@ -14,9 +14,9 @@ const CustomLink = React.memo(({
}: CustomLinkProps) => {
return (
<Link
className='flex h-8 w-8 cursor-pointer items-center justify-center transition-opacity duration-200 ease-in-out hover:opacity-80'
target='_blank'
rel='noopener noreferrer'
className="flex h-8 w-8 cursor-pointer items-center justify-center transition-opacity duration-200 ease-in-out hover:opacity-80"
target="_blank"
rel="noopener noreferrer"
href={href}
>
{children}
@ -28,18 +28,18 @@ const Footer = () => {
const { t } = useTranslation()
return (
<footer className='relative shrink-0 grow-0 px-12 py-2'>
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3>
<p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p>
<div className='mt-3 flex items-center gap-2'>
<CustomLink href='https://github.com/langgenius/dify'>
<RiGithubFill className='h-5 w-5 text-text-tertiary' />
<footer className="relative shrink-0 grow-0 px-12 py-2">
<h3 className="text-gradient text-xl font-semibold leading-tight">{t('app.join')}</h3>
<p className="system-sm-regular mt-1 text-text-tertiary">{t('app.communityIntro')}</p>
<div className="mt-3 flex items-center gap-2">
<CustomLink href="https://github.com/langgenius/dify">
<RiGithubFill className="h-5 w-5 text-text-tertiary" />
</CustomLink>
<CustomLink href='https://discord.gg/FngNHpbcY7'>
<RiDiscordFill className='h-5 w-5 text-text-tertiary' />
<CustomLink href="https://discord.gg/FngNHpbcY7">
<RiDiscordFill className="h-5 w-5 text-text-tertiary" />
</CustomLink>
<CustomLink href='https://forum.dify.ai'>
<RiDiscussLine className='h-5 w-5 text-text-tertiary' />
<CustomLink href="https://forum.dify.ai">
<RiDiscussLine className="h-5 w-5 text-text-tertiary" />
</CustomLink>
</div>
</footer>

View File

@ -11,6 +11,9 @@
*/
import { act, renderHook } from '@testing-library/react'
// Import the hook after mocks are set up
import useAppsQueryState from './use-apps-query-state'
// Mock Next.js navigation hooks
const mockPush = vi.fn()
const mockPathname = '/apps'
@ -24,9 +27,6 @@ vi.mock('next/navigation', () => ({
useSearchParams: vi.fn(() => mockSearchParams),
}))
// Import the hook after mocks are set up
import useAppsQueryState from './use-apps-query-state'
describe('useAppsQueryState', () => {
beforeEach(() => {
vi.clearAllMocks()

View File

@ -1,4 +1,5 @@
import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
import type { ReadonlyURLSearchParams } from 'next/navigation'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from 'react'
type AppsQuery = {

View File

@ -1,5 +1,8 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import React from 'react'
// Import after mocks
import Apps from './index'
// Track mock calls
let documentTitleCalls: string[] = []
@ -29,9 +32,6 @@ vi.mock('./list', () => ({
},
}))
// Import after mocks
import Apps from './index'
describe('Apps', () => {
beforeEach(() => {
vi.clearAllMocks()

View File

@ -1,8 +1,8 @@
'use client'
import { useEducationInit } from '@/app/education-apply/hooks'
import List from './list'
import useDocumentTitle from '@/hooks/use-document-title'
import { useTranslation } from 'react-i18next'
import { useEducationInit } from '@/app/education-apply/hooks'
import useDocumentTitle from '@/hooks/use-document-title'
import List from './list'
const Apps = () => {
const { t } = useTranslation()
@ -11,9 +11,9 @@ const Apps = () => {
useEducationInit()
return (
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
<div className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
<List />
</div >
</div>
)
}

View File

@ -1,7 +1,10 @@
import React from 'react'
import { act, fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import { AppModeEnum } from '@/types/app'
// Import after mocks
import List from './list'
// Mock next/navigation
const mockReplace = vi.fn()
const mockRouter = { replace: mockReplace }
@ -117,7 +120,7 @@ vi.mock('@/service/use-apps', () => ({
// Mock tag store
vi.mock('@/app/components/base/tag-management/store', () => ({
useStore: (selector: (state: { tagList: any[]; setTagList: any; showTagManagementModal: boolean; setShowTagManagementModal: any }) => any) => {
useStore: (selector: (state: { tagList: any[], setTagList: any, showTagManagementModal: boolean, setShowTagManagementModal: any }) => any) => {
const state = {
tagList: [{ id: 'tag-1', name: 'Test Tag', type: 'app' }],
setTagList: vi.fn(),
@ -181,11 +184,9 @@ vi.mock('next/dynamic', () => {
}
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
if (!show) return null
return React.createElement('div', { 'data-testid': 'create-dsl-modal' },
React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-dsl-modal' }, 'Close'),
React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-dsl-modal' }, 'Success'),
)
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-dsl-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-dsl-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-dsl-modal' }, 'Success'))
}
}
return () => null
@ -231,9 +232,6 @@ vi.mock('./footer', () => ({
},
}))
// Import after mocks
import List from './list'
// Store IntersectionObserver callback
let intersectionCallback: IntersectionObserverCallback | null = null
const mockObserve = vi.fn()

View File

@ -1,11 +1,5 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
useRouter,
} from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import {
RiApps2Line,
RiDragDropLine,
@ -14,25 +8,31 @@ import {
RiMessage3Line,
RiRobot3Line,
} from '@remixicon/react'
import AppCard from './app-card'
import NewAppCard from './new-app-card'
import useAppsQueryState from './hooks/use-apps-query-state'
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
import { useAppContext } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import Input from '@/app/components/base/input'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import TagFilter from '@/app/components/base/tag-management/filter'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import { useDebounceFn } from 'ahooks'
import dynamic from 'next/dynamic'
import {
useRouter,
} from 'next/navigation'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import TagFilter from '@/app/components/base/tag-management/filter'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { CheckModal } from '@/hooks/use-pay'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useInfiniteAppList } from '@/service/use-apps'
import { AppModeEnum } from '@/types/app'
import AppCard from './app-card'
import Empty from './empty'
import Footer from './footer'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AppModeEnum } from '@/types/app'
import { useInfiniteAppList } from '@/service/use-apps'
import useAppsQueryState from './hooks/use-apps-query-state'
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
import NewAppCard from './new-app-card'
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
ssr: false,
@ -97,12 +97,12 @@ const List = () => {
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: AppModeEnum.WORKFLOW, text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: AppModeEnum.ADVANCED_CHAT, text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: AppModeEnum.CHAT, text: t('app.types.chatbot'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: AppModeEnum.AGENT_CHAT, text: t('app.types.agent'), icon: <RiRobot3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: AppModeEnum.COMPLETION, text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className="mr-1 h-[14px] w-[14px]" /> },
{ value: AppModeEnum.WORKFLOW, text: t('app.types.workflow'), icon: <RiExchange2Line className="mr-1 h-[14px] w-[14px]" /> },
{ value: AppModeEnum.ADVANCED_CHAT, text: t('app.types.advanced'), icon: <RiMessage3Line className="mr-1 h-[14px] w-[14px]" /> },
{ value: AppModeEnum.CHAT, text: t('app.types.chatbot'), icon: <RiMessage3Line className="mr-1 h-[14px] w-[14px]" /> },
{ value: AppModeEnum.AGENT_CHAT, text: t('app.types.agent'), icon: <RiRobot3Line className="mr-1 h-[14px] w-[14px]" /> },
{ value: AppModeEnum.COMPLETION, text: t('app.types.completion'), icon: <RiFile4Line className="mr-1 h-[14px] w-[14px]" /> },
]
useEffect(() => {
@ -174,30 +174,30 @@ const List = () => {
return (
<>
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
<div ref={containerRef} className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
{dragging && (
<div className="absolute inset-0 z-50 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent bg-[rgba(21,90,239,0.14)] p-2">
</div>
)}
<div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-5 pt-7'>
<div className="sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-5 pt-7">
<TabSliderNew
value={activeTab}
onChange={setActiveTab}
options={options}
/>
<div className='flex items-center gap-2'>
<div className="flex items-center gap-2">
<CheckboxWithLabel
className='mr-2'
className="mr-2"
label={t('app.showMyCreatedAppsOnly')}
isChecked={isCreatedByMe}
onChange={handleCreatedByMeChange}
/>
<TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
<TagFilter type="app" value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px]'
wrapperClassName="w-[200px]"
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
@ -205,18 +205,22 @@ const List = () => {
</div>
</div>
{hasAnyApp
? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
{pages.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={refetch} />
)))}
</div>
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={refetch} selectedAppType={activeTab} />}
<Empty />
</div>}
? (
<div className="relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6">
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
{pages.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={refetch} />
)))}
</div>
)
: (
<div className="relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6">
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} className="z-10" onSuccess={refetch} selectedAppType={activeTab} />}
<Empty />
</div>
)}
{isCurrentWorkspaceEditor && (
<div
@ -232,9 +236,9 @@ const List = () => {
<Footer />
)}
<CheckModal />
<div ref={anchorRef} className='h-0'> </div>
<div ref={anchorRef} className="h-0"> </div>
{showTagManagementModal && (
<TagManagementModal type='app' show={showTagManagementModal} />
<TagManagementModal type="app" show={showTagManagementModal} />
)}
</div>

View File

@ -1,5 +1,8 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
// Import after mocks
import CreateAppCard from './new-app-card'
// Mock next/navigation
const mockReplace = vi.fn()
@ -27,31 +30,23 @@ vi.mock('next/dynamic', () => {
if (fnString.includes('create-app-modal') && !fnString.includes('create-from-dsl-modal')) {
return function MockCreateAppModal({ show, onClose, onSuccess, onCreateFromTemplate }: any) {
if (!show) return null
return React.createElement('div', { 'data-testid': 'create-app-modal' },
React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-create-modal' }, 'Close'),
React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-create-modal' }, 'Success'),
React.createElement('button', { 'onClick': onCreateFromTemplate, 'data-testid': 'to-template-modal' }, 'To Template'),
)
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-app-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-create-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-create-modal' }, 'Success'), React.createElement('button', { 'onClick': onCreateFromTemplate, 'data-testid': 'to-template-modal' }, 'To Template'))
}
}
if (fnString.includes('create-app-dialog')) {
return function MockCreateAppTemplateDialog({ show, onClose, onSuccess, onCreateFromBlank }: any) {
if (!show) return null
return React.createElement('div', { 'data-testid': 'create-template-dialog' },
React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-template-dialog' }, 'Close'),
React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-template-dialog' }, 'Success'),
React.createElement('button', { 'onClick': onCreateFromBlank, 'data-testid': 'to-blank-modal' }, 'To Blank'),
)
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-template-dialog' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-template-dialog' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-template-dialog' }, 'Success'), React.createElement('button', { 'onClick': onCreateFromBlank, 'data-testid': 'to-blank-modal' }, 'To Blank'))
}
}
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
if (!show) return null
return React.createElement('div', { 'data-testid': 'create-dsl-modal' },
React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-dsl-modal' }, 'Close'),
React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-dsl-modal' }, 'Success'),
)
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-dsl-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-dsl-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-dsl-modal' }, 'Success'))
}
}
return () => null
@ -66,9 +61,6 @@ vi.mock('@/app/components/app/create-from-dsl-modal', () => ({
},
}))
// Import after mocks
import CreateAppCard from './new-app-card'
describe('CreateAppCard', () => {
const defaultRef = { current: null } as React.RefObject<HTMLDivElement | null>

View File

@ -1,16 +1,16 @@
'use client'
import React, { useMemo, useState } from 'react'
import dynamic from 'next/dynamic'
import {
useRouter,
useSearchParams,
} from 'next/navigation'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
import { useProviderContext } from '@/context/provider-context'
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
import dynamic from 'next/dynamic'
const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), {
ssr: false,
@ -57,21 +57,22 @@ const CreateAppCard = ({
ref={ref}
className={cn('relative col-span-1 inline-flex h-[160px] flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg', className)}
>
<div className='grow rounded-t-xl p-2'>
<div className='px-6 pb-1 pt-2 text-xs font-medium leading-[18px] text-text-tertiary'>{t('app.createApp')}</div>
<button type="button" className='mb-1 flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' onClick={() => setShowNewAppModal(true)}>
<FilePlus01 className='mr-2 h-4 w-4 shrink-0' />
<div className="grow rounded-t-xl p-2">
<div className="px-6 pb-1 pt-2 text-xs font-medium leading-[18px] text-text-tertiary">{t('app.createApp')}</div>
<button type="button" className="mb-1 flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" onClick={() => setShowNewAppModal(true)}>
<FilePlus01 className="mr-2 h-4 w-4 shrink-0" />
{t('app.newApp.startFromBlank')}
</button>
<button type="button" className='flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' onClick={() => setShowNewAppTemplateDialog(true)}>
<FilePlus02 className='mr-2 h-4 w-4 shrink-0' />
<button type="button" className="flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" onClick={() => setShowNewAppTemplateDialog(true)}>
<FilePlus02 className="mr-2 h-4 w-4 shrink-0" />
{t('app.newApp.startFromTemplate')}
</button>
<button
type="button"
onClick={() => setShowCreateFromDSLModal(true)}
className='flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'>
<FileArrow01 className='mr-2 h-4 w-4 shrink-0' />
className="flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
>
<FileArrow01 className="mr-2 h-4 w-4 shrink-0" />
{t('app.importDSL')}
</button>
</div>