mirror of
https://github.com/langgenius/dify.git
synced 2026-01-21 06:24:01 +00:00
Compare commits
11 Commits
deploy/dev
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ea88d185 | ||
|
|
6f40d79538 | ||
|
|
171dd120ca | ||
|
|
e71ebc19e6 | ||
|
|
19137c173f | ||
|
|
e69f1a1f46 | ||
|
|
14f0c5dd38 | ||
|
|
23f1adc833 | ||
|
|
1fe46ce0b8 | ||
|
|
b3acb74331 | ||
|
|
91856b09ca |
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||
import type { App } from '@/types/app'
|
||||
import {
|
||||
RiDashboard2Fill,
|
||||
RiDashboard2Line,
|
||||
@@ -12,13 +11,11 @@ import {
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import { useUnmount } from 'ahooks'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@@ -26,7 +23,7 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from './style.module.css'
|
||||
@@ -41,47 +38,41 @@ export type IAppDetailLayoutProps = {
|
||||
}
|
||||
|
||||
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
appId, // get appId in path
|
||||
} = props
|
||||
const { children, appId } = props
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, currentWorkspace } = useAppContext()
|
||||
const { appDetail, setAppDetail, setAppSidebarExpand } = useStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
setAppDetail: state.setAppDetail,
|
||||
setAppSidebarExpand: state.setAppSidebarExpand,
|
||||
})))
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
|
||||
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
|
||||
const [navigation, setNavigation] = useState<Array<{
|
||||
name: string
|
||||
href: string
|
||||
icon: NavIcon
|
||||
selectedIcon: NavIcon
|
||||
}>>([])
|
||||
|
||||
const getNavigationConfig = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: AppModeEnum) => {
|
||||
const navConfig = [
|
||||
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext()
|
||||
const setAppSidebarExpand = useStore(s => s.setAppSidebarExpand)
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
|
||||
const { data: appDetail, isPending, error } = useAppDetail(appId)
|
||||
|
||||
const navigation = useMemo(() => {
|
||||
if (!appDetail)
|
||||
return []
|
||||
|
||||
const mode = appDetail.mode
|
||||
const isWorkflowMode = mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
return [
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: t('appMenus.promptEng', { ns: 'common' }),
|
||||
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
|
||||
icon: RiTerminalWindowLine,
|
||||
selectedIcon: RiTerminalWindowFill,
|
||||
href: `/app/${appId}/${isWorkflowMode ? 'workflow' : 'configuration'}`,
|
||||
icon: RiTerminalWindowLine as NavIcon,
|
||||
selectedIcon: RiTerminalWindowFill as NavIcon,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('appMenus.apiAccess', { ns: 'common' }),
|
||||
href: `/app/${appId}/develop`,
|
||||
icon: RiTerminalBoxLine,
|
||||
selectedIcon: RiTerminalBoxFill,
|
||||
icon: RiTerminalBoxLine as NavIcon,
|
||||
selectedIcon: RiTerminalBoxFill as NavIcon,
|
||||
},
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
@@ -89,74 +80,64 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
? t('appMenus.logAndAnn', { ns: 'common' })
|
||||
: t('appMenus.logs', { ns: 'common' }),
|
||||
href: `/app/${appId}/logs`,
|
||||
icon: RiFileList3Line,
|
||||
selectedIcon: RiFileList3Fill,
|
||||
icon: RiFileList3Line as NavIcon,
|
||||
selectedIcon: RiFileList3Fill as NavIcon,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('appMenus.overview', { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
icon: RiDashboard2Line as NavIcon,
|
||||
selectedIcon: RiDashboard2Fill as NavIcon,
|
||||
},
|
||||
]
|
||||
return navConfig
|
||||
}, [t])
|
||||
}, [appDetail, appId, isCurrentWorkspaceEditor, t])
|
||||
|
||||
useDocumentTitle(appDetail?.name || t('menus.appDetail', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (appDetail) {
|
||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||
const mode = isMobile ? 'collapse' : 'expand'
|
||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||
// TODO: consider screen size and mode
|
||||
// if ((appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
|
||||
// setAppSidebarExpand('collapse')
|
||||
}
|
||||
}, [appDetail, isMobile])
|
||||
|
||||
useEffect(() => {
|
||||
setAppDetail()
|
||||
setIsLoadingAppDetail(true)
|
||||
fetchAppDetailDirect({ url: '/apps', id: appId }).then((res: App) => {
|
||||
setAppDetailRes(res)
|
||||
}).catch((e: any) => {
|
||||
if (e.status === 404)
|
||||
router.replace('/apps')
|
||||
}).finally(() => {
|
||||
setIsLoadingAppDetail(false)
|
||||
})
|
||||
}, [appId, pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (!appDetailRes || !currentWorkspace.id || isLoadingCurrentWorkspace || isLoadingAppDetail)
|
||||
if (!appDetail)
|
||||
return
|
||||
const res = appDetailRes
|
||||
// redirection
|
||||
const canIEditApp = isCurrentWorkspaceEditor
|
||||
if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
|
||||
router.replace(`/app/${appId}/overview`)
|
||||
return
|
||||
}
|
||||
if ((res.mode === AppModeEnum.WORKFLOW || res.mode === AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('configuration')) {
|
||||
router.replace(`/app/${appId}/workflow`)
|
||||
}
|
||||
else if ((res.mode !== AppModeEnum.WORKFLOW && res.mode !== AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('workflow')) {
|
||||
router.replace(`/app/${appId}/configuration`)
|
||||
if (isMobile) {
|
||||
setAppSidebarExpand('collapse')
|
||||
}
|
||||
else {
|
||||
setAppDetail({ ...res, enable_sso: false })
|
||||
setNavigation(getNavigationConfig(appId, isCurrentWorkspaceEditor, res.mode))
|
||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||
setAppSidebarExpand(localeMode)
|
||||
}
|
||||
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace])
|
||||
}, [appDetail, isMobile, setAppSidebarExpand])
|
||||
|
||||
useUnmount(() => {
|
||||
setAppDetail()
|
||||
})
|
||||
useEffect(() => {
|
||||
if (!appDetail || isLoadingCurrentWorkspace)
|
||||
return
|
||||
|
||||
if (!appDetail) {
|
||||
const mode = appDetail.mode
|
||||
const isWorkflowMode = mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
if (!isCurrentWorkspaceEditor) {
|
||||
const restrictedPaths = ['configuration', 'workflow', 'logs']
|
||||
if (restrictedPaths.some(p => pathname.endsWith(p))) {
|
||||
router.replace(`/app/${appId}/overview`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isWorkflowMode && pathname.endsWith('configuration'))
|
||||
router.replace(`/app/${appId}/workflow`)
|
||||
else if (!isWorkflowMode && pathname.endsWith('workflow'))
|
||||
router.replace(`/app/${appId}/configuration`)
|
||||
}, [appDetail, isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, pathname, appId, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const httpError = error as { status?: number }
|
||||
if (httpError.status === 404)
|
||||
router.replace('/apps')
|
||||
}
|
||||
}, [error, router])
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
@@ -164,13 +145,12 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
if (!appDetail)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={cn(s.app, 'relative flex', 'overflow-hidden')}>
|
||||
{appDetail && (
|
||||
<AppSideBar
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
<AppSideBar navigation={navigation} />
|
||||
<div className="grow overflow-hidden bg-components-panel-bg">
|
||||
{children}
|
||||
</div>
|
||||
@@ -180,4 +160,5 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetailLayout)
|
||||
|
||||
@@ -5,13 +5,11 @@ import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import type { App } from '@/types/app'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppCard from '@/app/components/app/overview/app-card'
|
||||
import TriggerCard from '@/app/components/app/overview/trigger-card'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
@@ -19,11 +17,11 @@ import { isTriggerNode } from '@/app/components/workflow/types'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
updateAppSiteAccessToken,
|
||||
updateAppSiteConfig,
|
||||
updateAppSiteStatus,
|
||||
} from '@/service/apps'
|
||||
import { useAppDetail, useInvalidateAppDetail } from '@/service/use-apps'
|
||||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
@@ -38,8 +36,8 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const { data: appDetail } = useAppDetail(appId)
|
||||
const invalidateAppDetail = useInvalidateAppDetail()
|
||||
|
||||
const isWorkflowApp = appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
const showMCPCard = isInPanel
|
||||
@@ -89,21 +87,13 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' }))
|
||||
: null
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
try {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appId })
|
||||
setAppDetail({ ...res })
|
||||
}
|
||||
catch (error) { console.error(error) }
|
||||
}
|
||||
|
||||
const handleCallbackResult = (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => {
|
||||
const type = err ? 'error' : 'success'
|
||||
|
||||
message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
|
||||
|
||||
if (type === 'success')
|
||||
updateAppDetail()
|
||||
invalidateAppDetail(appId)
|
||||
|
||||
notify({
|
||||
type,
|
||||
|
||||
@@ -8,8 +8,8 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TIME_PERIOD_MAPPING as LONG_TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
|
||||
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import LongTimeRangePicker from './long-time-range-picker'
|
||||
import TimeRangePicker from './time-range-picker'
|
||||
|
||||
@@ -34,7 +34,7 @@ export type IChartViewProps = {
|
||||
|
||||
export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
||||
const { t } = useTranslation()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { data: appDetail } = useAppDetail(appId)
|
||||
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
|
||||
const isWorkflow = appDetail?.mode === 'workflow'
|
||||
const [period, setPeriod] = useState<PeriodParams>(IS_CLOUD_EDITION
|
||||
|
||||
@@ -12,13 +12,12 @@ import {
|
||||
RiFileUploadLine,
|
||||
} from '@remixicon/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import ContentDialog from '@/app/components/base/content-dialog'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
@@ -26,7 +25,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { useInvalidateAppList } from '@/service/use-apps'
|
||||
import { useAppDetail, useInvalidateAppDetail, useInvalidateAppList } from '@/service/use-apps'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
@@ -64,9 +63,10 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { replace } = useRouter()
|
||||
const { appId } = useParams()
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const invalidateAppDetail = useInvalidateAppDetail()
|
||||
const invalidateAppList = useInvalidateAppList()
|
||||
const [open, setOpen] = useState(openState)
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
@@ -89,7 +89,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
if (!appDetail)
|
||||
return
|
||||
try {
|
||||
const app = await updateAppInfo({
|
||||
await updateAppInfo({
|
||||
appID: appDetail.id,
|
||||
name,
|
||||
icon_type,
|
||||
@@ -104,12 +104,12 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
type: 'success',
|
||||
message: t('editDone', { ns: 'app' }),
|
||||
})
|
||||
setAppDetail(app)
|
||||
invalidateAppDetail(appId as string)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('editFailed', { ns: 'app' }) })
|
||||
}
|
||||
}, [appDetail, notify, setAppDetail, t])
|
||||
}, [appDetail, notify, invalidateAppDetail, appId, t])
|
||||
|
||||
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
|
||||
if (!appDetail)
|
||||
@@ -195,7 +195,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) })
|
||||
invalidateAppList()
|
||||
onPlanInfoChanged()
|
||||
setAppDetail()
|
||||
replace('/apps')
|
||||
}
|
||||
catch (e: any) {
|
||||
@@ -205,7 +204,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
})
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [appDetail, invalidateAppList, notify, onPlanInfoChanged, replace, setAppDetail, t])
|
||||
}, [appDetail, invalidateAppList, notify, onPlanInfoChanged, replace, t])
|
||||
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
|
||||
@@ -242,7 +241,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
]
|
||||
|
||||
const secondaryOperations: Operation[] = [
|
||||
// Import DSL (conditional)
|
||||
...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW)
|
||||
? [{
|
||||
id: 'import',
|
||||
@@ -255,7 +253,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
},
|
||||
}]
|
||||
: [],
|
||||
// Divider
|
||||
{
|
||||
id: 'divider-1',
|
||||
title: '',
|
||||
@@ -263,7 +260,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
onClick: () => { /* divider has no action */ },
|
||||
type: 'divider' as const,
|
||||
},
|
||||
// Delete operation
|
||||
{
|
||||
id: 'delete',
|
||||
title: t('operation.delete', { ns: 'common' }),
|
||||
@@ -276,7 +272,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
},
|
||||
]
|
||||
|
||||
// Keep the switch operation separate as it's not part of the main operations
|
||||
const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT)
|
||||
? {
|
||||
id: 'switch',
|
||||
@@ -370,11 +365,9 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('types.advanced', { ns: 'app' }) : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('types.agent', { ns: 'app' }) : appDetail.mode === AppModeEnum.CHAT ? t('types.chatbot', { ns: 'app' }) : appDetail.mode === AppModeEnum.COMPLETION ? t('types.completion', { ns: 'app' }) : t('types.workflow', { ns: 'app' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* description */}
|
||||
{appDetail.description && (
|
||||
<div className="system-xs-regular overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal break-words text-text-tertiary">{appDetail.description}</div>
|
||||
)}
|
||||
{/* operations */}
|
||||
<AppOperations
|
||||
gap={4}
|
||||
primaryOperations={primaryOperations}
|
||||
@@ -386,7 +379,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
isInPanel={true}
|
||||
className="flex flex-1 flex-col gap-2 overflow-auto px-2 py-1"
|
||||
/>
|
||||
{/* Switch operation (if available) */}
|
||||
{switchOperation && (
|
||||
<div className="flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch pb-2">
|
||||
<Button
|
||||
|
||||
@@ -3,16 +3,17 @@ import {
|
||||
RiEqualizer2Line,
|
||||
RiMenuLine,
|
||||
} from '@remixicon/react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import AppIcon from '../base/app-icon'
|
||||
@@ -31,8 +32,9 @@ type Props = {
|
||||
|
||||
const AppSidebarDropdown = ({ navigation }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appId } = useParams()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const [detailExpand, setDetailExpand] = useState(false)
|
||||
|
||||
const [open, doSetOpen] = useState(false)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
RiVerifiedBadgeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
|
||||
@@ -41,8 +41,8 @@ import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { useAppDetail, useInvalidateAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Divider from '../../base/divider'
|
||||
@@ -139,15 +139,15 @@ const AppPublisher = ({
|
||||
startNodeLimitExceeded = false,
|
||||
}: AppPublisherProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { appId } = useParams()
|
||||
|
||||
const [published, setPublished] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
||||
const [isAppAccessSet, setIsAppAccessSet] = useState(true)
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const invalidateAppDetail = useInvalidateAppDetail()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
@@ -177,16 +177,12 @@ const AppPublisher = ({
|
||||
refetch()
|
||||
}, [open, appDetail, refetch, systemFeatures])
|
||||
|
||||
useEffect(() => {
|
||||
if (appDetail && appAccessSubjects) {
|
||||
if (appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0)
|
||||
setIsAppAccessSet(false)
|
||||
else
|
||||
setIsAppAccessSet(true)
|
||||
}
|
||||
else {
|
||||
setIsAppAccessSet(true)
|
||||
}
|
||||
const isAppAccessSet = useMemo(() => {
|
||||
if (!appDetail || !appAccessSubjects)
|
||||
return true
|
||||
if (appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0)
|
||||
return false
|
||||
return true
|
||||
}, [appAccessSubjects, appDetail])
|
||||
|
||||
const handlePublish = useCallback(async (params?: ModelAndParameter | PublishWorkflowParams) => {
|
||||
@@ -239,16 +235,15 @@ const AppPublisher = ({
|
||||
}, [appDetail?.id, openAsyncWindow])
|
||||
|
||||
const handleAccessControlUpdate = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
if (!appId)
|
||||
return
|
||||
try {
|
||||
const res = await fetchAppDetailDirect({ url: '/apps', id: appDetail.id })
|
||||
setAppDetail(res)
|
||||
invalidateAppDetail(appId as string)
|
||||
}
|
||||
finally {
|
||||
setShowAppAccessControl(false)
|
||||
}
|
||||
}, [appDetail, setAppDetail])
|
||||
}, [appId, invalidateAppDetail])
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -4,11 +4,11 @@ import type { Item as SelectItem } from './type-select'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { InputVar, MoreInfo, UploadFileSetting } from '@/app/components/workflow/types'
|
||||
import { produce } from 'immer'
|
||||
import { useParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import Input from '@/app/components/base/input'
|
||||
@@ -22,6 +22,7 @@ import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { ChangeType, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum, TransferMethod } from '@/types/app'
|
||||
import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
|
||||
import ConfigSelect from '../config-select'
|
||||
@@ -72,10 +73,11 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
}) => {
|
||||
const { modelConfig } = useContext(ConfigContext)
|
||||
const { t } = useTranslation()
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const [tempPayload, setTempPayload] = useState<InputVar>(() => normalizeSelectDefaultValue(payload || getNewVarInWorkflow('') as any))
|
||||
const { type, label, variable, options, max_length } = tempPayload
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const isBasicApp = appDetail?.mode !== AppModeEnum.ADVANCED_CHAT && appDetail?.mode !== AppModeEnum.WORKFLOW
|
||||
const jsonSchemaStr = useMemo(() => {
|
||||
const isJsonObject = type === InputVarType.jsonObject
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { ReactNode } from 'react'
|
||||
import type { IConfigVarProps } from './index'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import type { App } from '@/types/app'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import DebugConfigurationContext from '@/context/debug-configuration'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index'
|
||||
@@ -38,6 +40,15 @@ vi.mock('@/context/modal-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({
|
||||
appId: 'test-app-id',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-apps')
|
||||
const mockUseAppDetail = vi.mocked(useAppDetail)
|
||||
|
||||
type SortableItem = {
|
||||
id: string
|
||||
variable: PromptVariable
|
||||
@@ -85,6 +96,18 @@ const createPromptVariable = (overrides: Partial<PromptVariable> = {}): PromptVa
|
||||
}
|
||||
}
|
||||
|
||||
function setupUseAppDetailMock() {
|
||||
mockUseAppDetail.mockReturnValue({
|
||||
data: {
|
||||
id: 'test-app-id',
|
||||
mode: AppModeEnum.CHAT,
|
||||
} as App,
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useAppDetail>)
|
||||
}
|
||||
|
||||
const renderConfigVar = (props: Partial<IConfigVarProps> = {}, debugOverrides: Partial<DebugConfigurationState> = {}) => {
|
||||
const defaultProps: IConfigVarProps = {
|
||||
promptVariables: [],
|
||||
@@ -219,6 +242,7 @@ describe('ConfigVar', () => {
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
setupUseAppDetailMock()
|
||||
})
|
||||
|
||||
it('should save updates when editing a basic variable', async () => {
|
||||
|
||||
@@ -71,9 +71,10 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { fetchAppDetailDirect, updateAppModelConfig } from '@/service/apps'
|
||||
import { updateAppModelConfig } from '@/service/apps'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { fetchCollectionList } from '@/service/tools'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
import { AgentStrategy, AppModeEnum, ModelModeType, Resolution, RETRIEVE_TYPE, TransferMethod } from '@/types/app'
|
||||
import {
|
||||
@@ -95,22 +96,22 @@ const Configuration: FC = () => {
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { isLoadingCurrentWorkspace, currentWorkspace } = useAppContext()
|
||||
|
||||
const { appDetail, showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
const { showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
|
||||
setAppSidebarExpand: state.setAppSidebarExpand,
|
||||
showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal,
|
||||
setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal,
|
||||
})))
|
||||
const { data: fileUploadConfigResponse } = useFileUploadConfig()
|
||||
const pathname = usePathname()
|
||||
const matched = pathname.match(/\/app\/([^/]+)/)
|
||||
const appId = (matched?.length && matched[1]) ? matched[1] : ''
|
||||
const { data: appDetail } = useAppDetail(appId)
|
||||
|
||||
const latestPublishedAt = useMemo(() => appDetail?.model_config?.updated_at, [appDetail])
|
||||
const [formattingChanged, setFormattingChanged] = useState(false)
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
|
||||
const isLoading = !hasFetchedDetail
|
||||
const pathname = usePathname()
|
||||
const matched = pathname.match(/\/app\/([^/]+)/)
|
||||
const appId = (matched?.length && matched[1]) ? matched[1] : ''
|
||||
const [mode, setMode] = useState<AppModeEnum>(AppModeEnum.CHAT)
|
||||
const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null)
|
||||
|
||||
@@ -548,7 +549,10 @@ const Configuration: FC = () => {
|
||||
}, [modelConfig])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!appDetail)
|
||||
return
|
||||
|
||||
const initConfig = async () => {
|
||||
const collectionList = await fetchCollectionList()
|
||||
if (basePath) {
|
||||
collectionList.forEach((item) => {
|
||||
@@ -557,9 +561,8 @@ const Configuration: FC = () => {
|
||||
})
|
||||
}
|
||||
setCollectionList(collectionList)
|
||||
const res = await fetchAppDetailDirect({ url: '/apps', id: appId })
|
||||
setMode(res.mode as AppModeEnum)
|
||||
const modelConfig = res.model_config as BackendModelConfig
|
||||
setMode(appDetail.mode as AppModeEnum)
|
||||
const modelConfig = appDetail.model_config as BackendModelConfig
|
||||
const promptMode = modelConfig.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
|
||||
doSetPromptMode(promptMode)
|
||||
if (promptMode === PromptMode.advanced) {
|
||||
@@ -669,7 +672,7 @@ const Configuration: FC = () => {
|
||||
external_data_tools: modelConfig.external_data_tools ?? [],
|
||||
system_parameters: modelConfig.system_parameters,
|
||||
dataSets: datasets || [],
|
||||
agentConfig: res.mode === AppModeEnum.AGENT_CHAT ? {
|
||||
agentConfig: appDetail.mode === AppModeEnum.AGENT_CHAT ? {
|
||||
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
|
||||
...modelConfig.agent_mode,
|
||||
// remove dataset
|
||||
@@ -680,7 +683,7 @@ const Configuration: FC = () => {
|
||||
const toolInCollectionList = collectionList.find(c => tool.provider_id === c.id)
|
||||
return {
|
||||
...tool,
|
||||
isDeleted: res.deleted_tools?.some((deletedTool: any) => deletedTool.provider_id === tool.provider_id && deletedTool.tool_name === tool.tool_name) ?? false,
|
||||
isDeleted: appDetail.deleted_tools?.some((deletedTool: any) => deletedTool.provider_id === tool.provider_id && deletedTool.tool_name === tool.tool_name) ?? false,
|
||||
notAuthor: toolInCollectionList?.is_team_authorization === false,
|
||||
...(tool.provider_type === 'builtin'
|
||||
? {
|
||||
@@ -726,8 +729,10 @@ const Configuration: FC = () => {
|
||||
datasetConfigsToSet.retrieval_model = datasetConfigsToSet.retrieval_model ?? RETRIEVE_TYPE.multiWay
|
||||
setDatasetConfigs(datasetConfigsToSet)
|
||||
setHasFetchedDetail(true)
|
||||
})()
|
||||
}, [appId])
|
||||
}
|
||||
|
||||
initConfig()
|
||||
}, [appDetail])
|
||||
|
||||
const promptEmpty = (() => {
|
||||
if (mode !== AppModeEnum.COMPLETION)
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import type { App, AppIconType } from '@/types/app'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import LogAnnotation from './index'
|
||||
|
||||
vi.mock('@/service/use-apps')
|
||||
const mockUseAppDetail = vi.mocked(useAppDetail)
|
||||
|
||||
const mockRouterPush = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockRouterPush,
|
||||
}),
|
||||
useParams: () => ({
|
||||
appId: 'app-123',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/annotation', () => ({
|
||||
@@ -61,17 +67,25 @@ const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
function mockAppDetailReturn(app: App | undefined) {
|
||||
mockUseAppDetail.mockReturnValue({
|
||||
data: app,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useAppDetail>)
|
||||
}
|
||||
|
||||
describe('LogAnnotation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
mockAppDetailReturn(createMockApp())
|
||||
})
|
||||
|
||||
// Rendering behavior
|
||||
describe('Rendering', () => {
|
||||
it('should render loading state when app detail is missing', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: undefined })
|
||||
mockAppDetailReturn(undefined)
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
@@ -82,7 +96,7 @@ describe('LogAnnotation', () => {
|
||||
|
||||
it('should render log and annotation tabs for non-completion apps', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
mockAppDetailReturn(createMockApp({ mode: AppModeEnum.CHAT }))
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
@@ -94,7 +108,7 @@ describe('LogAnnotation', () => {
|
||||
|
||||
it('should render only log tab for completion apps', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.COMPLETION }) })
|
||||
mockAppDetailReturn(createMockApp({ mode: AppModeEnum.COMPLETION }))
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
@@ -106,7 +120,7 @@ describe('LogAnnotation', () => {
|
||||
|
||||
it('should hide tabs and render workflow log in workflow mode', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.WORKFLOW }) })
|
||||
mockAppDetailReturn(createMockApp({ mode: AppModeEnum.WORKFLOW }))
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
@@ -121,7 +135,7 @@ describe('LogAnnotation', () => {
|
||||
describe('Props', () => {
|
||||
it('should render log content when page type is log', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
mockAppDetailReturn(createMockApp({ mode: AppModeEnum.CHAT }))
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
@@ -133,7 +147,7 @@ describe('LogAnnotation', () => {
|
||||
|
||||
it('should render annotation content when page type is annotation', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
mockAppDetailReturn(createMockApp({ mode: AppModeEnum.CHAT }))
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.annotation} />)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Annotation from '@/app/components/app/annotation'
|
||||
import Log from '@/app/components/app/log'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import WorkflowLog from '@/app/components/app/workflow-log'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@@ -23,7 +23,8 @@ const LogAnnotation: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (appDetail?.mode === AppModeEnum.COMPLETION)
|
||||
|
||||
@@ -19,7 +19,6 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppBasic from '@/app/components/app-sidebar/basic'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
@@ -35,7 +34,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { useAppDetail, useInvalidateAppDetail } from '@/service/use-apps'
|
||||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
@@ -73,11 +72,11 @@ function AppCard({
|
||||
}: IAppCardProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const invalidateAppDetail = useInvalidateAppDetail()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { data: currentWorkflow } = useAppWorkflow(appInfo.mode === AppModeEnum.WORKFLOW ? appInfo.id : '')
|
||||
const docLink = useDocLink()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const { data: appDetail } = useAppDetail(appInfo.id)
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
const [showEmbedded, setShowEmbedded] = useState(false)
|
||||
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
|
||||
@@ -178,16 +177,10 @@ function AppCard({
|
||||
return
|
||||
setShowAccessControl(true)
|
||||
}, [appDetail])
|
||||
const handleAccessControlUpdate = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchAppDetailDirect({ url: '/apps', id: appDetail!.id })
|
||||
setAppDetail(res)
|
||||
setShowAccessControl(false)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch app detail:', error)
|
||||
}
|
||||
}, [appDetail, setAppDetail])
|
||||
const handleAccessControlUpdate = useCallback(() => {
|
||||
invalidateAppDetail(appInfo.id)
|
||||
setShowAccessControl(false)
|
||||
}, [invalidateAppDetail, appInfo.id])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { App, AppSSO } from '@/types/app'
|
||||
import { create } from 'zustand'
|
||||
|
||||
type State = {
|
||||
appDetail?: App & Partial<AppSSO>
|
||||
appSidebarExpand: string
|
||||
currentLogItem?: IChatItem
|
||||
currentLogModalActiveTab: string
|
||||
@@ -14,7 +12,6 @@ type State = {
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setAppDetail: (appDetail?: App & Partial<AppSSO>) => void
|
||||
setAppSidebarExpand: (state: string) => void
|
||||
setCurrentLogItem: (item?: IChatItem) => void
|
||||
setCurrentLogModalActiveTab: (tab: string) => void
|
||||
@@ -25,8 +22,6 @@ type Action = {
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>(set => ({
|
||||
appDetail: undefined,
|
||||
setAppDetail: appDetail => set(() => ({ appDetail })),
|
||||
appSidebarExpand: '',
|
||||
setAppSidebarExpand: appSidebarExpand => set(() => ({ appSidebarExpand })),
|
||||
currentLogItem: undefined,
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { App } from '@/types/app'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
@@ -16,9 +15,13 @@ vi.mock('next/navigation', () => ({
|
||||
push: mockPush,
|
||||
replace: mockReplace,
|
||||
}),
|
||||
useParams: () => ({ appId: 'app-123' }),
|
||||
}))
|
||||
|
||||
// Use real store - global zustand mock will auto-reset between tests
|
||||
const mockInvalidateAppDetail = vi.fn()
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useInvalidateAppDetail: () => mockInvalidateAppDetail,
|
||||
}))
|
||||
|
||||
const mockSwitchApp = vi.fn()
|
||||
const mockDeleteApp = vi.fn()
|
||||
@@ -135,17 +138,9 @@ const renderComponent = (overrides: Partial<React.ComponentProps<typeof SwitchAp
|
||||
}
|
||||
}
|
||||
|
||||
const setAppDetailSpy = vi.fn()
|
||||
|
||||
describe('SwitchAppModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Spy on setAppDetail
|
||||
const originalSetAppDetail = useAppStore.getState().setAppDetail
|
||||
setAppDetailSpy.mockImplementation((...args: Parameters<typeof originalSetAppDetail>) => {
|
||||
originalSetAppDetail(...args)
|
||||
})
|
||||
useAppStore.setState({ setAppDetail: setAppDetailSpy as typeof originalSetAppDetail })
|
||||
mockIsEditor = true
|
||||
mockEnableBilling = false
|
||||
mockPlan = {
|
||||
@@ -281,7 +276,7 @@ describe('SwitchAppModal', () => {
|
||||
})
|
||||
expect(mockReplace).toHaveBeenCalledWith('/app/new-app-002/workflow')
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
expect(setAppDetailSpy).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAppDetail).toHaveBeenCalledWith('app-123')
|
||||
})
|
||||
|
||||
it('should notify error when switch app fails', async () => {
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import type { App } from '@/types/app'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
@@ -21,6 +20,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { deleteApp, switchApp } from '@/service/apps'
|
||||
import { useInvalidateAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@@ -38,7 +38,8 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
const { push, replace } = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const { appId } = useParams()
|
||||
const invalidateAppDetail = useInvalidateAppDetail()
|
||||
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
@@ -70,7 +71,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
onClose()
|
||||
notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) })
|
||||
if (inAppDetail)
|
||||
setAppDetail()
|
||||
invalidateAppDetail(appId as string)
|
||||
if (removeOriginal)
|
||||
await deleteApp(appDetail.id)
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
|
||||
@@ -11,18 +11,24 @@
|
||||
import type { App, AppIconType, AppModeEnum } from '@/types/app'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import DetailPanel from './detail'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
vi.mock('@/service/use-apps')
|
||||
const mockUseAppDetail = vi.mocked(useAppDetail)
|
||||
|
||||
const mockRouterPush = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockRouterPush,
|
||||
}),
|
||||
useParams: () => ({
|
||||
appId: 'test-app-id',
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the Run component as it has complex dependencies
|
||||
@@ -88,6 +94,18 @@ const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function mockAppDetailReturn(app: App | undefined) {
|
||||
mockUseAppDetail.mockReturnValue({
|
||||
data: app,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useAppDetail>)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
@@ -97,7 +115,7 @@ describe('DetailPanel', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
mockAppDetailReturn(createMockApp())
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -125,7 +143,7 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should render Run component with correct URLs', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-456' }) })
|
||||
mockAppDetailReturn(createMockApp({ id: 'app-456' }))
|
||||
|
||||
render(<DetailPanel runID="run-789" onClose={defaultOnClose} />)
|
||||
|
||||
@@ -185,7 +203,7 @@ describe('DetailPanel', () => {
|
||||
|
||||
it('should navigate to workflow page with replayRunId when replay button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-replay-test' }) })
|
||||
mockAppDetailReturn(createMockApp({ id: 'app-replay-test' }))
|
||||
|
||||
render(<DetailPanel runID="run-to-replay" onClose={defaultOnClose} canReplay={true} />)
|
||||
|
||||
@@ -197,7 +215,7 @@ describe('DetailPanel', () => {
|
||||
|
||||
it('should not navigate when replay clicked but appDetail is missing', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: undefined })
|
||||
mockAppDetailReturn(undefined)
|
||||
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
|
||||
|
||||
@@ -213,7 +231,7 @@ describe('DetailPanel', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
describe('URL Generation', () => {
|
||||
it('should generate correct run detail URL', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'my-app' }) })
|
||||
mockAppDetailReturn(createMockApp({ id: 'my-app' }))
|
||||
|
||||
render(<DetailPanel runID="my-run" onClose={defaultOnClose} />)
|
||||
|
||||
@@ -221,7 +239,7 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should generate correct tracing list URL', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'my-app' }) })
|
||||
mockAppDetailReturn(createMockApp({ id: 'my-app' }))
|
||||
|
||||
render(<DetailPanel runID="my-run" onClose={defaultOnClose} />)
|
||||
|
||||
@@ -229,7 +247,7 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should handle special characters in runID', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-id' }) })
|
||||
mockAppDetailReturn(createMockApp({ id: 'app-id' }))
|
||||
|
||||
render(<DetailPanel runID="run-with-special-123" onClose={defaultOnClose} />)
|
||||
|
||||
@@ -242,7 +260,7 @@ describe('DetailPanel', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Store Integration', () => {
|
||||
it('should read appDetail from store', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'store-app-id' }) })
|
||||
mockAppDetailReturn(createMockApp({ id: 'store-app-id' }))
|
||||
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
@@ -250,7 +268,7 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should handle undefined appDetail from store gracefully', () => {
|
||||
useAppStore.setState({ appDetail: undefined })
|
||||
mockAppDetailReturn(undefined)
|
||||
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
@@ -272,7 +290,7 @@ describe('DetailPanel', () => {
|
||||
|
||||
it('should handle very long runID', () => {
|
||||
const longRunId = 'a'.repeat(100)
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-id' }) })
|
||||
mockAppDetailReturn(createMockApp({ id: 'app-id' }))
|
||||
|
||||
render(<DetailPanel runID={longRunId} onClose={defaultOnClose} />)
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import TooltipPlus from '@/app/components/base/tooltip'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import Run from '@/app/components/workflow/run'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
|
||||
type ILogDetail = {
|
||||
runID: string
|
||||
@@ -16,7 +16,8 @@ type ILogDetail = {
|
||||
|
||||
const DetailPanel: FC<ILogDetail> = ({ runID, onClose, canReplay = false }) => {
|
||||
const { t } = useTranslation()
|
||||
const appDetail = useStore(state => state.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const router = useRouter()
|
||||
|
||||
const handleReplay = () => {
|
||||
|
||||
@@ -34,6 +34,18 @@ import Logs from './index'
|
||||
|
||||
vi.mock('@/service/use-log')
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useAppDetail: () => ({
|
||||
data: {
|
||||
id: 'test-app-id',
|
||||
name: 'Test App',
|
||||
mode: 'workflow',
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounce: <T,>(value: T) => value,
|
||||
useDebounceFn: (fn: (value: string) => void) => ({ run: fn }),
|
||||
@@ -51,6 +63,9 @@ vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
useParams: () => ({
|
||||
appId: 'test-app-id',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } fr
|
||||
import type { App, AppIconType, AppModeEnum } from '@/types/app'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
import { WorkflowRunTriggeredFrom } from '@/models/log'
|
||||
import WorkflowAppLogList from './list'
|
||||
@@ -27,6 +26,9 @@ vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockRouterPush,
|
||||
}),
|
||||
useParams: () => ({
|
||||
appId: 'test-app-id',
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useTimestamp hook
|
||||
@@ -86,6 +88,40 @@ vi.mock('ahooks', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock app detail data for useAppDetail hook
|
||||
const mockAppDetail: App = {
|
||||
id: 'test-app-id',
|
||||
name: 'Test App',
|
||||
description: 'Test app description',
|
||||
author_name: 'Test Author',
|
||||
icon_type: 'emoji' as AppIconType,
|
||||
icon: '',
|
||||
icon_background: '#FFEAD5',
|
||||
icon_url: null,
|
||||
use_icon_as_answer_icon: false,
|
||||
mode: 'workflow' as AppModeEnum,
|
||||
enable_site: true,
|
||||
enable_api: true,
|
||||
api_rpm: 60,
|
||||
api_rph: 3600,
|
||||
is_demo: false,
|
||||
model_config: {} as App['model_config'],
|
||||
app_model_config: {} as App['app_model_config'],
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
site: {
|
||||
access_token: 'token',
|
||||
app_base_url: 'https://example.com',
|
||||
} as App['site'],
|
||||
api_base_url: 'https://api.example.com',
|
||||
tags: [],
|
||||
access_mode: 'public_access' as App['access_mode'],
|
||||
}
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useAppDetail: () => ({ data: mockAppDetail, isPending: false }),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
@@ -168,7 +204,6 @@ describe('WorkflowAppLogList', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -426,7 +461,6 @@ describe('WorkflowAppLogList', () => {
|
||||
describe('Drawer', () => {
|
||||
it('should open drawer when clicking on a log row', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-123' }) })
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
id: 'log-1',
|
||||
@@ -449,7 +483,6 @@ describe('WorkflowAppLogList', () => {
|
||||
it('should close drawer and call onRefresh when closing', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onRefresh = vi.fn()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
const logs = createMockLogsResponse([createMockWorkflowLog()])
|
||||
|
||||
render(
|
||||
@@ -498,7 +531,6 @@ describe('WorkflowAppLogList', () => {
|
||||
describe('Replay Functionality', () => {
|
||||
it('should allow replay when triggered from app-run', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-replay' }) })
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
workflow_run: createMockWorkflowRun({
|
||||
@@ -521,12 +553,11 @@ describe('WorkflowAppLogList', () => {
|
||||
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
|
||||
await user.click(replayButton)
|
||||
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/app/app-replay/workflow?replayRunId=run-to-replay')
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/app/test-app-id/workflow?replayRunId=run-to-replay')
|
||||
})
|
||||
|
||||
it('should allow replay when triggered from debugging', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-debug' }) })
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
workflow_run: createMockWorkflowRun({
|
||||
@@ -552,7 +583,6 @@ describe('WorkflowAppLogList', () => {
|
||||
|
||||
it('should not show replay for webhook triggers', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-webhook' }) })
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
workflow_run: createMockWorkflowRun({
|
||||
|
||||
@@ -4,14 +4,15 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentIteration, AgentLogDetailResponse } from '@/models/log'
|
||||
import { uniq } from 'es-toolkit/array'
|
||||
import { flatten } from 'es-toolkit/compat'
|
||||
import { useParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ResultPanel from './result'
|
||||
import TracingPanel from './tracing'
|
||||
@@ -32,7 +33,8 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [currentTab, setCurrentTab] = useState<string>(activeTab)
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [runDetail, setRunDetail] = useState<AgentLogDetailResponse>()
|
||||
const [list, setList] = useState<AgentIteration[]>([])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import AgentLogModal from '.'
|
||||
|
||||
@@ -64,21 +64,34 @@ const MOCK_CHAT_ITEM: IChatItem = {
|
||||
conversationId: 'conv-123',
|
||||
}
|
||||
|
||||
const MOCK_APP_DETAIL = {
|
||||
id: 'app-1',
|
||||
name: 'Analytics Agent',
|
||||
mode: 'agent-chat',
|
||||
}
|
||||
|
||||
const createQueryClient = () => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
client.setQueryData(['apps', 'detail', 'app-1'], MOCK_APP_DETAIL)
|
||||
return client
|
||||
}
|
||||
|
||||
const AgentLogModalDemo = ({
|
||||
width = 960,
|
||||
}: {
|
||||
width?: number
|
||||
}) => {
|
||||
const originalFetchRef = useRef<typeof globalThis.fetch>(null)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const [queryClient] = useState(() => createQueryClient())
|
||||
|
||||
useEffect(() => {
|
||||
setAppDetail({
|
||||
id: 'app-1',
|
||||
name: 'Analytics Agent',
|
||||
mode: 'agent-chat',
|
||||
} as any)
|
||||
|
||||
originalFetchRef.current = globalThis.fetch?.bind(globalThis)
|
||||
|
||||
const handler = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
@@ -104,22 +117,23 @@ const AgentLogModalDemo = ({
|
||||
return () => {
|
||||
if (originalFetchRef.current)
|
||||
globalThis.fetch = originalFetchRef.current
|
||||
setAppDetail(undefined)
|
||||
}
|
||||
}, [setAppDetail])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<div className="relative min-h-[540px] w-full bg-background-default-subtle p-6">
|
||||
<AgentLogModal
|
||||
currentLogItem={MOCK_CHAT_ITEM}
|
||||
width={width}
|
||||
onCancel={() => {
|
||||
console.log('Agent log modal closed')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ToastProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
<div className="relative min-h-[540px] w-full bg-background-default-subtle p-6">
|
||||
<AgentLogModal
|
||||
currentLogItem={MOCK_CHAT_ITEM}
|
||||
width={width}
|
||||
onCancel={() => {
|
||||
console.log('Agent log modal closed')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { WorkflowRunDetailResponse } from '@/models/log'
|
||||
import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow'
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import MessageLogModal from '.'
|
||||
@@ -94,12 +94,24 @@ const mockCurrentLogItem: IChatItem = {
|
||||
workflow_run_id: 'run-demo-1',
|
||||
}
|
||||
|
||||
const useMessageLogMocks = () => {
|
||||
useEffect(() => {
|
||||
const store = useStore.getState()
|
||||
store.setAppDetail(SAMPLE_APP_DETAIL)
|
||||
const createQueryClient = () => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
client.setQueryData(['apps', 'detail', 'app-demo-1'], SAMPLE_APP_DETAIL)
|
||||
return client
|
||||
}
|
||||
|
||||
const originalFetch = globalThis.fetch?.bind(globalThis) ?? null
|
||||
const useMessageLogMocks = () => {
|
||||
const originalFetchRef = useRef<typeof globalThis.fetch>(null)
|
||||
|
||||
useEffect(() => {
|
||||
originalFetchRef.current = globalThis.fetch?.bind(globalThis) ?? null
|
||||
|
||||
const handle = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === 'string'
|
||||
@@ -122,8 +134,8 @@ const useMessageLogMocks = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (originalFetch)
|
||||
return originalFetch(input, init)
|
||||
if (originalFetchRef.current)
|
||||
return originalFetchRef.current(input, init)
|
||||
|
||||
throw new Error(`Unmocked fetch call for ${url}`)
|
||||
}
|
||||
@@ -131,8 +143,8 @@ const useMessageLogMocks = () => {
|
||||
globalThis.fetch = handle as typeof globalThis.fetch
|
||||
|
||||
return () => {
|
||||
globalThis.fetch = originalFetch || globalThis.fetch
|
||||
useStore.getState().setAppDetail(undefined)
|
||||
if (originalFetchRef.current)
|
||||
globalThis.fetch = originalFetchRef.current
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -140,17 +152,21 @@ const useMessageLogMocks = () => {
|
||||
type MessageLogModalProps = React.ComponentProps<typeof MessageLogModal>
|
||||
|
||||
const MessageLogPreview = (props: MessageLogModalProps) => {
|
||||
const [queryClient] = useState(() => createQueryClient())
|
||||
|
||||
useMessageLogMocks()
|
||||
|
||||
return (
|
||||
<div className="relative min-h-[640px] w-full bg-background-default-subtle p-6">
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
{...props}
|
||||
currentLogItem={mockCurrentLogItem}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
</div>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="relative min-h-[640px] w-full bg-background-default-subtle p-6">
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
{...props}
|
||||
currentLogItem={mockCurrentLogItem}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { FC } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import Run from '@/app/components/workflow/run'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type MessageLogModalProps = {
|
||||
@@ -25,7 +26,8 @@ const MessageLogModal: FC<MessageLogModalProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const ref = useRef(null)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const appDetail = useStore(state => state.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
|
||||
useClickAway(() => {
|
||||
if (mounted)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
'use client'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import ApiServer from '@/app/components/develop/ApiServer'
|
||||
import Doc from '@/app/components/develop/doc'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
|
||||
type IDevelopMainProps = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
const DevelopMain = ({ appId }: IDevelopMainProps) => {
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { data: appDetail, isPending } = useAppDetail(appId)
|
||||
|
||||
if (!appDetail) {
|
||||
if (isPending || !appDetail) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background-default">
|
||||
<Loading />
|
||||
|
||||
@@ -6,16 +6,14 @@ import {
|
||||
RiRobot2Line,
|
||||
} from '@remixicon/react'
|
||||
import { flatten } from 'es-toolkit/compat'
|
||||
import { produce } from 'immer'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
|
||||
import CreateAppModal from '@/app/components/app/create-app-modal'
|
||||
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
import { useAppDetail, useInfiniteAppList } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import Nav from '../nav'
|
||||
|
||||
@@ -23,11 +21,10 @@ const AppNav = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appId } = useParams()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
|
||||
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
|
||||
const [navItems, setNavItems] = useState<NavItem[]>([])
|
||||
|
||||
const {
|
||||
data: appsData,
|
||||
@@ -55,48 +52,34 @@ const AppNav = () => {
|
||||
setShowCreateFromDSLModal(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (appsData) {
|
||||
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
|
||||
const navItems = appItems.map((app) => {
|
||||
const link = ((isCurrentWorkspaceEditor, app) => {
|
||||
if (!isCurrentWorkspaceEditor) {
|
||||
return `/app/${app.id}/overview`
|
||||
}
|
||||
else {
|
||||
if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
return `/app/${app.id}/workflow`
|
||||
else
|
||||
return `/app/${app.id}/configuration`
|
||||
}
|
||||
})(isCurrentWorkspaceEditor, app)
|
||||
return {
|
||||
id: app.id,
|
||||
icon_type: app.icon_type,
|
||||
icon: app.icon,
|
||||
icon_background: app.icon_background,
|
||||
icon_url: app.icon_url,
|
||||
name: app.name,
|
||||
mode: app.mode,
|
||||
link,
|
||||
}
|
||||
})
|
||||
setNavItems(navItems as any)
|
||||
}
|
||||
}, [appsData, isCurrentWorkspaceEditor, setNavItems])
|
||||
const navItems = useMemo(() => {
|
||||
if (!appsData)
|
||||
return []
|
||||
|
||||
// update current app name
|
||||
useEffect(() => {
|
||||
if (appDetail) {
|
||||
const newNavItems = produce(navItems, (draft: NavItem[]) => {
|
||||
navItems.forEach((app, index) => {
|
||||
if (app.id === appDetail.id)
|
||||
draft[index].name = appDetail.name
|
||||
})
|
||||
})
|
||||
setNavItems(newNavItems)
|
||||
}
|
||||
}, [appDetail, navItems])
|
||||
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
|
||||
return appItems.map((app) => {
|
||||
const link = (() => {
|
||||
if (!isCurrentWorkspaceEditor)
|
||||
return `/app/${app.id}/overview`
|
||||
|
||||
if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
return `/app/${app.id}/workflow`
|
||||
|
||||
return `/app/${app.id}/configuration`
|
||||
})()
|
||||
|
||||
return {
|
||||
id: app.id,
|
||||
icon_type: app.icon_type,
|
||||
icon: app.icon,
|
||||
icon_background: app.icon_background,
|
||||
icon_url: app.icon_url,
|
||||
name: app.id === appDetail?.id ? appDetail.name : app.name,
|
||||
mode: app.mode,
|
||||
link,
|
||||
}
|
||||
}) as NavItem[]
|
||||
}, [appsData, isCurrentWorkspaceEditor, appDetail])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -5,7 +5,6 @@ import Link from 'next/link'
|
||||
import { useSelectedLayoutSegment } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import NavSelector from './nav-selector'
|
||||
@@ -33,7 +32,6 @@ const Nav = ({
|
||||
isLoadingMore,
|
||||
isApp,
|
||||
}: INavProps) => {
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const segment = useSelectedLayoutSegment()
|
||||
const isActivated = Array.isArray(activeSegment) ? activeSegment.includes(segment!) : segment === activeSegment
|
||||
@@ -47,12 +45,6 @@ const Nav = ({
|
||||
>
|
||||
<Link href={link}>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
// Don't clear state if opening in new tab/window
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
|
||||
return
|
||||
setAppDetail()
|
||||
}}
|
||||
className={cn('flex h-7 cursor-pointer items-center rounded-[10px] px-2.5', isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text', curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover')}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { debounce } from 'es-toolkit/compat'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Fragment, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
@@ -42,7 +41,6 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
|
||||
const handleScroll = useCallback(debounce((e) => {
|
||||
if (typeof onLoadMore === 'function') {
|
||||
@@ -84,7 +82,6 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
||||
onClick={() => {
|
||||
if (curNav?.id === nav.id)
|
||||
return
|
||||
setAppDetail()
|
||||
router.push(nav.link)
|
||||
}}
|
||||
title={nav.name}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MarketplaceCollection } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { act, render, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
|
||||
// ================================
|
||||
@@ -157,6 +157,45 @@ vi.mock('@/config', () => ({
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.dify.ai/api/v1',
|
||||
}))
|
||||
|
||||
// Mock service/client - configurable mock for testing
|
||||
const {
|
||||
mockMarketplaceCollections,
|
||||
mockMarketplaceCollectionPlugins,
|
||||
mockMarketplaceSearchAdvanced,
|
||||
} = vi.hoisted(() => {
|
||||
const mockMarketplaceCollections = vi.fn(() => Promise.resolve({
|
||||
data: {
|
||||
collections: [
|
||||
{ name: 'test-collection', label: 'Test Collection' },
|
||||
],
|
||||
},
|
||||
}))
|
||||
const mockMarketplaceCollectionPlugins = vi.fn(() => Promise.resolve({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
},
|
||||
}))
|
||||
const mockMarketplaceSearchAdvanced = vi.fn(() => Promise.resolve({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
bundles: [],
|
||||
total: 1,
|
||||
},
|
||||
}))
|
||||
return { mockMarketplaceCollections, mockMarketplaceCollectionPlugins, mockMarketplaceSearchAdvanced }
|
||||
})
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceClient: {
|
||||
collections: mockMarketplaceCollections,
|
||||
collectionPlugins: mockMarketplaceCollectionPlugins,
|
||||
searchAdvanced: mockMarketplaceSearchAdvanced,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock var utils
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string, _params?: Record<string, string | undefined>) => `https://marketplace.dify.ai${path}`,
|
||||
@@ -199,7 +238,7 @@ vi.mock('@/i18n-config/language', () => ({
|
||||
}))
|
||||
|
||||
// Mock global fetch for utils testing
|
||||
const originalFetch = globalThis.fetch
|
||||
const _originalFetch = globalThis.fetch
|
||||
|
||||
// Mock useTags hook
|
||||
const mockTags = [
|
||||
@@ -1477,25 +1516,33 @@ describe('flatMap Coverage', () => {
|
||||
describe('Async Utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
// Reset mocks to default behavior
|
||||
mockMarketplaceCollections.mockImplementation(() => Promise.resolve({
|
||||
data: {
|
||||
collections: [
|
||||
{ name: 'test-collection', label: 'Test Collection' },
|
||||
],
|
||||
},
|
||||
}))
|
||||
mockMarketplaceCollectionPlugins.mockImplementation(() => Promise.resolve({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('getMarketplacePluginsByCollectionId', () => {
|
||||
it('should fetch plugins by collection id successfully', async () => {
|
||||
const mockPlugins = [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1' },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin2' },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin2', tags: [] },
|
||||
]
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
mockMarketplaceCollectionPlugins.mockResolvedValue({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
const result = await getMarketplacePluginsByCollectionId('test-collection', {
|
||||
@@ -1504,12 +1551,12 @@ describe('Async Utils', () => {
|
||||
type: 'plugin',
|
||||
})
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalled()
|
||||
expect(mockMarketplaceCollectionPlugins).toHaveBeenCalled()
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle fetch error and return empty array', async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
mockMarketplaceCollectionPlugins.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
const result = await getMarketplacePluginsByCollectionId('test-collection')
|
||||
@@ -1518,53 +1565,39 @@ describe('Async Utils', () => {
|
||||
})
|
||||
|
||||
it('should pass abort signal when provided', async () => {
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1' }]
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1', tags: [] }]
|
||||
mockMarketplaceCollectionPlugins.mockResolvedValue({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const controller = new AbortController()
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
await getMarketplacePluginsByCollectionId('test-collection', {}, { signal: controller.signal })
|
||||
|
||||
// oRPC uses Request objects, so check that fetch was called with a Request containing the right URL
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
expect.any(Request),
|
||||
expect.any(Object),
|
||||
// Check that collectionPlugins was called with the correct params including signal
|
||||
expect(mockMarketplaceCollectionPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: { collectionId: 'test-collection' },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
signal: controller.signal,
|
||||
}),
|
||||
)
|
||||
const call = vi.mocked(globalThis.fetch).mock.calls[0]
|
||||
const request = call[0] as Request
|
||||
expect(request.url).toContain('test-collection')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceCollectionsAndPlugins', () => {
|
||||
it('should fetch collections and plugins successfully', async () => {
|
||||
const mockCollections = [
|
||||
{ name: 'collection1', label: {}, description: {}, rule: '', created_at: '', updated_at: '' },
|
||||
{ name: 'collection1', label: 'Collection 1' },
|
||||
]
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1' }]
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1', tags: [] }]
|
||||
|
||||
let callCount = 0
|
||||
globalThis.fetch = vi.fn().mockImplementation(() => {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { collections: mockCollections } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
mockMarketplaceCollections.mockResolvedValue({
|
||||
data: { collections: mockCollections },
|
||||
})
|
||||
mockMarketplaceCollectionPlugins.mockResolvedValue({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
@@ -1578,7 +1611,7 @@ describe('Async Utils', () => {
|
||||
})
|
||||
|
||||
it('should handle fetch error and return empty data', async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
mockMarketplaceCollections.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
const result = await getMarketplaceCollectionsAndPlugins()
|
||||
@@ -1588,12 +1621,9 @@ describe('Async Utils', () => {
|
||||
})
|
||||
|
||||
it('should append condition and type to URL when provided', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { collections: [] } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
mockMarketplaceCollections.mockResolvedValue({
|
||||
data: { collections: [] },
|
||||
})
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
await getMarketplaceCollectionsAndPlugins({
|
||||
@@ -1601,11 +1631,16 @@ describe('Async Utils', () => {
|
||||
type: 'bundle',
|
||||
})
|
||||
|
||||
// oRPC uses Request objects, so check that fetch was called with a Request containing the right URL
|
||||
expect(globalThis.fetch).toHaveBeenCalled()
|
||||
const call = vi.mocked(globalThis.fetch).mock.calls[0]
|
||||
const request = call[0] as Request
|
||||
expect(request.url).toContain('condition=category%3Dtool')
|
||||
// Check that collections was called with the correct query params
|
||||
expect(mockMarketplaceCollections).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: expect.objectContaining({
|
||||
condition: 'category=tool',
|
||||
type: 'bundle',
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { AppPublisherProps } from '@/app/components/app/app-publisher'
|
||||
import type { App } from '@/types/app'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||
import FeaturesTrigger from './features-trigger'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ appId: 'app-id' }),
|
||||
}))
|
||||
|
||||
const mockUseIsChatMode = vi.fn()
|
||||
const mockUseTheme = vi.fn()
|
||||
const mockUseNodesReadOnly = vi.fn()
|
||||
@@ -27,7 +29,7 @@ const mockPublishWorkflow = vi.fn()
|
||||
const mockUpdatePublishedWorkflow = vi.fn()
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
const mockInvalidateAppTriggers = vi.fn()
|
||||
const mockFetchAppDetail = vi.fn()
|
||||
const mockInvalidateAppDetail = vi.fn()
|
||||
const mockSetPublishedAt = vi.fn()
|
||||
const mockSetLastPublishedHasUserInput = vi.fn()
|
||||
|
||||
@@ -126,16 +128,14 @@ vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAppTriggers: () => mockInvalidateAppTriggers,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/apps', () => ({
|
||||
fetchAppDetail: (...args: unknown[]) => mockFetchAppDetail(...args),
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useInvalidateAppDetail: () => mockInvalidateAppDetail,
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => mockUseTheme(),
|
||||
}))
|
||||
|
||||
// Use real app store - global zustand mock will auto-reset between tests
|
||||
|
||||
const createProviderContext = ({
|
||||
type = Plan.sandbox,
|
||||
isFetchedPlan = true,
|
||||
@@ -176,9 +176,6 @@ describe('FeaturesTrigger', () => {
|
||||
mockUseProviderContext.mockReturnValue(createProviderContext({}))
|
||||
mockUseNodes.mockReturnValue([])
|
||||
mockUseEdges.mockReturnValue([])
|
||||
// Set up app store state
|
||||
useAppStore.setState({ appDetail: { id: 'app-id' } as unknown as App })
|
||||
mockFetchAppDetail.mockResolvedValue({ id: 'app-id' })
|
||||
mockPublishWorkflow.mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
|
||||
})
|
||||
|
||||
@@ -422,8 +419,7 @@ describe('FeaturesTrigger', () => {
|
||||
expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true)
|
||||
expect(mockResetWorkflowVersionHistory).toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.api.actionSuccess' })
|
||||
expect(mockFetchAppDetail).toHaveBeenCalledWith({ url: '/apps', id: 'app-id' })
|
||||
expect(useAppStore.getState().appDetail).toBeDefined()
|
||||
expect(mockInvalidateAppDetail).toHaveBeenCalledWith('app-id')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -444,23 +440,5 @@ describe('FeaturesTrigger', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should log error when app detail refresh fails after publish', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
||||
mockFetchAppDetail.mockRejectedValue(new Error('fetch failed'))
|
||||
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish' }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
})
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { RiApps2AddLine } from '@remixicon/react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges } from 'reactflow'
|
||||
import AppPublisher from '@/app/components/app/app-publisher'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { useInvalidateAppDetail } from '@/service/use-apps'
|
||||
import { useInvalidateAppTriggers } from '@/service/use-tools'
|
||||
import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@@ -49,9 +49,9 @@ const FeaturesTrigger = () => {
|
||||
const { theme } = useTheme()
|
||||
const isChatMode = useIsChatMode()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appID = appDetail?.id
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const { appId } = useParams()
|
||||
const appID = appId as string
|
||||
const invalidateAppDetail = useInvalidateAppDetail()
|
||||
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
|
||||
const { plan, isFetchedPlan } = useProviderContext()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
@@ -125,15 +125,9 @@ const FeaturesTrigger = () => {
|
||||
setShowFeaturesPanel(!showFeaturesPanel)
|
||||
}, [workflowStore, getNodesReadOnly])
|
||||
|
||||
const updateAppDetail = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appID! })
|
||||
setAppDetail({ ...res })
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, [appID, setAppDetail])
|
||||
const updateAppDetail = useCallback(() => {
|
||||
invalidateAppDetail(appID)
|
||||
}, [appID, invalidateAppDetail])
|
||||
|
||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
|
||||
// const { validateBeforeRun } = useWorkflowRunValidation()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AppModeEnum } from '@/types/app'
|
||||
import WorkflowHeader from './index'
|
||||
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
const mockUseAppDetail = vi.fn()
|
||||
|
||||
const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
id: 'app-id',
|
||||
@@ -38,14 +39,19 @@ const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Helper to set up app store state
|
||||
const setupAppStore = (overrides: Partial<App> = {}) => {
|
||||
const setupAppDetail = (overrides: Partial<App> = {}) => {
|
||||
const appDetail = createMockApp(overrides)
|
||||
useAppStore.setState({ appDetail })
|
||||
mockUseAppDetail.mockReturnValue({ data: appDetail })
|
||||
return appDetail
|
||||
}
|
||||
|
||||
// Use real store - global zustand mock will auto-reset between tests
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ appId: 'app-id' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useAppDetail: (...args: unknown[]) => mockUseAppDetail(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/header', () => ({
|
||||
default: (props: HeaderProps) => {
|
||||
@@ -80,7 +86,7 @@ vi.mock('@/service/use-workflow', () => ({
|
||||
describe('WorkflowHeader', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupAppStore()
|
||||
setupAppDetail()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -103,7 +109,7 @@ describe('WorkflowHeader', () => {
|
||||
describe('Props', () => {
|
||||
it('should configure preview mode when app is in advanced chat mode', () => {
|
||||
// Arrange
|
||||
setupAppStore({ mode: AppModeEnum.ADVANCED_CHAT })
|
||||
setupAppDetail({ mode: AppModeEnum.ADVANCED_CHAT })
|
||||
|
||||
// Act
|
||||
render(<WorkflowHeader />)
|
||||
@@ -117,7 +123,7 @@ describe('WorkflowHeader', () => {
|
||||
|
||||
it('should configure run mode when app is not in advanced chat mode', () => {
|
||||
// Arrange
|
||||
setupAppStore({ mode: AppModeEnum.COMPLETION })
|
||||
setupAppDetail({ mode: AppModeEnum.COMPLETION })
|
||||
|
||||
// Act
|
||||
render(<WorkflowHeader />)
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { HeaderProps } from '@/app/components/workflow/header'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Header from '@/app/components/workflow/header'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { useIsChatMode } from '../../hooks'
|
||||
import ChatVariableTrigger from './chat-variable-trigger'
|
||||
import FeaturesTrigger from './features-trigger'
|
||||
|
||||
const WorkflowHeader = () => {
|
||||
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
})))
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const setCurrentLogItem = useAppStore(state => state.setCurrentLogItem)
|
||||
const setShowMessageLogModal = useAppStore(state => state.setShowMessageLogModal)
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PanelProps } from '@/app/components/workflow/panel'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
@@ -8,6 +9,7 @@ import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Panel from '@/app/components/workflow/panel'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import {
|
||||
useIsChatMode,
|
||||
} from '../hooks'
|
||||
@@ -104,16 +106,16 @@ const WorkflowPanelOnRight = () => {
|
||||
)
|
||||
}
|
||||
const WorkflowPanel = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const versionHistoryPanelProps = useMemo(() => {
|
||||
const appId = appDetail?.id
|
||||
return {
|
||||
getVersionListUrl: `/apps/${appId}/workflows`,
|
||||
deleteVersionUrl: (versionId: string) => `/apps/${appId}/workflows/${versionId}`,
|
||||
updateVersionUrl: (versionId: string) => `/apps/${appId}/workflows/${versionId}`,
|
||||
latestVersionId: appDetail?.workflow?.id,
|
||||
}
|
||||
}, [appDetail?.id, appDetail?.workflow?.id])
|
||||
}, [appId, appDetail?.workflow?.id])
|
||||
|
||||
const panelProps: PanelProps = useMemo(() => {
|
||||
return {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
DSL_EXPORT_CHECK,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { exportAppConfig } from '@/service/apps'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
@@ -20,7 +21,8 @@ export const useDSL = () => {
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
|
||||
const handleExportDSL = useCallback(async (include = false, workflowId?: string) => {
|
||||
if (!appDetail)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
|
||||
return appDetail?.mode === AppModeEnum.ADVANCED_CHAT
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import type { FileUploadConfigResponse } from '@/models/common'
|
||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
||||
import {
|
||||
fetchNodesDefaultConfigs,
|
||||
@@ -38,13 +39,16 @@ export const useWorkflowInit = () => {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
} = useWorkflowTemplate()
|
||||
const appDetail = useAppStore(state => state.appDetail)!
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash)
|
||||
const [data, setData] = useState<FetchWorkflowDraftResponse>()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
workflowStore.setState({ appId: appDetail.id, appName: appDetail.name })
|
||||
}, [appDetail.id, workflowStore])
|
||||
if (appDetail) {
|
||||
workflowStore.setState({ appId: appDetail.id, appName: appDetail.name })
|
||||
}
|
||||
}, [appDetail, workflowStore])
|
||||
|
||||
const handleUpdateWorkflowFileUploadConfig = useCallback((config: FileUploadConfigResponse) => {
|
||||
const { setFileUploadConfig } = workflowStore.getState()
|
||||
@@ -56,6 +60,8 @@ export const useWorkflowInit = () => {
|
||||
} = useWorkflowConfig('/files/upload', handleUpdateWorkflowFileUploadConfig)
|
||||
|
||||
const handleGetInitialWorkflowData = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
return
|
||||
try {
|
||||
const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
|
||||
setData(res)
|
||||
@@ -109,8 +115,9 @@ export const useWorkflowInit = () => {
|
||||
}, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash])
|
||||
|
||||
useEffect(() => {
|
||||
handleGetInitialWorkflowData()
|
||||
}, [])
|
||||
if (appDetail)
|
||||
handleGetInitialWorkflowData()
|
||||
}, [appDetail, handleGetInitialWorkflowData])
|
||||
|
||||
const handleFetchPreloadData = useCallback(async () => {
|
||||
try {
|
||||
|
||||
@@ -4,14 +4,13 @@ import type { IOtherOptions } from '@/service/base'
|
||||
import type { VersionHistory } from '@/types/workflow'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { v4 as uuidV4 } from 'uuid'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
@@ -23,6 +22,7 @@ import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { handleStream, post, ssePost } from '@/service/base'
|
||||
import { ContentType } from '@/service/fetch'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { stopWorkflowRun } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@@ -63,6 +63,8 @@ export const useWorkflowRun = () => {
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
||||
const pathname = usePathname()
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const configsMap = useConfigsMap()
|
||||
const { flowId, flowType } = configsMap
|
||||
const invalidAllLastRun = useInvalidAllLastRun(flowType, flowId)
|
||||
@@ -180,7 +182,6 @@ export const useWorkflowRun = () => {
|
||||
...restCallback
|
||||
} = callback || {}
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
const workflowContainer = document.getElementById('workflow-container')
|
||||
|
||||
const {
|
||||
@@ -667,7 +668,7 @@ export const useWorkflowRun = () => {
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace])
|
||||
}, [store, doSyncWorkflowDraft, workflowStore, pathname, appDetail, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace])
|
||||
|
||||
const handleStopRun = useCallback((taskId: string) => {
|
||||
const setStoppedState = () => {
|
||||
@@ -696,7 +697,6 @@ export const useWorkflowRun = () => {
|
||||
}
|
||||
|
||||
if (taskId) {
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`)
|
||||
setStoppedState()
|
||||
return
|
||||
@@ -725,7 +725,7 @@ export const useWorkflowRun = () => {
|
||||
|
||||
abortControllerRef.current = null
|
||||
setStoppedState()
|
||||
}, [workflowStore])
|
||||
}, [workflowStore, appId])
|
||||
|
||||
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
|
||||
const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } }))
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
||||
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useParams, useSearchParams } from 'next/navigation'
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchRunDetail } from '@/service/log'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useAppTriggers } from '@/service/use-tools'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import WorkflowAppMain from './components/workflow-main'
|
||||
@@ -47,10 +47,10 @@ const WorkflowAppWithAdditionalContext = () => {
|
||||
|
||||
// Initialize trigger status at application level
|
||||
const { setTriggerStatuses } = useTriggerStatusStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const isWorkflowMode = appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
const { data: triggersResponse } = useAppTriggers(isWorkflowMode ? appId : undefined, {
|
||||
const { data: triggersResponse } = useAppTriggers(isWorkflowMode ? appId as string : undefined, {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes cache
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
@@ -44,7 +44,6 @@ const ViewWorkflowHistory = () => {
|
||||
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
})))
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { produce } from 'immer'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { fetchWebhookUrl } from '@/service/apps'
|
||||
|
||||
export const useAutoGenerateWebhookUrl = () => {
|
||||
const reactFlowStore = useStoreApi()
|
||||
const { appId } = useParams()
|
||||
|
||||
return useCallback(async (nodeId: string) => {
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
@@ -22,7 +22,7 @@ export const useAutoGenerateWebhookUrl = () => {
|
||||
return
|
||||
|
||||
try {
|
||||
const response = await fetchWebhookUrl({ appId, nodeId })
|
||||
const response = await fetchWebhookUrl({ appId: appId as string, nodeId })
|
||||
const { getNodes: getLatestNodes, setNodes } = reactFlowStore.getState()
|
||||
let hasUpdated = false
|
||||
const updatedNodes = produce(getLatestNodes(), (draft) => {
|
||||
@@ -44,5 +44,5 @@ export const useAutoGenerateWebhookUrl = () => {
|
||||
catch (error: unknown) {
|
||||
console.error('Failed to auto-generate webhook URL:', error)
|
||||
}
|
||||
}, [reactFlowStore])
|
||||
}, [reactFlowStore, appId])
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges, useStoreApi } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
@@ -29,6 +29,7 @@ import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { MAX_TREE_DEPTH } from '@/config'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
@@ -96,7 +97,9 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
|
||||
const getToolIcon = useGetToolIcon()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const appMode = appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const map = useNodesAvailableVarList(nodes)
|
||||
@@ -249,7 +252,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
})
|
||||
|
||||
return list
|
||||
}, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map, shouldCheckStartNode])
|
||||
}, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, triggerPlugins, getCheckData, t, map, shouldCheckStartNode])
|
||||
|
||||
return needWarningNodes
|
||||
}
|
||||
@@ -270,7 +273,9 @@ export const useChecklistBeforePublish = () => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const appMode = appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
|
||||
@@ -419,7 +424,7 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode])
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode, getNodesAvailableVarList])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
@@ -18,9 +19,9 @@ import {
|
||||
getOutgoers,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useNodesMetaData } from '.'
|
||||
import {
|
||||
@@ -43,7 +44,8 @@ import {
|
||||
import { useAvailableBlocks } from './use-available-blocks'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
|
||||
return appDetail?.mode === AppModeEnum.ADVANCED_CHAT
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { useParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
isSupportCustomRunForm,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { FlowType } from '@/types/common'
|
||||
@@ -109,6 +111,8 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const { showMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
})))
|
||||
@@ -196,7 +200,6 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
const isSupportSingleRun = canRunBySingle(data.type, isChildNode)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
|
||||
const hasClickRunning = useRef(false)
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { HttpMethod, WebhookHeader, WebhookParameter, WebhookTriggerNodeType } from './types'
|
||||
import type { Variable } from '@/app/components/workflow/types'
|
||||
import { produce } from 'immer'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useNodesReadOnly, useWorkflow } from '@/app/components/workflow/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
@@ -17,7 +17,7 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
|
||||
const { t } = useTranslation()
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<WebhookTriggerNodeType>(id, payload)
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
const { appId } = useParams()
|
||||
const { isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
|
||||
|
||||
const handleMethodChange = useCallback((method: HttpMethod) => {
|
||||
@@ -217,7 +217,7 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
|
||||
|
||||
try {
|
||||
// Call backend to generate or fetch webhook url for this node
|
||||
const response = await fetchWebhookUrl({ appId, nodeId: id })
|
||||
const response = await fetchWebhookUrl({ appId: appId as string, nodeId: id })
|
||||
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.webhook_url = response.webhook_url
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { fetchConversationMessages } from '@/service/debug'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { useWorkflowRun } from '../../hooks'
|
||||
import {
|
||||
useStore,
|
||||
@@ -51,7 +52,8 @@ const ChatRecord = () => {
|
||||
const [fetched, setFetched] = useState(false)
|
||||
const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([])
|
||||
const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleLoadBackupDraft } = useWorkflowRun()
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { StartNodeType } from '../../nodes/start/types'
|
||||
import type { ChatWrapperRefType } from './index'
|
||||
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { memo, useCallback, useEffect, useImperativeHandle, useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
fetchSuggestedQuestions,
|
||||
stopChatMessageResponding,
|
||||
} from '@/service/debug'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
@@ -45,7 +46,8 @@ const ChatWrapper = (
|
||||
const nodes = useNodes<StartNodeType>()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const inputs = useStore(s => s.inputs)
|
||||
const setInputs = useStore(s => s.setInputs)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
RiFileDownloadLine,
|
||||
} from '@remixicon/react'
|
||||
import { load as yamlLoad } from 'js-yaml'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
importDSL,
|
||||
importDSLConfirm,
|
||||
} from '@/service/apps'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { WORKFLOW_DATA_UPDATE } from './constants'
|
||||
@@ -60,7 +61,8 @@ const UpdateDSLModal = ({
|
||||
}: UpdateDSLModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { appId } = useParams()
|
||||
const { data: appDetail } = useAppDetail(appId as string)
|
||||
const [currentFile, setDSLFile] = useState<File>()
|
||||
const [fileContent, setFileContent] = useState<string>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
@@ -73,14 +73,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx": {
|
||||
"no-console": {
|
||||
"count": 19
|
||||
@@ -245,9 +237,6 @@
|
||||
}
|
||||
},
|
||||
"app/components/app/app-publisher/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 3
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 6
|
||||
}
|
||||
@@ -656,9 +645,6 @@
|
||||
"app/components/base/agent-log-modal/index.stories.tsx": {
|
||||
"no-console": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/agent-log-modal/index.tsx": {
|
||||
@@ -2187,14 +2173,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"app/components/header/app-nav/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/header/header-wrapper.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@@ -94,6 +94,17 @@ export const useAppDetail = (appID: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAppDetail = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return (appId?: string) => {
|
||||
if (!appId)
|
||||
return
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [NAME_SPACE, 'detail', appId],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const useAppList = (params: AppListParams, options?: { enabled?: boolean }) => {
|
||||
const normalizedParams = normalizeAppListParams(params)
|
||||
return useQuery<AppListResponse>({
|
||||
|
||||
Reference in New Issue
Block a user