mirror of
https://github.com/langgenius/dify.git
synced 2026-04-26 05:35:58 +08:00
refactor(web): remove ExploreContext and migrate to derived state with oRPC contracts
Replace ExploreContext with derived permission checks using useAppContext and useMembers, eliminating redundant state synchronization. Add oRPC contract for explore endpoints, extract TryAppSelection type, and migrate app-publisher icons from @remixicon/react components to CSS icon classes. Update all related tests to reflect the new context-free architecture.
This commit is contained in:
@ -9,8 +9,9 @@ import type { CreateAppModalProps } from '@/app/components/explore/create-app-mo
|
||||
import type { App } from '@/models/explore'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import AppList from '@/app/components/explore/app-list'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
const allCategoriesEn = 'explore.apps.allCategories:{"lng":"en"}'
|
||||
@ -57,6 +58,14 @@ vi.mock('@/service/explore', () => ({
|
||||
fetchAppList: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useMembers: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-import-dsl', () => ({
|
||||
useImportDSL: () => ({
|
||||
handleImportDSL: mockHandleImportDSL,
|
||||
@ -126,20 +135,25 @@ const createApp = (overrides: Partial<App> = {}): App => ({
|
||||
is_agent: overrides.is_agent ?? false,
|
||||
})
|
||||
|
||||
const createContextValue = (hasEditPermission = true) => ({
|
||||
hasEditPermission,
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: vi.fn(),
|
||||
})
|
||||
const mockMemberRole = (hasEditPermission: boolean) => {
|
||||
;(useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
})
|
||||
;(useMembers as Mock).mockReturnValue({
|
||||
data: {
|
||||
accounts: [{ id: 'user-1', role: hasEditPermission ? 'admin' : 'normal' }],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const wrapWithContext = (hasEditPermission = true, onSuccess?: () => void) => (
|
||||
<ExploreContext.Provider value={createContextValue(hasEditPermission)}>
|
||||
<AppList onSuccess={onSuccess} />
|
||||
</ExploreContext.Provider>
|
||||
)
|
||||
const renderAppList = (hasEditPermission = true, onSuccess?: () => void) => {
|
||||
mockMemberRole(hasEditPermission)
|
||||
return render(<AppList onSuccess={onSuccess} />)
|
||||
}
|
||||
|
||||
const renderWithContext = (hasEditPermission = true, onSuccess?: () => void) => {
|
||||
return render(wrapWithContext(hasEditPermission, onSuccess))
|
||||
const appListElement = (hasEditPermission = true, onSuccess?: () => void) => {
|
||||
mockMemberRole(hasEditPermission)
|
||||
return <AppList onSuccess={onSuccess} />
|
||||
}
|
||||
|
||||
describe('Explore App List Flow', () => {
|
||||
@ -159,7 +173,7 @@ describe('Explore App List Flow', () => {
|
||||
|
||||
describe('Browse and Filter Flow', () => {
|
||||
it('should display all apps when no category filter is applied', () => {
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
expect(screen.getByText('Writer Bot')).toBeInTheDocument()
|
||||
expect(screen.getByText('Translator')).toBeInTheDocument()
|
||||
@ -168,7 +182,7 @@ describe('Explore App List Flow', () => {
|
||||
|
||||
it('should filter apps by selected category', () => {
|
||||
mockTabValue = 'Writing'
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
expect(screen.getByText('Writer Bot')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Translator')).not.toBeInTheDocument()
|
||||
@ -176,7 +190,7 @@ describe('Explore App List Flow', () => {
|
||||
})
|
||||
|
||||
it('should filter apps by search keyword', async () => {
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
fireEvent.change(input, { target: { value: 'trans' } })
|
||||
@ -201,7 +215,7 @@ describe('Explore App List Flow', () => {
|
||||
options.onSuccess?.()
|
||||
})
|
||||
|
||||
renderWithContext(true, onSuccess)
|
||||
renderAppList(true, onSuccess)
|
||||
|
||||
// Step 2: Click add to workspace button - opens create modal
|
||||
fireEvent.click(screen.getAllByText('explore.appCard.addToWorkspace')[0])
|
||||
@ -234,7 +248,7 @@ describe('Explore App List Flow', () => {
|
||||
// Step 1: Loading state
|
||||
mockIsLoading = true
|
||||
mockExploreData = undefined
|
||||
const { rerender } = render(wrapWithContext())
|
||||
const { unmount } = render(appListElement())
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
|
||||
@ -244,7 +258,8 @@ describe('Explore App List Flow', () => {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp()],
|
||||
}
|
||||
rerender(wrapWithContext())
|
||||
unmount()
|
||||
renderAppList()
|
||||
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
@ -253,13 +268,13 @@ describe('Explore App List Flow', () => {
|
||||
|
||||
describe('Permission-Based Behavior', () => {
|
||||
it('should hide add-to-workspace button when user has no edit permission', () => {
|
||||
renderWithContext(false)
|
||||
renderAppList(false)
|
||||
|
||||
expect(screen.queryByText('explore.appCard.addToWorkspace')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show add-to-workspace button when user has edit permission', () => {
|
||||
renderWithContext(true)
|
||||
renderAppList(true)
|
||||
|
||||
expect(screen.getAllByText('explore.appCard.addToWorkspace').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
@ -2,18 +2,6 @@ import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiBuildingLine,
|
||||
RiGlobalLine,
|
||||
RiLockLine,
|
||||
RiPlanetLine,
|
||||
RiPlayCircleLine,
|
||||
RiPlayList2Line,
|
||||
RiTerminalBoxLine,
|
||||
RiVerifiedBadgeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import {
|
||||
memo,
|
||||
@ -57,22 +45,22 @@ import SuggestedAction from './suggested-action'
|
||||
|
||||
type AccessModeLabel = I18nKeysByPrefix<'app', 'accessControlDialog.accessItems.'>
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: AccessModeLabel, icon: React.ElementType }> = {
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: AccessModeLabel, icon: string }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
label: 'organization',
|
||||
icon: RiBuildingLine,
|
||||
icon: 'i-ri-building-line',
|
||||
},
|
||||
[AccessMode.SPECIFIC_GROUPS_MEMBERS]: {
|
||||
label: 'specific',
|
||||
icon: RiLockLine,
|
||||
icon: 'i-ri-lock-line',
|
||||
},
|
||||
[AccessMode.PUBLIC]: {
|
||||
label: 'anyone',
|
||||
icon: RiGlobalLine,
|
||||
icon: 'i-ri-global-line',
|
||||
},
|
||||
[AccessMode.EXTERNAL_MEMBERS]: {
|
||||
label: 'external',
|
||||
icon: RiVerifiedBadgeLine,
|
||||
icon: 'i-ri-verified-badge-line',
|
||||
},
|
||||
}
|
||||
|
||||
@ -82,13 +70,13 @@ const AccessModeDisplay: React.FC<{ mode?: AccessMode }> = ({ mode }) => {
|
||||
if (!mode || !ACCESS_MODE_MAP[mode])
|
||||
return null
|
||||
|
||||
const { icon: Icon, label } = ACCESS_MODE_MAP[mode]
|
||||
const { icon, label } = ACCESS_MODE_MAP[mode]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon className="h-4 w-4 shrink-0 text-text-secondary" />
|
||||
<span className={`${icon} h-4 w-4 shrink-0 text-text-secondary`} />
|
||||
<div className="grow truncate">
|
||||
<span className="system-sm-medium text-text-secondary">{t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-medium">{t(`accessControlDialog.accessItems.${label}`, { ns: 'app' })}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@ -225,7 +213,7 @@ const AppPublisher = ({
|
||||
await openAsyncWindow(async () => {
|
||||
if (!appDetail?.id)
|
||||
throw new Error('App not found')
|
||||
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
|
||||
const { installed_apps } = await fetchInstalledAppList(appDetail.id)
|
||||
if (installed_apps?.length > 0)
|
||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||
throw new Error('No app found in Explore')
|
||||
@ -284,19 +272,19 @@ const AppPublisher = ({
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('common.publish', { ns: 'workflow' })}
|
||||
<RiArrowDownSLine className="h-4 w-4 text-components-button-primary-text" />
|
||||
<span className="i-ri-arrow-down-s-line h-4 w-4 text-components-button-primary-text" />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[11]">
|
||||
<div className="w-[320px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
|
||||
<div className="p-4 pt-3">
|
||||
<div className="system-xs-medium-uppercase flex h-6 items-center text-text-tertiary">
|
||||
<div className="flex h-6 items-center text-text-tertiary system-xs-medium-uppercase">
|
||||
{publishedAt ? t('common.latestPublished', { ns: 'workflow' }) : t('common.currentDraftUnpublished', { ns: 'workflow' })}
|
||||
</div>
|
||||
{publishedAt
|
||||
? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="system-sm-medium flex items-center text-text-secondary">
|
||||
<div className="flex items-center text-text-secondary system-sm-medium">
|
||||
{t('common.publishedAt', { ns: 'workflow' })}
|
||||
{' '}
|
||||
{formatTimeFromNow(publishedAt)}
|
||||
@ -314,7 +302,7 @@ const AppPublisher = ({
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="system-sm-medium flex items-center text-text-secondary">
|
||||
<div className="flex items-center text-text-secondary system-sm-medium">
|
||||
{t('common.autoSaved', { ns: 'workflow' })}
|
||||
{' '}
|
||||
·
|
||||
@ -377,10 +365,10 @@ const AppPublisher = ({
|
||||
{systemFeatures.webapp_auth.enabled && (
|
||||
<div className="p-4 pt-3">
|
||||
<div className="flex h-6 items-center">
|
||||
<p className="system-xs-medium text-text-tertiary">{t('publishApp.title', { ns: 'app' })}</p>
|
||||
<p className="text-text-tertiary system-xs-medium">{t('publishApp.title', { ns: 'app' })}</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-8 cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2 hover:bg-primary-50 hover:text-text-accent"
|
||||
className="flex h-8 cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2 hover:bg-primary-50 hover:text-text-accent"
|
||||
onClick={() => {
|
||||
setShowAppAccessControl(true)
|
||||
}}
|
||||
@ -388,12 +376,12 @@ const AppPublisher = ({
|
||||
<div className="flex grow items-center gap-x-1.5 overflow-hidden pr-1">
|
||||
<AccessModeDisplay mode={appDetail?.access_mode} />
|
||||
</div>
|
||||
{!isAppAccessSet && <p className="system-xs-regular shrink-0 text-text-tertiary">{t('publishApp.notSet', { ns: 'app' })}</p>}
|
||||
{!isAppAccessSet && <p className="shrink-0 text-text-tertiary system-xs-regular">{t('publishApp.notSet', { ns: 'app' })}</p>}
|
||||
<div className="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
<RiArrowRightSLine className="h-4 w-4 text-text-quaternary" />
|
||||
<span className="i-ri-arrow-right-s-line h-4 w-4 text-text-quaternary" />
|
||||
</div>
|
||||
</div>
|
||||
{!isAppAccessSet && <p className="system-xs-regular mt-1 text-text-warning">{t('publishApp.notSetDesc', { ns: 'app' })}</p>}
|
||||
{!isAppAccessSet && <p className="mt-1 text-text-warning system-xs-regular">{t('publishApp.notSetDesc', { ns: 'app' })}</p>}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
@ -405,7 +393,7 @@ const AppPublisher = ({
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
link={appURL}
|
||||
icon={<RiPlayCircleLine className="h-4 w-4" />}
|
||||
icon={<span className="i-ri-play-circle-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.runApp', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
@ -417,7 +405,7 @@ const AppPublisher = ({
|
||||
className="flex-1"
|
||||
disabled={disabledFunctionButton}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<RiPlayList2Line className="h-4 w-4" />}
|
||||
icon={<span className="i-ri-play-list-2-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.batchRunApp', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
@ -443,7 +431,7 @@ const AppPublisher = ({
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={disabledFunctionButton}
|
||||
icon={<RiPlanetLine className="h-4 w-4" />}
|
||||
icon={<span className="i-ri-planet-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.openInExplore', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
@ -453,7 +441,7 @@ const AppPublisher = ({
|
||||
className="flex-1"
|
||||
disabled={!publishedAt || missingStartNode}
|
||||
link="./develop"
|
||||
icon={<RiTerminalBoxLine className="h-4 w-4" />}
|
||||
icon={<span className="i-ri-terminal-box-line h-4 w-4" />}
|
||||
>
|
||||
{t('common.accessAPIReference', { ns: 'workflow' })}
|
||||
</SuggestedAction>
|
||||
|
||||
@ -248,7 +248,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
await openAsyncWindow(async () => {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
|
||||
const { installed_apps } = await fetchInstalledAppList(app.id)
|
||||
if (installed_apps?.length > 0)
|
||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||
throw new Error('No app found in Explore')
|
||||
@ -258,21 +258,22 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
},
|
||||
})
|
||||
}
|
||||
catch (e: any) {
|
||||
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||
catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : `${e}`
|
||||
Toast.notify({ type: 'error', message })
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickSettings}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('editApp', { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-regular">{t('editApp', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickDuplicate}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('duplicate', { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-regular">{t('duplicate', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickExport}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('export', { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-regular">{t('export', { ns: 'app' })}</span>
|
||||
</button>
|
||||
{(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && (
|
||||
<>
|
||||
@ -293,7 +294,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('openInExplore', { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
@ -301,7 +302,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('openInExplore', { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
@ -323,7 +324,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
className="group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover"
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
<span className="system-sm-regular text-text-secondary group-hover:text-text-destructive">
|
||||
<span className="text-text-secondary system-sm-regular group-hover:text-text-destructive">
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { CreateAppModalProps } from '../explore/create-app-modal'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import type { TryAppSelection } from '@/types/try-app'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEducationInit } from '@/app/education-apply/hooks'
|
||||
@ -20,13 +20,13 @@ const Apps = () => {
|
||||
useDocumentTitle(t('menus.apps', { ns: 'common' }))
|
||||
useEducationInit()
|
||||
|
||||
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
|
||||
const [currentTryAppParams, setCurrentTryAppParams] = useState<TryAppSelection | undefined>(undefined)
|
||||
const currApp = currentTryAppParams?.app
|
||||
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
|
||||
const hideTryAppPanel = useCallback(() => {
|
||||
setIsShowTryAppPanel(false)
|
||||
}, [])
|
||||
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
|
||||
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: TryAppSelection) => {
|
||||
if (showTryAppPanel)
|
||||
setCurrentTryAppParams(params)
|
||||
else
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import Explore from '../index'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
@ -47,83 +42,31 @@ vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useMembers: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
const ContextReader = ({ triggerTryPanel }: { triggerTryPanel?: boolean }) => {
|
||||
const { hasEditPermission, setShowTryAppPanel, isShowTryAppPanel, currentApp } = useContext(ExploreContext)
|
||||
return (
|
||||
<div>
|
||||
{hasEditPermission ? 'edit-yes' : 'edit-no'}
|
||||
{isShowTryAppPanel && <span data-testid="try-panel-open">open</span>}
|
||||
{currentApp && <span data-testid="current-app">{currentApp.appId}</span>}
|
||||
{triggerTryPanel && (
|
||||
<>
|
||||
<button data-testid="show-try" onClick={() => setShowTryAppPanel(true, { appId: 'test-app' } as CurrentTryAppParams)}>show</button>
|
||||
<button data-testid="hide-try" onClick={() => setShowTryAppPanel(false)}>hide</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Explore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render children and provide edit permission from members role', async () => {
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({
|
||||
data: {
|
||||
accounts: [{ id: 'user-1', role: 'admin' }],
|
||||
},
|
||||
})
|
||||
|
||||
render((
|
||||
<Explore>
|
||||
<ContextReader />
|
||||
</Explore>
|
||||
))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('edit-yes')).toBeInTheDocument()
|
||||
})
|
||||
;(useAppContext as Mock).mockReturnValue({
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Effects', () => {
|
||||
it('should set document title on render', () => {
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render children', () => {
|
||||
render((
|
||||
<Explore>
|
||||
<div>child</div>
|
||||
</Explore>
|
||||
))
|
||||
|
||||
expect(useDocumentTitle).toHaveBeenCalledWith('common.menus.explore')
|
||||
expect(screen.getByText('child')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Effects', () => {
|
||||
it('should redirect dataset operators to /datasets', async () => {
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
;(useAppContext as Mock).mockReturnValue({
|
||||
isCurrentWorkspaceDatasetOperator: true,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
|
||||
})
|
||||
|
||||
render((
|
||||
<Explore>
|
||||
@ -136,68 +79,14 @@ describe('Explore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip permission check when membersData has no accounts', () => {
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({ data: undefined })
|
||||
|
||||
it('should not redirect non dataset operators', () => {
|
||||
render((
|
||||
<Explore>
|
||||
<ContextReader />
|
||||
<div>child</div>
|
||||
</Explore>
|
||||
))
|
||||
|
||||
expect(screen.getByText('edit-no')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Context: setShowTryAppPanel', () => {
|
||||
it('should set currentApp params when showing try panel', async () => {
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
|
||||
|
||||
render((
|
||||
<Explore>
|
||||
<ContextReader triggerTryPanel />
|
||||
</Explore>
|
||||
))
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-try'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('try-panel-open')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('current-app')).toHaveTextContent('test-app')
|
||||
})
|
||||
})
|
||||
|
||||
it('should clear currentApp params when hiding try panel', async () => {
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
|
||||
|
||||
render((
|
||||
<Explore>
|
||||
<ContextReader triggerTryPanel />
|
||||
</Explore>
|
||||
))
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-try'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('try-panel-open')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('hide-try'))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('try-panel-open')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('current-app')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,7 +2,6 @@ import type { AppCardProps } from '../index'
|
||||
import type { App } from '@/models/explore'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppCard from '../index'
|
||||
|
||||
@ -41,12 +40,14 @@ const createApp = (overrides?: Partial<App>): App => ({
|
||||
|
||||
describe('AppCard', () => {
|
||||
const onCreate = vi.fn()
|
||||
const onTry = vi.fn()
|
||||
|
||||
const renderComponent = (props?: Partial<AppCardProps>) => {
|
||||
const mergedProps: AppCardProps = {
|
||||
app: createApp(),
|
||||
canCreate: false,
|
||||
onCreate,
|
||||
onTry,
|
||||
isExplore: false,
|
||||
...props,
|
||||
}
|
||||
@ -138,25 +139,14 @@ describe('AppCard', () => {
|
||||
expect(screen.getByText('Sample App')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call setShowTryAppPanel when try button is clicked', () => {
|
||||
const mockSetShowTryAppPanel = vi.fn()
|
||||
it('should call onTry when try button is clicked', () => {
|
||||
const app = createApp()
|
||||
|
||||
render(
|
||||
<ExploreContext.Provider
|
||||
value={{
|
||||
hasEditPermission: false,
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: mockSetShowTryAppPanel,
|
||||
}}
|
||||
>
|
||||
<AppCard app={app} canCreate={true} onCreate={vi.fn()} isExplore={true} />
|
||||
</ExploreContext.Provider>,
|
||||
)
|
||||
renderComponent({ app, canCreate: true, isExplore: true })
|
||||
|
||||
fireEvent.click(screen.getByText('explore.appCard.try'))
|
||||
|
||||
expect(mockSetShowTryAppPanel).toHaveBeenCalledWith(true, { appId: 'app-id', app })
|
||||
expect(onTry).toHaveBeenCalledWith({ appId: 'app-id', app })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
'use client'
|
||||
import type { App } from '@/models/explore'
|
||||
import type { TryAppSelection } from '@/types/try-app'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
import { RiInformation2Line } from '@remixicon/react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -17,25 +15,24 @@ export type AppCardProps = {
|
||||
app: App
|
||||
canCreate: boolean
|
||||
onCreate: () => void
|
||||
isExplore: boolean
|
||||
onTry: (params: TryAppSelection) => void
|
||||
isExplore?: boolean
|
||||
}
|
||||
|
||||
const AppCard = ({
|
||||
app,
|
||||
canCreate,
|
||||
onCreate,
|
||||
isExplore,
|
||||
onTry,
|
||||
isExplore = true,
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { app: appBasicInfo } = app
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
||||
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
|
||||
const showTryAPPPanel = useCallback((appId: string) => {
|
||||
return () => {
|
||||
setShowTryAppPanel?.(true, { appId, app })
|
||||
}
|
||||
}, [setShowTryAppPanel, app])
|
||||
const handleTryApp = () => {
|
||||
onTry({ appId: app.app_id, 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:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg')}>
|
||||
@ -67,7 +64,7 @@ const AppCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="description-wrapper system-xs-regular h-[90px] px-[14px] text-text-tertiary">
|
||||
<div className="description-wrapper h-[90px] px-[14px] text-text-tertiary system-xs-regular">
|
||||
<div className="line-clamp-4 group-hover:line-clamp-2">
|
||||
{app.description}
|
||||
</div>
|
||||
@ -83,7 +80,7 @@ const AppCard = ({
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button className="h-7" onClick={showTryAPPPanel(app.app_id)}>
|
||||
<Button className="h-7" onClick={handleTryApp}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import type { App } from '@/models/explore'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppList from '../index'
|
||||
|
||||
@ -29,6 +29,14 @@ vi.mock('@/service/explore', () => ({
|
||||
fetchAppList: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useMembers: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-import-dsl', () => ({
|
||||
useImportDSL: () => ({
|
||||
handleImportDSL: mockHandleImportDSL,
|
||||
@ -111,18 +119,22 @@ const createApp = (overrides: Partial<App> = {}): App => ({
|
||||
is_agent: overrides.is_agent ?? false,
|
||||
})
|
||||
|
||||
const renderWithContext = (hasEditPermission = false, onSuccess?: () => void, searchParams?: Record<string, string>) => {
|
||||
const mockMemberRole = (hasEditPermission: boolean) => {
|
||||
;(useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
})
|
||||
;(useMembers as Mock).mockReturnValue({
|
||||
data: {
|
||||
accounts: [{ id: 'user-1', role: hasEditPermission ? 'admin' : 'normal' }],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const renderAppList = (hasEditPermission = false, onSuccess?: () => void, searchParams?: Record<string, string>) => {
|
||||
mockMemberRole(hasEditPermission)
|
||||
return render(
|
||||
<NuqsTestingAdapter searchParams={searchParams}>
|
||||
<ExploreContext.Provider
|
||||
value={{
|
||||
hasEditPermission,
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<AppList onSuccess={onSuccess} />
|
||||
</ExploreContext.Provider>
|
||||
<AppList onSuccess={onSuccess} />
|
||||
</NuqsTestingAdapter>,
|
||||
)
|
||||
}
|
||||
@ -145,7 +157,7 @@ describe('AppList', () => {
|
||||
mockExploreData = undefined
|
||||
mockIsLoading = true
|
||||
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
@ -156,7 +168,7 @@ describe('AppList', () => {
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })],
|
||||
}
|
||||
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.getByText('Beta')).toBeInTheDocument()
|
||||
@ -170,7 +182,7 @@ describe('AppList', () => {
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })],
|
||||
}
|
||||
|
||||
renderWithContext(false, undefined, { category: 'Writing' })
|
||||
renderAppList(false, undefined, { category: 'Writing' })
|
||||
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
|
||||
@ -183,7 +195,7 @@ describe('AppList', () => {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
|
||||
}
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
fireEvent.change(input, { target: { value: 'gam' } })
|
||||
@ -211,7 +223,7 @@ describe('AppList', () => {
|
||||
options.onSuccess?.()
|
||||
})
|
||||
|
||||
renderWithContext(true, onSuccess)
|
||||
renderAppList(true, onSuccess)
|
||||
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
|
||||
fireEvent.click(await screen.findByTestId('confirm-create'))
|
||||
|
||||
@ -235,7 +247,7 @@ describe('AppList', () => {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
|
||||
}
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
fireEvent.change(input, { target: { value: 'gam' } })
|
||||
@ -257,7 +269,7 @@ describe('AppList', () => {
|
||||
mockIsError = true
|
||||
mockExploreData = undefined
|
||||
|
||||
const { container } = renderWithContext()
|
||||
const { container } = renderAppList()
|
||||
|
||||
expect(container.innerHTML).toBe('')
|
||||
})
|
||||
@ -265,7 +277,7 @@ describe('AppList', () => {
|
||||
it('should render nothing when data is undefined', () => {
|
||||
mockExploreData = undefined
|
||||
|
||||
const { container } = renderWithContext()
|
||||
const { container } = renderAppList()
|
||||
|
||||
expect(container.innerHTML).toBe('')
|
||||
})
|
||||
@ -275,7 +287,7 @@ describe('AppList', () => {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
|
||||
}
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
fireEvent.change(input, { target: { value: 'gam' } })
|
||||
@ -298,7 +310,7 @@ describe('AppList', () => {
|
||||
};
|
||||
(fetchAppDetail as unknown as Mock).mockResolvedValue({ export_data: 'yaml' })
|
||||
|
||||
renderWithContext(true)
|
||||
renderAppList(true)
|
||||
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
|
||||
expect(await screen.findByTestId('create-app-modal')).toBeInTheDocument()
|
||||
|
||||
@ -319,7 +331,7 @@ describe('AppList', () => {
|
||||
options.onSuccess?.()
|
||||
})
|
||||
|
||||
renderWithContext(true)
|
||||
renderAppList(true)
|
||||
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
|
||||
fireEvent.click(await screen.findByTestId('confirm-create'))
|
||||
|
||||
@ -339,7 +351,7 @@ describe('AppList', () => {
|
||||
options.onPending?.()
|
||||
})
|
||||
|
||||
renderWithContext(true)
|
||||
renderAppList(true)
|
||||
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
|
||||
fireEvent.click(await screen.findByTestId('confirm-create'))
|
||||
|
||||
@ -356,58 +368,16 @@ describe('AppList', () => {
|
||||
|
||||
describe('TryApp Panel', () => {
|
||||
it('should open create modal from try app panel', async () => {
|
||||
vi.useRealTimers()
|
||||
const mockSetShowTryAppPanel = vi.fn()
|
||||
const app = createApp()
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
allList: [app],
|
||||
}
|
||||
|
||||
render(
|
||||
<NuqsTestingAdapter>
|
||||
<ExploreContext.Provider
|
||||
value={{
|
||||
hasEditPermission: true,
|
||||
isShowTryAppPanel: true,
|
||||
setShowTryAppPanel: mockSetShowTryAppPanel,
|
||||
currentApp: { appId: 'app-1', app },
|
||||
}}
|
||||
>
|
||||
<AppList />
|
||||
</ExploreContext.Provider>
|
||||
</NuqsTestingAdapter>,
|
||||
)
|
||||
|
||||
const createBtn = screen.getByTestId('try-app-create')
|
||||
fireEvent.click(createBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-app-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should open create modal with null currApp when appParams has no app', async () => {
|
||||
vi.useRealTimers()
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp()],
|
||||
}
|
||||
|
||||
render(
|
||||
<NuqsTestingAdapter>
|
||||
<ExploreContext.Provider
|
||||
value={{
|
||||
hasEditPermission: true,
|
||||
isShowTryAppPanel: true,
|
||||
setShowTryAppPanel: vi.fn(),
|
||||
currentApp: { appId: 'app-1' } as CurrentTryAppParams,
|
||||
}}
|
||||
>
|
||||
<AppList />
|
||||
</ExploreContext.Provider>
|
||||
</NuqsTestingAdapter>,
|
||||
)
|
||||
renderAppList(true)
|
||||
|
||||
fireEvent.click(screen.getByText('explore.appCard.try'))
|
||||
expect(screen.getByTestId('try-app-panel')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('try-app-create'))
|
||||
|
||||
@ -416,27 +386,19 @@ describe('AppList', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should render try app panel with empty appId when currentApp is undefined', () => {
|
||||
it('should close try app panel when close is clicked', () => {
|
||||
mockExploreData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp()],
|
||||
}
|
||||
|
||||
render(
|
||||
<NuqsTestingAdapter>
|
||||
<ExploreContext.Provider
|
||||
value={{
|
||||
hasEditPermission: true,
|
||||
isShowTryAppPanel: true,
|
||||
setShowTryAppPanel: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<AppList />
|
||||
</ExploreContext.Provider>
|
||||
</NuqsTestingAdapter>,
|
||||
)
|
||||
renderAppList(true)
|
||||
|
||||
fireEvent.click(screen.getByText('explore.appCard.try'))
|
||||
expect(screen.getByTestId('try-app-panel')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('try-app-close'))
|
||||
expect(screen.queryByTestId('try-app-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -453,7 +415,7 @@ describe('AppList', () => {
|
||||
allList: [createApp()],
|
||||
}
|
||||
|
||||
renderWithContext()
|
||||
renderAppList()
|
||||
|
||||
expect(screen.getByTestId('explore-banner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import type { App } from '@/models/explore'
|
||||
import type { TryAppSelection } from '@/types/try-app'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
@ -16,13 +16,14 @@ import AppCard from '@/app/components/explore/app-card'
|
||||
import Banner from '@/app/components/explore/banner/banner'
|
||||
import Category from '@/app/components/explore/category'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import {
|
||||
DSLImportMode,
|
||||
} from '@/models/app'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import { useExploreAppList } from '@/service/use-explore'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import TryApp from '../try-app'
|
||||
@ -36,9 +37,12 @@ const Apps = ({
|
||||
onSuccess,
|
||||
}: AppsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
const { data: membersData } = useMembers()
|
||||
const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' })
|
||||
const userAccount = membersData?.accounts?.find(account => account.id === userProfile.id)
|
||||
const hasEditPermission = !!userAccount && userAccount.role !== 'normal'
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
@ -85,8 +89,8 @@ const Apps = ({
|
||||
)
|
||||
}, [searchKeywords, filteredList])
|
||||
|
||||
const [currApp, setCurrApp] = React.useState<App | null>(null)
|
||||
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
|
||||
const [currApp, setCurrApp] = useState<App | null>(null)
|
||||
const [isShowCreateModal, setIsShowCreateModal] = useState(false)
|
||||
|
||||
const {
|
||||
handleImportDSL,
|
||||
@ -96,16 +100,18 @@ const Apps = ({
|
||||
} = useImportDSL()
|
||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
||||
|
||||
const isShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.isShowTryAppPanel)
|
||||
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
|
||||
const [currentTryApp, setCurrentTryApp] = useState<TryAppSelection | undefined>(undefined)
|
||||
const isShowTryAppPanel = !!currentTryApp
|
||||
const hideTryAppPanel = useCallback(() => {
|
||||
setShowTryAppPanel(false)
|
||||
}, [setShowTryAppPanel])
|
||||
const appParams = useContextSelector(ExploreContext, ctx => ctx.currentApp)
|
||||
setCurrentTryApp(undefined)
|
||||
}, [])
|
||||
const handleTryApp = useCallback((params: TryAppSelection) => {
|
||||
setCurrentTryApp(params)
|
||||
}, [])
|
||||
const handleShowFromTryApp = useCallback(() => {
|
||||
setCurrApp(appParams?.app || null)
|
||||
setCurrApp(currentTryApp?.app || null)
|
||||
setIsShowCreateModal(true)
|
||||
}, [appParams?.app])
|
||||
}, [currentTryApp?.app])
|
||||
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
@ -175,7 +181,7 @@ const Apps = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="system-xl-semibold grow truncate text-text-primary">{!hasFilterCondition ? t('apps.title', { ns: 'explore' }) : t('apps.resultNum', { num: searchFilteredList.length, ns: 'explore' })}</div>
|
||||
<div className="grow truncate text-text-primary system-xl-semibold">{!hasFilterCondition ? t('apps.title', { ns: 'explore' }) : t('apps.resultNum', { num: searchFilteredList.length, ns: 'explore' })}</div>
|
||||
{hasFilterCondition && (
|
||||
<>
|
||||
<div className="mx-3 h-4 w-px bg-divider-regular"></div>
|
||||
@ -216,13 +222,13 @@ const Apps = ({
|
||||
{searchFilteredList.map(app => (
|
||||
<AppCard
|
||||
key={app.app_id}
|
||||
isExplore
|
||||
app={app}
|
||||
canCreate={hasEditPermission}
|
||||
onCreate={() => {
|
||||
setCurrApp(app)
|
||||
setIsShowCreateModal(true)
|
||||
}}
|
||||
onTry={handleTryApp}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
@ -255,9 +261,9 @@ const Apps = ({
|
||||
|
||||
{isShowTryAppPanel && (
|
||||
<TryApp
|
||||
appId={appParams?.appId || ''}
|
||||
app={appParams?.app}
|
||||
category={appParams?.app?.category}
|
||||
appId={currentTryApp?.appId || ''}
|
||||
app={currentTryApp?.app}
|
||||
category={currentTryApp?.app?.category}
|
||||
onClose={hideTryAppPanel}
|
||||
onCreate={handleShowFromTryApp}
|
||||
/>
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
'use client'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect } from 'react'
|
||||
import Sidebar from '@/app/components/explore/sidebar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
|
||||
const Explore = ({
|
||||
children,
|
||||
@ -16,47 +11,19 @@ const Explore = ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
const { data: membersData } = useMembers()
|
||||
|
||||
useDocumentTitle(t('menus.explore', { ns: 'common' }))
|
||||
|
||||
const userAccount = membersData?.accounts?.find(account => account.id === userProfile.id)
|
||||
const hasEditPermission = !!userAccount && userAccount.role !== 'normal'
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
return router.replace('/datasets')
|
||||
}, [isCurrentWorkspaceDatasetOperator])
|
||||
|
||||
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
|
||||
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
|
||||
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
|
||||
if (showTryAppPanel)
|
||||
setCurrentTryAppParams(params)
|
||||
else
|
||||
setCurrentTryAppParams(undefined)
|
||||
setIsShowTryAppPanel(showTryAppPanel)
|
||||
}
|
||||
router.replace('/datasets')
|
||||
}, [isCurrentWorkspaceDatasetOperator, router])
|
||||
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden border-t border-divider-regular bg-background-body">
|
||||
<ExploreContext.Provider
|
||||
value={
|
||||
{
|
||||
hasEditPermission,
|
||||
currentApp: currentTryAppParams,
|
||||
isShowTryAppPanel,
|
||||
setShowTryAppPanel,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Sidebar />
|
||||
<div className="h-full min-h-0 w-0 grow">
|
||||
{children}
|
||||
</div>
|
||||
</ExploreContext.Provider>
|
||||
<Sidebar />
|
||||
<div className="h-full min-h-0 w-0 grow">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -24,9 +24,9 @@ const InstalledApp = ({
|
||||
const updateAppParams = useWebAppStore(s => s.updateAppParams)
|
||||
const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta)
|
||||
const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp)
|
||||
const { isLoading: isLoadingWebAppAccessMode, data: webAppAccessMode, error: webAppAccessModeError } = useGetInstalledAppAccessModeByAppId(installedApp?.id ?? null)
|
||||
const { isLoading: isLoadingAppParams, data: appParams, error: appParamsError } = useGetInstalledAppParams(installedApp?.id ?? null)
|
||||
const { isLoading: isLoadingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null)
|
||||
const { isPending: isPendingWebAppAccessMode, data: webAppAccessMode, error: webAppAccessModeError } = useGetInstalledAppAccessModeByAppId(installedApp?.id ?? null)
|
||||
const { isPending: isPendingAppParams, data: appParams, error: appParamsError } = useGetInstalledAppParams(installedApp?.id ?? null)
|
||||
const { isPending: isPendingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null)
|
||||
const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: installedApp?.app.id, isInstalledApp: true })
|
||||
|
||||
useEffect(() => {
|
||||
@ -97,7 +97,7 @@ const InstalledApp = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isLoadingAppParams || isLoadingAppMeta || isLoadingWebAppAccessMode || isPendingInstalledApps) {
|
||||
if (isPendingInstalledApps || (installedApp && (isPendingAppParams || isPendingAppMeta || isPendingWebAppAccessMode))) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading />
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { CurrentTryAppParams } from './explore-context'
|
||||
import type { SetTryAppPanel, TryAppSelection } from '@/types/try-app'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext } from 'use-context-selector'
|
||||
|
||||
type Props = {
|
||||
currentApp?: CurrentTryAppParams
|
||||
currentApp?: TryAppSelection
|
||||
isShowTryAppPanel: boolean
|
||||
setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void
|
||||
setShowTryAppPanel: SetTryAppPanel
|
||||
controlHideCreateFromTemplatePanel: number
|
||||
}
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import type { App } from '@/models/explore'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext } from 'use-context-selector'
|
||||
|
||||
export type CurrentTryAppParams = {
|
||||
appId: string
|
||||
app: App
|
||||
}
|
||||
|
||||
export type IExplore = {
|
||||
hasEditPermission: boolean
|
||||
currentApp?: CurrentTryAppParams
|
||||
isShowTryAppPanel: boolean
|
||||
setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void
|
||||
}
|
||||
|
||||
const ExploreContext = createContext<IExplore>({
|
||||
hasEditPermission: false,
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: noop,
|
||||
currentApp: undefined,
|
||||
})
|
||||
|
||||
export default ExploreContext
|
||||
95
web/contract/console/explore.ts
Normal file
95
web/contract/console/explore.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import type { AccessMode } from '@/models/access-control'
|
||||
import type { Banner } from '@/models/app'
|
||||
import type { App, AppCategory, InstalledApp } from '@/models/explore'
|
||||
import type { AppModeEnum } from '@/types/app'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
export type ExploreAppsResponse = {
|
||||
categories: AppCategory[]
|
||||
recommended_apps: App[]
|
||||
}
|
||||
|
||||
export type ExploreAppDetailResponse = {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
icon_background: string
|
||||
mode: AppModeEnum
|
||||
export_data: string
|
||||
can_trial?: boolean
|
||||
}
|
||||
|
||||
export type InstalledAppsResponse = {
|
||||
installed_apps: InstalledApp[]
|
||||
}
|
||||
|
||||
export type InstalledAppMutationResponse = {
|
||||
result: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type AppAccessModeResponse = {
|
||||
accessMode: AccessMode
|
||||
}
|
||||
|
||||
export const exploreAppsContract = base
|
||||
.route({
|
||||
path: '/explore/apps',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{ query?: { language?: string } }>())
|
||||
.output(type<ExploreAppsResponse>())
|
||||
|
||||
export const exploreAppDetailContract = base
|
||||
.route({
|
||||
path: '/explore/apps/{id}',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{ params: { id: string } }>())
|
||||
.output(type<ExploreAppDetailResponse | null>())
|
||||
|
||||
export const exploreInstalledAppsContract = base
|
||||
.route({
|
||||
path: '/installed-apps',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{ query?: { app_id?: string } }>())
|
||||
.output(type<InstalledAppsResponse>())
|
||||
|
||||
export const exploreInstalledAppUninstallContract = base
|
||||
.route({
|
||||
path: '/installed-apps/{id}',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(type<{ params: { id: string } }>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const exploreInstalledAppPinContract = base
|
||||
.route({
|
||||
path: '/installed-apps/{id}',
|
||||
method: 'PATCH',
|
||||
})
|
||||
.input(type<{
|
||||
params: { id: string }
|
||||
body: {
|
||||
is_pinned: boolean
|
||||
}
|
||||
}>())
|
||||
.output(type<InstalledAppMutationResponse>())
|
||||
|
||||
export const exploreInstalledAppAccessModeContract = base
|
||||
.route({
|
||||
path: '/enterprise/webapp/app/access-mode',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{ query: { appId: string } }>())
|
||||
.output(type<AppAccessModeResponse>())
|
||||
|
||||
export const exploreBannersContract = base
|
||||
.route({
|
||||
path: '/explore/banners',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{ query?: { language?: string } }>())
|
||||
.output(type<Banner[]>())
|
||||
@ -1,5 +1,14 @@
|
||||
import type { InferContractRouterInputs } from '@orpc/contract'
|
||||
import { bindPartnerStackContract, invoicesContract } from './console/billing'
|
||||
import {
|
||||
exploreAppDetailContract,
|
||||
exploreAppsContract,
|
||||
exploreBannersContract,
|
||||
exploreInstalledAppAccessModeContract,
|
||||
exploreInstalledAppPinContract,
|
||||
exploreInstalledAppsContract,
|
||||
exploreInstalledAppUninstallContract,
|
||||
} from './console/explore'
|
||||
import { systemFeaturesContract } from './console/system'
|
||||
import {
|
||||
triggerOAuthConfigContract,
|
||||
@ -31,6 +40,15 @@ export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRout
|
||||
|
||||
export const consoleRouterContract = {
|
||||
systemFeatures: systemFeaturesContract,
|
||||
explore: {
|
||||
apps: exploreAppsContract,
|
||||
appDetail: exploreAppDetailContract,
|
||||
installedApps: exploreInstalledAppsContract,
|
||||
uninstallInstalledApp: exploreInstalledAppUninstallContract,
|
||||
updateInstalledApp: exploreInstalledAppPinContract,
|
||||
appAccessMode: exploreInstalledAppAccessModeContract,
|
||||
banners: exploreBannersContract,
|
||||
},
|
||||
trialApps: {
|
||||
info: trialAppInfoContract,
|
||||
datasets: trialAppDatasetsContract,
|
||||
|
||||
@ -506,14 +506,8 @@
|
||||
}
|
||||
},
|
||||
"app/components/app/app-publisher/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 7
|
||||
},
|
||||
"tailwindcss/no-unnecessary-whitespace": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 6
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"app/components/app/app-publisher/suggested-action.tsx": {
|
||||
@ -1233,11 +1227,8 @@
|
||||
"react/no-nested-component-definitions": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 6
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/apps/empty.tsx": {
|
||||
@ -4053,16 +4044,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/explore/app-card/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/explore/app-list/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/explore/banner/banner-item.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 4
|
||||
|
||||
@ -1,30 +1,42 @@
|
||||
import type { AccessMode } from '@/models/access-control'
|
||||
import type { Banner } from '@/models/app'
|
||||
import type { App, AppCategory, InstalledApp } from '@/models/explore'
|
||||
import { del, get, patch } from './base'
|
||||
import type { ExploreAppDetailResponse } from '@/contract/console/explore'
|
||||
import { consoleClient } from './client'
|
||||
|
||||
export const fetchAppList = () => {
|
||||
return get<{
|
||||
categories: AppCategory[]
|
||||
recommended_apps: App[]
|
||||
}>('/explore/apps')
|
||||
export const fetchAppList = (language?: string) => {
|
||||
if (!language)
|
||||
return consoleClient.explore.apps({})
|
||||
|
||||
return consoleClient.explore.apps({
|
||||
query: { language },
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
export const fetchAppDetail = (id: string): Promise<any> => {
|
||||
return get(`/explore/apps/${id}`)
|
||||
export const fetchAppDetail = async (id: string): Promise<ExploreAppDetailResponse> => {
|
||||
const response = await consoleClient.explore.appDetail({
|
||||
params: { id },
|
||||
})
|
||||
if (!response)
|
||||
throw new Error('Recommended app not found')
|
||||
return response
|
||||
}
|
||||
|
||||
export const fetchInstalledAppList = (app_id?: string | null) => {
|
||||
return get<{ installed_apps: InstalledApp[] }>(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`)
|
||||
export const fetchInstalledAppList = (appId?: string | null) => {
|
||||
if (!appId)
|
||||
return consoleClient.explore.installedApps({})
|
||||
|
||||
return consoleClient.explore.installedApps({
|
||||
query: { app_id: appId },
|
||||
})
|
||||
}
|
||||
|
||||
export const uninstallApp = (id: string) => {
|
||||
return del(`/installed-apps/${id}`)
|
||||
return consoleClient.explore.uninstallInstalledApp({
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePinStatus = (id: string, isPinned: boolean) => {
|
||||
return patch(`/installed-apps/${id}`, {
|
||||
return consoleClient.explore.updateInstalledApp({
|
||||
params: { id },
|
||||
body: {
|
||||
is_pinned: isPinned,
|
||||
},
|
||||
@ -32,10 +44,16 @@ export const updatePinStatus = (id: string, isPinned: boolean) => {
|
||||
}
|
||||
|
||||
export const getAppAccessModeByAppId = (appId: string) => {
|
||||
return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`)
|
||||
return consoleClient.explore.appAccessMode({
|
||||
query: { appId },
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchBanners = (language?: string): Promise<Banner[]> => {
|
||||
const url = language ? `/explore/banners?language=${language}` : '/explore/banners'
|
||||
return get<Banner[]>(url)
|
||||
export const fetchBanners = (language?: string) => {
|
||||
if (!language)
|
||||
return consoleClient.explore.banners({})
|
||||
|
||||
return consoleClient.explore.banners({
|
||||
query: { language },
|
||||
})
|
||||
}
|
||||
|
||||
@ -3,11 +3,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { consoleQuery } from './client'
|
||||
import { fetchAppList, fetchBanners, fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore'
|
||||
import { AppSourceType, fetchAppMeta, fetchAppParams } from './share'
|
||||
|
||||
const NAME_SPACE = 'explore'
|
||||
|
||||
type ExploreAppListData = {
|
||||
categories: AppCategory[]
|
||||
allList: App[]
|
||||
@ -15,10 +14,14 @@ type ExploreAppListData = {
|
||||
|
||||
export const useExploreAppList = () => {
|
||||
const locale = useLocale()
|
||||
const exploreAppsInput = locale
|
||||
? { query: { language: locale } }
|
||||
: {}
|
||||
|
||||
return useQuery<ExploreAppListData>({
|
||||
queryKey: [NAME_SPACE, 'appList', locale],
|
||||
queryKey: [...consoleQuery.explore.apps.queryKey({ input: exploreAppsInput }), locale],
|
||||
queryFn: async () => {
|
||||
const { categories, recommended_apps } = await fetchAppList()
|
||||
const { categories, recommended_apps } = await fetchAppList(locale)
|
||||
return {
|
||||
categories,
|
||||
allList: [...recommended_apps].sort((a, b) => a.position - b.position),
|
||||
@ -29,7 +32,7 @@ export const useExploreAppList = () => {
|
||||
|
||||
export const useGetInstalledApps = () => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'installedApps'],
|
||||
queryKey: consoleQuery.explore.installedApps.queryKey({ input: {} }),
|
||||
queryFn: () => {
|
||||
return fetchInstalledAppList()
|
||||
},
|
||||
@ -39,10 +42,12 @@ export const useGetInstalledApps = () => {
|
||||
export const useUninstallApp = () => {
|
||||
const client = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'uninstallApp'],
|
||||
mutationKey: consoleQuery.explore.uninstallInstalledApp.mutationKey(),
|
||||
mutationFn: (appId: string) => uninstallApp(appId),
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] })
|
||||
client.invalidateQueries({
|
||||
queryKey: consoleQuery.explore.installedApps.queryKey({ input: {} }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -50,25 +55,33 @@ export const useUninstallApp = () => {
|
||||
export const useUpdateAppPinStatus = () => {
|
||||
const client = useQueryClient()
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'updateAppPinStatus'],
|
||||
mutationKey: consoleQuery.explore.updateInstalledApp.mutationKey(),
|
||||
mutationFn: ({ appId, isPinned }: { appId: string, isPinned: boolean }) => updatePinStatus(appId, isPinned),
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] })
|
||||
client.invalidateQueries({
|
||||
queryKey: consoleQuery.explore.installedApps.queryKey({ input: {} }),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => {
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const appAccessModeInput = { query: { appId: appId ?? '' } }
|
||||
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'appAccessMode', appId, systemFeatures.webapp_auth.enabled],
|
||||
queryKey: [
|
||||
...consoleQuery.explore.appAccessMode.queryKey({ input: appAccessModeInput }),
|
||||
systemFeatures.webapp_auth.enabled,
|
||||
appId,
|
||||
],
|
||||
queryFn: () => {
|
||||
if (systemFeatures.webapp_auth.enabled === false) {
|
||||
return {
|
||||
accessMode: AccessMode.PUBLIC,
|
||||
}
|
||||
}
|
||||
if (!appId || appId.length === 0)
|
||||
if (!appId)
|
||||
return Promise.reject(new Error('App code is required to get access mode'))
|
||||
|
||||
return getAppAccessModeByAppId(appId)
|
||||
@ -79,7 +92,7 @@ export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => {
|
||||
|
||||
export const useGetInstalledAppParams = (appId: string | null) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'appParams', appId],
|
||||
queryKey: ['explore', 'appParams', appId],
|
||||
queryFn: () => {
|
||||
if (!appId || appId.length === 0)
|
||||
return Promise.reject(new Error('App ID is required to get app params'))
|
||||
@ -91,7 +104,7 @@ export const useGetInstalledAppParams = (appId: string | null) => {
|
||||
|
||||
export const useGetInstalledAppMeta = (appId: string | null) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'appMeta', appId],
|
||||
queryKey: ['explore', 'appMeta', appId],
|
||||
queryFn: () => {
|
||||
if (!appId || appId.length === 0)
|
||||
return Promise.reject(new Error('App ID is required to get app meta'))
|
||||
@ -102,8 +115,12 @@ export const useGetInstalledAppMeta = (appId: string | null) => {
|
||||
}
|
||||
|
||||
export const useGetBanners = (locale?: string) => {
|
||||
const bannersInput = locale
|
||||
? { query: { language: locale } }
|
||||
: {}
|
||||
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'banners', locale],
|
||||
queryKey: [...consoleQuery.explore.banners.queryKey({ input: bannersInput }), locale],
|
||||
queryFn: () => {
|
||||
return fetchBanners(locale)
|
||||
},
|
||||
|
||||
8
web/types/try-app.ts
Normal file
8
web/types/try-app.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { App } from '@/models/explore'
|
||||
|
||||
export type TryAppSelection = {
|
||||
appId: string
|
||||
app: App
|
||||
}
|
||||
|
||||
export type SetTryAppPanel = (showTryAppPanel: boolean, params?: TryAppSelection) => void
|
||||
Reference in New Issue
Block a user