fix(web): clarify unpublished explore app handling (#38260)

This commit is contained in:
eux 2026-07-01 19:04:01 +08:00 committed by GitHub
parent c1d03a888f
commit bf46b82303
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 82 additions and 9 deletions

View File

@ -215,7 +215,7 @@ describe('App Publisher Flow', () => {
fireEvent.click(screen.getByText('common.openInExplore'))
await waitFor(() => {
expect(mockToastError).toHaveBeenCalledWith('No app found in Explore')
expect(mockToastError).toHaveBeenCalledWith('notPublishedYet')
})
})
})

View File

@ -562,7 +562,7 @@ describe('AppPublisher', () => {
fireEvent.click(screen.getByText('publisher-open-in-explore'))
await waitFor(() => {
expect(mockToastError).toHaveBeenCalledWith('No app found in Explore')
expect(mockToastError).toHaveBeenCalledWith('notPublishedYet')
})
})

View File

@ -231,7 +231,7 @@ export function AppPublisher({
const { installed_apps } = await fetchInstalledAppList(appDetail.id)
if (installed_apps?.length > 0)
return `${basePath}${buildInstalledAppPath(installed_apps[0]!.id)}`
throw new Error('No app found in Explore')
throw new Error(t('notPublishedYet', { ns: 'app' }))
}, {
onError: (err) => {
toast.error(`${err.message || err}`)

View File

@ -14,6 +14,10 @@ import { StarredAppCard } from '../starred-app-card'
let mockWebappAuthEnabled = false
let mockRbacEnabled = true
const mockUserCanAccessApp = vi.hoisted(() => ({
result: true as boolean | undefined,
isLoading: false,
}))
const render = (ui: React.ReactElement) => renderWithSystemFeatures(ui, {
systemFeatures: {
@ -118,15 +122,15 @@ vi.mock('@/service/explore', () => ({
vi.mock('@/service/access-control', () => ({
useGetUserCanAccessApp: () => ({
data: { result: true },
isLoading: false,
data: mockUserCanAccessApp.result === undefined ? undefined : { result: mockUserCanAccessApp.result },
isLoading: mockUserCanAccessApp.isLoading,
}),
}))
vi.mock('@/service/access-control/use-app-access-control', () => ({
useGetUserCanAccessApp: () => ({
data: { result: true },
isLoading: false,
data: mockUserCanAccessApp.result === undefined ? undefined : { result: mockUserCanAccessApp.result },
isLoading: mockUserCanAccessApp.isLoading,
}),
}))
@ -380,6 +384,8 @@ describe('AppCard', () => {
mockOpenAsyncWindow.mockReset()
mockWebappAuthEnabled = false
mockRbacEnabled = true
mockUserCanAccessApp.result = true
mockUserCanAccessApp.isLoading = false
mockDeleteMutationPending = false
mockToggleStarMutationPending = false
mockAppContext.isCurrentWorkspaceEditor = true
@ -1733,6 +1739,28 @@ describe('AppCard', () => {
})
describe('Open in Explore - No App Found', () => {
it('should tell workflow users to publish before opening in explore', async () => {
const workflowApp = createMockApp({
mode: AppModeEnum.WORKFLOW,
workflow: undefined,
})
render(<AppCard app={workflowApp} />)
fireEvent.click(screen.getByTestId('dropdown-menu-trigger'))
await waitFor(() => {
expect(screen.getByText('app.openInExplore')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('app.openInExplore'))
expect(mockOpenAsyncWindow).not.toHaveBeenCalled()
expect(exploreService.fetchInstalledAppList).not.toHaveBeenCalled()
expect(toastMocks.record).toHaveBeenCalledWith({
type: 'error',
message: 'app.notPublishedYet',
})
})
it('should handle case when installed_apps is empty array', async () => {
(exploreService.fetchInstalledAppList as Mock).mockResolvedValueOnce({ installed_apps: [] })
@ -1756,6 +1784,10 @@ describe('AppCard', () => {
await waitFor(() => {
expect(exploreService.fetchInstalledAppList).toHaveBeenCalled()
expect(toastMocks.record).toHaveBeenCalledWith({
type: 'error',
message: 'app.notPublishedYet',
})
})
})
@ -1944,6 +1976,31 @@ describe('AppCard', () => {
})
})
it('should keep open in explore visible for unpublished workflow apps while access check is pending', async () => {
mockUserCanAccessApp.result = false
mockUserCanAccessApp.isLoading = true
const workflowApp = createMockApp({
mode: AppModeEnum.WORKFLOW,
workflow: undefined,
})
render(<AppCard app={workflowApp} />)
fireEvent.click(screen.getByTestId('dropdown-menu-trigger'))
await waitFor(() => {
expect(screen.getByText('app.openInExplore')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('app.openInExplore'))
expect(mockOpenAsyncWindow).not.toHaveBeenCalled()
expect(exploreService.fetchInstalledAppList).not.toHaveBeenCalled()
expect(toastMocks.record).toHaveBeenCalledWith({
type: 'error',
message: 'app.notPublishedYet',
})
})
it('should close access control modal when onClose is called', async () => {
render(<AppCard app={mockApp} />)

View File

@ -90,6 +90,11 @@ const ACCESS_MODE_LABEL_KEYS = {
[AccessMode.EXTERNAL_MEMBERS]: 'accessItemsDescription.external',
} as const
const APP_MODES_REQUIRING_PUBLISHED_WORKFLOW_IN_EXPLORE = new Set<AppModeEnum>([
AppModeEnum.ADVANCED_CHAT,
AppModeEnum.WORKFLOW,
])
type AppCardProps = {
app: App
onlineUsers?: WorkflowOnlineUser[]
@ -103,6 +108,10 @@ type AppAccessModeIconProps = {
const getAppResourceMaintainer = (app: App) => app.maintainer
function requiresPublishedWorkflowInExplore(app: App) {
return APP_MODES_REQUIRING_PUBLISHED_WORKFLOW_IN_EXPLORE.has(app.mode)
}
function AppAccessModeIcon({ accessMode }: AppAccessModeIconProps) {
const { t } = useTranslation()
@ -182,12 +191,17 @@ function AppCardOperationsMenu({
async function handleOpenInstalledApp(e: MouseEvent<HTMLElement>) {
e.stopPropagation()
e.preventDefault()
if (requiresPublishedWorkflowInExplore(app) && !app.workflow?.id) {
toast.error(t('notPublishedYet', { ns: 'app' }))
return
}
try {
await openAsyncWindow(async () => {
const { installed_apps } = await fetchInstalledAppList(app.id)
if (installed_apps?.length > 0)
return `${basePath}${buildInstalledAppPath(installed_apps[0]!.id)}`
throw new Error('No app found in Explore')
throw new Error(t('notPublishedYet', { ns: 'app' }))
}, {
onError: (err) => {
toast.error(`${err.message || err}`)
@ -272,10 +286,12 @@ function AppCardOperationsMenuContent(props: AppCardOperationsMenuContentProps)
appId: props.app.id,
enabled: systemFeatures.webapp_auth.enabled,
})
const needsPublishBeforeExplore = requiresPublishedWorkflowInExplore(props.app) && !props.app.workflow?.id
const shouldShowOpenInExploreOption = !props.app.has_draft_trigger
&& (
!systemFeatures.webapp_auth.enabled
needsPublishBeforeExplore
|| !systemFeatures.webapp_auth.enabled
|| (!isGettingUserCanAccessApp && Boolean(userCanAccessApp?.result))
)