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
co-authored by yyh
parent 9701a2994b
commit f2842da397
3356 changed files with 85039 additions and 81271 deletions
@@ -1,8 +1,9 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import AppCard, { type AppCardProps } from './index'
import type { AppCardProps } from './index'
import type { App } from '@/models/explore'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import { AppModeEnum } from '@/types/app'
import AppCard from './index'
vi.mock('../../app/type-selector', () => ({
AppTypeIcon: ({ type }: any) => <div data-testid="app-type-icon">{type}</div>,
+27 -23
View File
@@ -1,12 +1,13 @@
'use client'
import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/20/solid'
import Button from '../../base/button'
import { cn } from '@/utils/classnames'
import type { App } from '@/models/explore'
import { PlusIcon } from '@heroicons/react/20/solid'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { AppTypeIcon } from '../../app/type-selector'
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
import { AppTypeIcon } from '../../app/type-selector'
import Button from '../../base/button'
export type AppCardProps = {
app: App
canCreate: boolean
@@ -24,42 +25,45 @@ const AppCard = ({
const { app: appBasicInfo } = app
return (
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 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'
size="large"
iconType={appBasicInfo.icon_type}
icon={appBasicInfo.icon}
background={appBasicInfo.icon_background}
imageUrl={appBasicInfo.icon_url}
/>
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm'
className='h-3 w-3' type={appBasicInfo.mode} />
<AppTypeIcon
wrapperClassName="absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm"
className="h-3 w-3"
type={appBasicInfo.mode}
/>
</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={appBasicInfo.name}>{appBasicInfo.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={appBasicInfo.name}>{appBasicInfo.name}</div>
</div>
<div className='flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary'>
{appBasicInfo.mode === AppModeEnum.ADVANCED_CHAT && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.CHAT && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.AGENT_CHAT && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.WORKFLOW && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.COMPLETION && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
<div className="flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary">
{appBasicInfo.mode === AppModeEnum.ADVANCED_CHAT && <div className="truncate">{t('app.types.advanced').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.CHAT && <div className="truncate">{t('app.types.chatbot').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.AGENT_CHAT && <div className="truncate">{t('app.types.agent').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.WORKFLOW && <div className="truncate">{t('app.types.workflow').toUpperCase()}</div>}
{appBasicInfo.mode === AppModeEnum.COMPLETION && <div className="truncate">{t('app.types.completion').toUpperCase()}</div>}
</div>
</div>
</div>
<div className="description-wrapper system-xs-regular h-[90px] px-[14px] text-text-tertiary">
<div className='line-clamp-4 group-hover:line-clamp-2'>
<div className="line-clamp-4 group-hover:line-clamp-2">
{app.description}
</div>
</div>
{isExplore && canCreate && (
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
<div className={cn('flex h-8 w-full items-center space-x-2')}>
<Button variant='primary' className='h-7 grow' onClick={() => onCreate()}>
<PlusIcon className='mr-1 h-4 w-4' />
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
<Button variant="primary" className="h-7 grow" onClick={() => onCreate()}>
<PlusIcon className="mr-1 h-4 w-4" />
<span className="text-xs">{t('explore.appCard.addToWorkspace')}</span>
</Button>
</div>
</div>
+26 -22
View File
@@ -1,27 +1,27 @@
'use client'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import type { App } from '@/models/explore'
import { useDebounceFn } from 'ahooks'
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import useSWR from 'swr'
import { useDebounceFn } from 'ahooks'
import s from './style.module.css'
import { cn } from '@/utils/classnames'
import ExploreContext from '@/context/explore-context'
import type { App } from '@/models/explore'
import Category from '@/app/components/explore/category'
import AppCard from '@/app/components/explore/app-card'
import { fetchAppDetail, fetchAppList } from '@/service/explore'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import Loading from '@/app/components/base/loading'
import { useContext } from 'use-context-selector'
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
import Input from '@/app/components/base/input'
import Loading from '@/app/components/base/loading'
import AppCard from '@/app/components/explore/app-card'
import Category from '@/app/components/explore/category'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import ExploreContext from '@/context/explore-context'
import { useImportDSL } from '@/hooks/use-import-dsl'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import {
DSLImportMode,
} from '@/models/app'
import { useImportDSL } from '@/hooks/use-import-dsl'
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
import { fetchAppDetail, fetchAppList } from '@/service/explore'
import { cn } from '@/utils/classnames'
import s from './style.module.css'
type AppsProps = {
onSuccess?: () => void
@@ -142,16 +142,18 @@ const Apps = ({
return (
<div className={cn(
'flex h-full flex-col border-l-[0.5px] border-divider-regular',
)}>
)}
>
<div className='shrink-0 px-12 pt-6'>
<div className="shrink-0 px-12 pt-6">
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div>
<div className='text-sm text-text-tertiary'>{t('explore.apps.description')}</div>
<div className="text-sm text-text-tertiary">{t('explore.apps.description')}</div>
</div>
<div className={cn(
'mt-6 flex items-center justify-between px-12',
)}>
)}
>
<Category
list={categories}
value={currCategory}
@@ -161,7 +163,7 @@ const Apps = ({
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px] self-start'
wrapperClassName="w-[200px] self-start"
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
@@ -170,12 +172,14 @@ const Apps = ({
<div className={cn(
'relative mt-4 flex flex-1 shrink-0 grow flex-col overflow-auto pb-6',
)}>
)}
>
<nav
className={cn(
s.appList,
'grid shrink-0 content-start gap-4 px-6 sm:px-12',
)}>
)}
>
{searchFilteredList.map(app => (
<AppCard
key={app.app_id}
+4 -4
View File
@@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import type { AppCategory } from '@/models/explore'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import exploreI18n from '@/i18n/en-US/explore'
import type { AppCategory } from '@/models/explore'
import { ThumbsUp } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import exploreI18n from '@/i18n/en-US/explore'
import { cn } from '@/utils/classnames'
const categoryI18n = exploreI18n.category
@@ -41,7 +41,7 @@ const Category: FC<ICategoryProps> = ({
className={itemClassName(isAllCategories)}
onClick={() => onChange(allCategoriesEn)}
>
<ThumbsUp className='mr-1 h-3.5 w-3.5' />
<ThumbsUp className="mr-1 h-3.5 w-3.5" />
{t('explore.apps.allCategories')}
</div>
{list.filter(name => name !== allCategoriesEn).map(name => (
@@ -1,11 +1,11 @@
import React from 'react'
import { act, fireEvent, render, screen } from '@testing-library/react'
import type { CreateAppModalProps } from './index'
import type { UsagePlanInfo } from '@/app/components/billing/type'
import { Plan } from '@/app/components/billing/type'
import { act, fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import { createMockPlan, createMockPlanTotal, createMockPlanUsage } from '@/__mocks__/provider-context'
import { Plan } from '@/app/components/billing/type'
import { AppModeEnum } from '@/types/app'
import CreateAppModal from './index'
import type { CreateAppModalProps } from './index'
let mockTranslationOverrides: Record<string, string | undefined> = {}
@@ -131,7 +131,7 @@ describe('CreateAppModal', () => {
// The title and form sections vary based on the modal mode (create vs edit).
describe('Rendering', () => {
test('should render create title and actions when creating', () => {
it('should render create title and actions when creating', () => {
setup({ appName: 'My App', isEditModal: false })
expect(screen.getByText('explore.appCustomize.title:{"name":"My App"}')).toBeInTheDocument()
@@ -139,7 +139,7 @@ describe('CreateAppModal', () => {
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
})
test('should render edit-only fields when editing a chat app', () => {
it('should render edit-only fields when editing a chat app', () => {
setup({ isEditModal: true, appMode: AppModeEnum.CHAT, max_active_requests: 5 })
expect(screen.getByText('app.editAppTitle')).toBeInTheDocument()
@@ -148,19 +148,19 @@ describe('CreateAppModal', () => {
expect((screen.getByRole('spinbutton') as HTMLInputElement).value).toBe('5')
})
test.each([AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT])('should render answer icon switch when editing %s app', (mode) => {
it.each([AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT])('should render answer icon switch when editing %s app', (mode) => {
setup({ isEditModal: true, appMode: mode })
expect(screen.getByRole('switch')).toBeInTheDocument()
})
test('should not render answer icon switch when editing a non-chat app', () => {
it('should not render answer icon switch when editing a non-chat app', () => {
setup({ isEditModal: true, appMode: AppModeEnum.COMPLETION })
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
})
test('should not render modal content when hidden', () => {
it('should not render modal content when hidden', () => {
setup({ show: false })
expect(screen.queryByRole('button', { name: 'common.operation.create' })).not.toBeInTheDocument()
@@ -169,13 +169,13 @@ describe('CreateAppModal', () => {
// Disabled states prevent submission and reflect parent-driven props.
describe('Props', () => {
test('should disable confirm action when confirmDisabled is true', () => {
it('should disable confirm action when confirmDisabled is true', () => {
setup({ confirmDisabled: true })
expect(screen.getByRole('button', { name: 'common.operation.create' })).toBeDisabled()
})
test('should disable confirm action when appName is empty', () => {
it('should disable confirm action when appName is empty', () => {
setup({ appName: ' ' })
expect(screen.getByRole('button', { name: 'common.operation.create' })).toBeDisabled()
@@ -184,13 +184,13 @@ describe('CreateAppModal', () => {
// Defensive coverage for falsy input values and translation edge cases.
describe('Edge Cases', () => {
test('should default description to empty string when appDescription is empty', () => {
it('should default description to empty string when appDescription is empty', () => {
setup({ appDescription: '' })
expect((screen.getByPlaceholderText('app.newApp.appDescriptionPlaceholder') as HTMLTextAreaElement).value).toBe('')
})
test('should fall back to empty placeholders when translations return empty string', () => {
it('should fall back to empty placeholders when translations return empty string', () => {
mockTranslationOverrides = {
'app.newApp.appNamePlaceholder': '',
'app.newApp.appDescriptionPlaceholder': '',
@@ -205,7 +205,7 @@ describe('CreateAppModal', () => {
// The modal should close from user-initiated cancellation actions.
describe('User Interactions', () => {
test('should call onHide when cancel button is clicked', () => {
it('should call onHide when cancel button is clicked', () => {
const { onConfirm, onHide } = setup()
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
@@ -214,7 +214,7 @@ describe('CreateAppModal', () => {
expect(onConfirm).not.toHaveBeenCalled()
})
test('should call onHide when pressing Escape while visible', () => {
it('should call onHide when pressing Escape while visible', () => {
const { onHide } = setup()
fireEvent.keyDown(window, { key: 'Escape', keyCode: 27 })
@@ -222,7 +222,7 @@ describe('CreateAppModal', () => {
expect(onHide).toHaveBeenCalledTimes(1)
})
test('should not call onHide when pressing Escape while hidden', () => {
it('should not call onHide when pressing Escape while hidden', () => {
const { onHide } = setup({ show: false })
fireEvent.keyDown(window, { key: 'Escape', keyCode: 27 })
@@ -233,7 +233,7 @@ describe('CreateAppModal', () => {
// When billing limits are reached, the modal blocks app creation and shows quota guidance.
describe('Quota Gating', () => {
test('should show AppsFull and disable create when apps quota is reached', () => {
it('should show AppsFull and disable create when apps quota is reached', () => {
mockEnableBilling = true
mockPlanType = Plan.team
mockUsagePlanInfo = createPlanInfo(10)
@@ -245,7 +245,7 @@ describe('CreateAppModal', () => {
expect(screen.getByRole('button', { name: 'common.operation.create' })).toBeDisabled()
})
test('should allow saving when apps quota is reached in edit mode', () => {
it('should allow saving when apps quota is reached in edit mode', () => {
mockEnableBilling = true
mockPlanType = Plan.team
mockUsagePlanInfo = createPlanInfo(10)
@@ -268,7 +268,7 @@ describe('CreateAppModal', () => {
vi.useRealTimers()
})
test.each([
it.each([
['meta+enter', { metaKey: true }],
['ctrl+enter', { ctrlKey: true }],
])('should submit when %s is pressed while visible', (_, modifier) => {
@@ -283,7 +283,7 @@ describe('CreateAppModal', () => {
expect(onHide).toHaveBeenCalledTimes(1)
})
test('should not submit when modal is hidden', () => {
it('should not submit when modal is hidden', () => {
const { onConfirm, onHide } = setup({ show: false })
fireEvent.keyDown(window, { key: 'Enter', keyCode: 13, metaKey: true })
@@ -295,7 +295,7 @@ describe('CreateAppModal', () => {
expect(onHide).not.toHaveBeenCalled()
})
test('should not submit when apps quota is reached in create mode', () => {
it('should not submit when apps quota is reached in create mode', () => {
mockEnableBilling = true
mockPlanType = Plan.team
mockUsagePlanInfo = createPlanInfo(10)
@@ -312,7 +312,7 @@ describe('CreateAppModal', () => {
expect(onHide).not.toHaveBeenCalled()
})
test('should submit when apps quota is reached in edit mode', () => {
it('should submit when apps quota is reached in edit mode', () => {
mockEnableBilling = true
mockPlanType = Plan.team
mockUsagePlanInfo = createPlanInfo(10)
@@ -329,7 +329,7 @@ describe('CreateAppModal', () => {
expect(onHide).toHaveBeenCalledTimes(1)
})
test('should not submit when name is empty', () => {
it('should not submit when name is empty', () => {
const { onConfirm, onHide } = setup({ appName: ' ' })
fireEvent.keyDown(window, { key: 'Enter', keyCode: 13, metaKey: true })
@@ -344,7 +344,7 @@ describe('CreateAppModal', () => {
// The app icon picker is a key user flow for customizing metadata.
describe('App Icon Picker', () => {
test('should open and close the picker when cancel is clicked', () => {
it('should open and close the picker when cancel is clicked', () => {
setup({
appIconType: 'image',
appIcon: 'file-123',
@@ -360,7 +360,7 @@ describe('CreateAppModal', () => {
expect(screen.queryByRole('button', { name: 'app.iconPicker.cancel' })).not.toBeInTheDocument()
})
test('should update icon payload when selecting emoji and confirming', () => {
it('should update icon payload when selecting emoji and confirming', () => {
vi.useFakeTimers()
try {
const { onConfirm } = setup({
@@ -399,7 +399,7 @@ describe('CreateAppModal', () => {
}
})
test('should reset emoji icon to initial props when picker is cancelled', () => {
it('should reset emoji icon to initial props when picker is cancelled', () => {
vi.useFakeTimers()
try {
const { onConfirm } = setup({
@@ -459,7 +459,7 @@ describe('CreateAppModal', () => {
vi.useRealTimers()
})
test('should call onConfirm with emoji payload and hide when create is clicked', () => {
it('should call onConfirm with emoji payload and hide when create is clicked', () => {
const { onConfirm, onHide } = setup({
appName: 'My App',
appDescription: 'My description',
@@ -488,7 +488,7 @@ describe('CreateAppModal', () => {
expect(payload).not.toHaveProperty('max_active_requests')
})
test('should include updated description when textarea is changed before submitting', () => {
it('should include updated description when textarea is changed before submitting', () => {
const { onConfirm } = setup({ appDescription: 'Old description' })
fireEvent.change(screen.getByPlaceholderText('app.newApp.appDescriptionPlaceholder'), { target: { value: 'Updated description' } })
@@ -501,7 +501,7 @@ describe('CreateAppModal', () => {
expect(onConfirm.mock.calls[0][0]).toMatchObject({ description: 'Updated description' })
})
test('should omit icon_background when submitting with image icon', () => {
it('should omit icon_background when submitting with image icon', () => {
const { onConfirm } = setup({
appIconType: 'image',
appIcon: 'file-123',
@@ -522,7 +522,7 @@ describe('CreateAppModal', () => {
expect(payload.icon_background).toBeUndefined()
})
test('should include max_active_requests and updated answer icon when saving', () => {
it('should include max_active_requests and updated answer icon when saving', () => {
const { onConfirm } = setup({
isEditModal: true,
appMode: AppModeEnum.CHAT,
@@ -545,7 +545,7 @@ describe('CreateAppModal', () => {
})
})
test('should omit max_active_requests when input is empty', () => {
it('should omit max_active_requests when input is empty', () => {
const { onConfirm } = setup({ isEditModal: true, max_active_requests: null })
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
@@ -557,7 +557,7 @@ describe('CreateAppModal', () => {
expect(payload.max_active_requests).toBeUndefined()
})
test('should omit max_active_requests when input is not a number', () => {
it('should omit max_active_requests when input is not a number', () => {
const { onConfirm } = setup({ isEditModal: true, max_active_requests: null })
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: 'abc' } })
@@ -570,7 +570,7 @@ describe('CreateAppModal', () => {
expect(payload.max_active_requests).toBeUndefined()
})
test('should show toast error and not submit when name becomes empty before debounced submit runs', () => {
it('should show toast error and not submit when name becomes empty before debounced submit runs', () => {
const { onConfirm, onHide } = setup({ appName: 'My App' })
fireEvent.click(screen.getByRole('button', { name: 'common.operation.create' }))
@@ -1,20 +1,21 @@
'use client'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { AppIconType } from '@/types/app'
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
import AppIconPicker from '../../base/app-icon-picker'
import Modal from '@/app/components/base/modal'
import { noop } from 'lodash-es'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import Modal from '@/app/components/base/modal'
import Switch from '@/app/components/base/switch'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { type AppIconType, AppModeEnum } from '@/types/app'
import { noop } from 'lodash-es'
import { useProviderContext } from '@/context/provider-context'
import { AppModeEnum } from '@/types/app'
import AppIconPicker from '../../base/app-icon-picker'
export type CreateAppModalProps = {
show: boolean
@@ -114,26 +115,26 @@ const CreateAppModal = ({
<Modal
isShow={show}
onClose={noop}
className='relative !max-w-[480px] px-8'
className="relative !max-w-[480px] px-8"
>
<div className='absolute right-4 top-4 cursor-pointer p-2' onClick={onHide}>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
<div className="absolute right-4 top-4 cursor-pointer p-2" onClick={onHide}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
{isEditModal && (
<div className='mb-9 text-xl font-semibold leading-[30px] text-text-primary'>{t('app.editAppTitle')}</div>
<div className="mb-9 text-xl font-semibold leading-[30px] text-text-primary">{t('app.editAppTitle')}</div>
)}
{!isEditModal && (
<div className='mb-9 text-xl font-semibold leading-[30px] text-text-primary'>{t('explore.appCustomize.title', { name: appName })}</div>
<div className="mb-9 text-xl font-semibold leading-[30px] text-text-primary">{t('explore.appCustomize.title', { name: appName })}</div>
)}
<div className='mb-9'>
<div className="mb-9">
{/* icon & name */}
<div className='pt-2'>
<div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.newApp.captionName')}</div>
<div className='flex items-center justify-between space-x-2'>
<div className="pt-2">
<div className="py-2 text-sm font-medium leading-[20px] text-text-primary">{t('app.newApp.captionName')}</div>
<div className="flex items-center justify-between space-x-2">
<AppIcon
size='large'
size="large"
onClick={() => { setShowAppIconPicker(true) }}
className='cursor-pointer'
className="cursor-pointer"
iconType={appIcon.type}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'image' ? undefined : appIcon.background}
@@ -143,15 +144,15 @@ const CreateAppModal = ({
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('app.newApp.appNamePlaceholder') || ''}
className='h-10 grow'
className="h-10 grow"
/>
</div>
</div>
{/* description */}
<div className='pt-2'>
<div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.newApp.captionDescription')}</div>
<div className="pt-2">
<div className="py-2 text-sm font-medium leading-[20px] text-text-primary">{t('app.newApp.captionDescription')}</div>
<Textarea
className='resize-none'
className="resize-none"
placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
value={description}
onChange={e => setDescription(e.target.value)}
@@ -159,63 +160,65 @@ const CreateAppModal = ({
</div>
{/* answer icon */}
{isEditModal && (appMode === AppModeEnum.CHAT || appMode === AppModeEnum.ADVANCED_CHAT || appMode === AppModeEnum.AGENT_CHAT) && (
<div className='pt-2'>
<div className='flex items-center justify-between'>
<div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.answerIcon.title')}</div>
<div className="pt-2">
<div className="flex items-center justify-between">
<div className="py-2 text-sm font-medium leading-[20px] text-text-primary">{t('app.answerIcon.title')}</div>
<Switch
defaultValue={useIconAsAnswerIcon}
onChange={v => setUseIconAsAnswerIcon(v)}
/>
</div>
<p className='body-xs-regular text-text-tertiary'>{t('app.answerIcon.descriptionInExplore')}</p>
<p className="body-xs-regular text-text-tertiary">{t('app.answerIcon.descriptionInExplore')}</p>
</div>
)}
{isEditModal && (
<div className='pt-2'>
<div className='mb-2 mt-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.maxActiveRequests')}</div>
<div className="pt-2">
<div className="mb-2 mt-2 text-sm font-medium leading-[20px] text-text-primary">{t('app.maxActiveRequests')}</div>
<Input
type='number'
type="number"
min={1}
placeholder={t('app.maxActiveRequestsPlaceholder')}
value={maxActiveRequestsInput}
onChange={(e) => {
setMaxActiveRequestsInput(e.target.value)
}}
className='h-10 w-full'
className="h-10 w-full"
/>
<p className='body-xs-regular mb-0 mt-2 text-text-tertiary'>{t('app.maxActiveRequestsTip')}</p>
<p className="body-xs-regular mb-0 mt-2 text-text-tertiary">{t('app.maxActiveRequestsTip')}</p>
</div>
)}
{!isEditModal && isAppsFull && <AppsFull className='mt-4' loc='app-explore-create' />}
{!isEditModal && isAppsFull && <AppsFull className="mt-4" loc="app-explore-create" />}
</div>
<div className='flex flex-row-reverse'>
<div className="flex flex-row-reverse">
<Button
disabled={(!isEditModal && isAppsFull) || !name.trim() || confirmDisabled}
className='ml-2 w-24 gap-1'
variant='primary'
className="ml-2 w-24 gap-1"
variant="primary"
onClick={handleSubmit}
>
<span>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</span>
<div className='flex gap-0.5'>
<RiCommandLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
<RiCornerDownLeftLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
<div className="flex gap-0.5">
<RiCommandLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
<RiCornerDownLeftLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
</div>
</Button>
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button className="w-24" onClick={onHide}>{t('common.operation.cancel')}</Button>
</div>
</Modal>
{showAppIconPicker && <AppIconPicker
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(appIconType === 'image'
? { type: 'image' as const, url: appIconUrl, fileId: _appIcon }
: { type: 'emoji' as const, icon: _appIcon, background: appIconBackground })
setShowAppIconPicker(false)
}}
/>}
{showAppIconPicker && (
<AppIconPicker
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(appIconType === 'image'
? { type: 'image' as const, url: appIconUrl, fileId: _appIcon }
: { type: 'emoji' as const, icon: _appIcon, background: appIconBackground })
setShowAppIconPicker(false)
}}
/>
)}
</>
)
}
+6 -6
View File
@@ -1,12 +1,12 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import type { InstalledApp } from '@/models/explore'
import { useRouter } from 'next/navigation'
import ExploreContext from '@/context/explore-context'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
import type { InstalledApp } from '@/models/explore'
import { useTranslation } from 'react-i18next'
import ExploreContext from '@/context/explore-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useMembers } from '@/service/use-common'
@@ -41,7 +41,7 @@ const Explore: FC<IExploreProps> = ({
}, [isCurrentWorkspaceDatasetOperator])
return (
<div className='flex h-full overflow-hidden border-t border-divider-regular bg-background-body'>
<div className="flex h-full overflow-hidden border-t border-divider-regular bg-background-body">
<ExploreContext.Provider
value={
{
@@ -56,7 +56,7 @@ const Explore: FC<IExploreProps> = ({
}
>
<Sidebar controlUpdateInstalledApps={controlUpdateInstalledApps} />
<div className='w-0 grow'>
<div className="w-0 grow">
{children}
</div>
</ExploreContext.Provider>
@@ -1,7 +1,14 @@
import type { Mock } from 'vitest'
import type { InstalledApp as InstalledAppType } from '@/models/explore'
import { render, screen, waitFor } from '@testing-library/react'
import { AppModeEnum } from '@/types/app'
import { useContext } from 'use-context-selector'
import { useWebAppStore } from '@/context/web-app-context'
import { AccessMode } from '@/models/access-control'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
import { AppModeEnum } from '@/types/app'
import InstalledApp from './index'
// Mock external dependencies BEFORE imports
vi.mock('use-context-selector', () => ({
@@ -20,13 +27,6 @@ vi.mock('@/service/use-explore', () => ({
useGetInstalledAppMeta: vi.fn(),
}))
import { useContext } from 'use-context-selector'
import InstalledApp from './index'
import { useWebAppStore } from '@/context/web-app-context'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
import type { InstalledApp as InstalledAppType } from '@/models/explore'
/**
* Mock child components for unit testing
*
@@ -69,7 +69,9 @@ vi.mock('@/app/components/base/chat/chat-with-history', () => ({
className?: string
}) => (
<div data-testid="chat-with-history" className={className}>
Chat With History - {installedAppInfo?.id}
Chat With History -
{' '}
{installedAppInfo?.id}
</div>
),
}))
@@ -1,18 +1,17 @@
'use client'
import type { FC } from 'react'
import { useEffect } from 'react'
import React from 'react'
import type { AppData } from '@/models/share'
import React, { useEffect } from 'react'
import { useContext } from 'use-context-selector'
import ExploreContext from '@/context/explore-context'
import TextGenerationApp from '@/app/components/share/text-generation'
import Loading from '@/app/components/base/loading'
import ChatWithHistory from '@/app/components/base/chat/chat-with-history'
import Loading from '@/app/components/base/loading'
import TextGenerationApp from '@/app/components/share/text-generation'
import ExploreContext from '@/context/explore-context'
import { useWebAppStore } from '@/context/web-app-context'
import AppUnavailable from '../../base/app-unavailable'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
import type { AppData } from '@/models/share'
import { AppModeEnum } from '@/types/app'
import AppUnavailable from '../../base/app-unavailable'
export type IInstalledAppProps = {
id: string
@@ -67,44 +66,58 @@ const InstalledApp: FC<IInstalledAppProps> = ({
}, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode])
if (appParamsError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appParamsError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appParamsError.message} />
</div>
)
}
if (appMetaError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appMetaError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appMetaError.message} />
</div>
)
}
if (useCanAccessAppError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
)
}
if (webAppAccessModeError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={webAppAccessModeError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={webAppAccessModeError.message} />
</div>
)
}
if (userCanAccessApp && !userCanAccessApp.result) {
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
</div>
return (
<div className="flex h-full flex-col items-center justify-center gap-y-2">
<AppUnavailable className="h-auto w-auto" code={403} unknownReason="no permission." />
</div>
)
}
if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || isFetchingInstalledApps) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
return (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)
}
if (!installedApp) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable code={404} isUnknownReason />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable code={404} isUnknownReason />
</div>
)
}
return (
<div className='h-full bg-background-default py-2 pl-0 pr-2 sm:p-2'>
<div className="h-full bg-background-default py-2 pl-0 pr-2 sm:p-2">
{installedApp?.app.mode !== AppModeEnum.COMPLETION && installedApp?.app.mode !== AppModeEnum.WORKFLOW && (
<ChatWithHistory installedAppInfo={installedApp} className='overflow-hidden rounded-2xl shadow-md' />
<ChatWithHistory installedAppInfo={installedApp} className="overflow-hidden rounded-2xl shadow-md" />
)}
{installedApp?.app.mode === AppModeEnum.COMPLETION && (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp} />
@@ -1,18 +1,18 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import {
RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { Pin02 } from '../../base/icons/src/vender/line/general'
import s from './style.module.css'
import { cn } from '@/utils/classnames'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { cn } from '@/utils/classnames'
import { Pin02 } from '../../base/icons/src/vender/line/general'
import s from './style.module.css'
export type IItemOperationProps = {
className?: string
isItemHovering?: boolean
@@ -46,7 +46,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
placement="bottom-end"
offset={4}
>
<PortalToFollowElemTrigger
@@ -59,7 +59,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
>
<div
ref={ref}
className={'min-w-[120px] rounded-lg border border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'}
className="min-w-[120px] rounded-lg border border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]"
onMouseEnter={setIsHovering}
onMouseLeave={setNotHovering}
onClick={(e) => {
@@ -67,17 +67,17 @@ const ItemOperation: FC<IItemOperationProps> = ({
}}
>
<div className={cn(s.actionItem, 'group hover:bg-state-base-hover')} onClick={togglePin}>
<Pin02 className='h-4 w-4 shrink-0 text-text-secondary' />
<Pin02 className="h-4 w-4 shrink-0 text-text-secondary" />
<span className={s.actionName}>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
</div>
{isShowRenameConversation && (
<div className={cn(s.actionItem, 'group hover:bg-state-base-hover')} onClick={onRenameConversation}>
<RiEditLine className='h-4 w-4 shrink-0 text-text-secondary' />
<RiEditLine className="h-4 w-4 shrink-0 text-text-secondary" />
<span className={s.actionName}>{t('explore.sidebar.action.rename')}</span>
</div>
)}
{isShowDelete && (
<div className={cn(s.actionItem, s.deleteActionItem, 'group hover:bg-state-base-hover')} onClick={onDelete} >
<div className={cn(s.actionItem, s.deleteActionItem, 'group hover:bg-state-base-hover')} onClick={onDelete}>
<RiDeleteBinLine className={cn(s.deleteActionItemChild, 'h-4 w-4 shrink-0 stroke-current stroke-2 text-text-secondary')} />
<span className={cn(s.actionName, s.deleteActionItemChild)}>{t('explore.sidebar.action.delete')}</span>
</div>
@@ -1,13 +1,13 @@
'use client'
import React, { useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useHover } from 'ahooks'
import { cn } from '@/utils/classnames'
import ItemOperation from '@/app/components/explore/item-operation'
import AppIcon from '@/app/components/base/app-icon'
import type { AppIconType } from '@/types/app'
import { useHover } from 'ahooks'
import { useRouter } from 'next/navigation'
import React, { useRef } from 'react'
import AppIcon from '@/app/components/base/app-icon'
import ItemOperation from '@/app/components/explore/item-operation'
import { cn } from '@/utils/classnames'
export type IAppNavItemProps = {
isMobile: boolean
name: string
@@ -45,21 +45,19 @@ export default function AppNavItem({
<div
ref={ref}
key={id}
className={cn('system-sm-medium flex h-8 items-center justify-between rounded-lg px-2 text-sm font-normal text-components-menu-item-text mobile:justify-center mobile:px-1',
isSelected ? 'bg-state-base-active text-components-menu-item-text-active' : 'hover:bg-state-base-hover hover:text-components-menu-item-text-hover',
)}
className={cn('system-sm-medium flex h-8 items-center justify-between rounded-lg px-2 text-sm font-normal text-components-menu-item-text mobile:justify-center mobile:px-1', isSelected ? 'bg-state-base-active text-components-menu-item-text-active' : 'hover:bg-state-base-hover hover:text-components-menu-item-text-hover')}
onClick={() => {
router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation().
}}
>
{isMobile && <AppIcon size='tiny' iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />}
{isMobile && <AppIcon size="tiny" iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />}
{!isMobile && (
<>
<div className='flex w-0 grow items-center space-x-2'>
<AppIcon size='tiny' iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
<div className='overflow-hidden text-ellipsis whitespace-nowrap' title={name}>{name}</div>
<div className="flex w-0 grow items-center space-x-2">
<AppIcon size="tiny" iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
<div className="overflow-hidden text-ellipsis whitespace-nowrap" title={name}>{name}</div>
</div>
<div className='h-6 shrink-0' onClick={e => e.stopPropagation()}>
<div className="h-6 shrink-0" onClick={e => e.stopPropagation()}>
<ItemOperation
isPinned={isPinned}
isItemHovering={isHovering}
+14 -14
View File
@@ -1,18 +1,18 @@
'use client'
import type { FC } from 'react'
import Link from 'next/link'
import { useSelectedLayoutSegments } from 'next/navigation'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useSelectedLayoutSegments } from 'next/navigation'
import Link from 'next/link'
import Toast from '../../base/toast'
import Item from './app-nav-item'
import { cn } from '@/utils/classnames'
import ExploreContext from '@/context/explore-context'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import ExploreContext from '@/context/explore-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
import { cn } from '@/utils/classnames'
import Toast from '../../base/toast'
import Item from './app-nav-item'
const SelectedDiscoveryIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
@@ -83,22 +83,22 @@ const SideBar: FC<IExploreSideBarProps> = ({
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
return (
<div className='w-fit shrink-0 cursor-pointer border-r border-divider-burn px-4 pt-6 sm:w-[216px]'>
<div className="w-fit shrink-0 cursor-pointer border-r border-divider-burn px-4 pt-6 sm:w-[216px]">
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
<Link
href='/explore/apps'
className={cn(isDiscoverySelected ? ' bg-components-main-nav-nav-button-bg-active' : 'font-medium hover:bg-state-base-hover',
'flex h-9 items-center gap-2 rounded-lg px-3 mobile:w-fit mobile:justify-center mobile:px-2 pc:w-full pc:justify-start')}
href="/explore/apps"
className={cn(isDiscoverySelected ? ' bg-components-main-nav-nav-button-bg-active' : 'font-medium hover:bg-state-base-hover', 'flex h-9 items-center gap-2 rounded-lg px-3 mobile:w-fit mobile:justify-center mobile:px-2 pc:w-full pc:justify-start')}
style={isDiscoverySelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}}
>
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
{!isMobile && <div className='text-sm'>{t('explore.sidebar.discovery')}</div>}
{!isMobile && <div className="text-sm">{t('explore.sidebar.discovery')}</div>}
</Link>
</div>
{installedApps.length > 0 && (
<div className='mt-10'>
<p className='break-all pl-2 text-xs font-medium uppercase text-text-tertiary mobile:px-0'>{t('explore.sidebar.workspace')}</p>
<div className='mt-3 space-y-1 overflow-y-auto overflow-x-hidden'
<div className="mt-10">
<p className="break-all pl-2 text-xs font-medium uppercase text-text-tertiary mobile:px-0">{t('explore.sidebar.workspace')}</p>
<div
className="mt-3 space-y-1 overflow-y-auto overflow-x-hidden"
style={{
height: 'calc(100vh - 250px)',
}}