mirror of
https://github.com/langgenius/dify.git
synced 2026-09-21 13:20:52 +08:00
chore(web): new lint setup (#30020)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)',
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user