diff --git a/web/__tests__/app/app-publisher-flow.test.tsx b/web/__tests__/app/app-publisher-flow.test.tsx
index cf7ca7e19bb..4b88d0de95b 100644
--- a/web/__tests__/app/app-publisher-flow.test.tsx
+++ b/web/__tests__/app/app-publisher-flow.test.tsx
@@ -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')
})
})
})
diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx
index b4593347780..5bbe67b6890 100644
--- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx
+++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx
@@ -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')
})
})
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx
index 2f35ae3f45a..01471186194 100644
--- a/web/app/components/app/app-publisher/index.tsx
+++ b/web/app/components/app/app-publisher/index.tsx
@@ -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}`)
diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx
index 55aa304316e..3be6fa998d9 100644
--- a/web/app/components/apps/__tests__/app-card.spec.tsx
+++ b/web/app/components/apps/__tests__/app-card.spec.tsx
@@ -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()
+
+ 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()
+
+ 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()
diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx
index f22bc28e931..3cb998febbf 100644
--- a/web/app/components/apps/app-card.tsx
+++ b/web/app/components/apps/app-card.tsx
@@ -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.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) {
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))
)