From 1247fa28f176eb390dad6fbdfef1ffa7eeccda16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 6 Jul 2026 17:11:56 +0800 Subject: [PATCH] fix(web): redirect imported apps with creator permissions (#38460) --- .../app-list/__tests__/index.spec.tsx | 34 +++++++++++- .../app/create-app-dialog/app-list/index.tsx | 12 +++- .../__tests__/index.spec.tsx | 55 ++++++++++++++++++- .../app/create-from-dsl-modal/index.tsx | 19 ++++++- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx index 81e27381649..14c4cc37312 100644 --- a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx @@ -17,6 +17,7 @@ const mockInvalidateAppList = vi.hoisted(() => vi.fn()) let latestDebounceFn = () => {} let mockWorkspacePermissionKeys: string[] = ['app.create_and_management'] let mockIsCurrentWorkspaceEditor = true +const mockUserProfile = { id: 'user-1' } vi.mock('ahooks', () => ({ useDebounceFn: (fn: () => void) => { @@ -31,6 +32,7 @@ vi.mock('ahooks', () => ({ vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor, + userProfile: mockUserProfile, workspacePermissionKeys: mockWorkspacePermissionKeys, }), })) @@ -296,7 +298,37 @@ describe('Apps', () => { id: 'created-app-id', mode: AppModeEnum.CHAT, permission_keys: ['app.acl.view_layout'], - }, mockPush, { isRbacEnabled: false }) + }, mockPush, { + currentUserId: 'user-1', + resourceMaintainer: 'user-1', + workspacePermissionKeys: ['app.create_and_management'], + isRbacEnabled: false, + }) + }) + + it('passes creator context when template import response has no permission keys', async () => { + mockImportDSL.mockResolvedValueOnce({ + app_id: 'created-without-permissions', + app_mode: AppModeEnum.WORKFLOW, + }) + + render() + + fireEvent.click(screen.getAllByTestId('app-card')[0]!) + fireEvent.click(screen.getByTestId('confirm-create')) + + await waitFor(() => { + expect(mockGetRedirection).toHaveBeenCalledWith({ + id: 'created-without-permissions', + mode: AppModeEnum.WORKFLOW, + permission_keys: undefined, + }, mockPush, { + currentUserId: 'user-1', + resourceMaintainer: 'user-1', + workspacePermissionKeys: ['app.create_and_management'], + isRbacEnabled: false, + }) + }) }) it('shows an error toast when importing the template fails', async () => { diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index f8c0bcd6ada..8efa3427f4a 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -48,7 +48,7 @@ const Apps = ({ }: AppsProps) => { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const { workspacePermissionKeys } = useAppContext() + const { userProfile, workspacePermissionKeys } = useAppContext() const isRbacEnabled = systemFeatures.rbac_enabled const canCreateAppFromTemplate = hasPermission(workspacePermissionKeys, 'app.create_and_management') const { push } = useRouter() @@ -163,8 +163,14 @@ const Apps = ({ await handleCheckPluginDependencies(app.app_id) setNeedRefresh('1') invalidateAppList() - if (app.app_id) - getRedirection({ id: app.app_id, mode: app.app_mode, permission_keys: app.permission_keys }, push, { isRbacEnabled }) + if (app.app_id) { + getRedirection({ id: app.app_id, mode: app.app_mode, permission_keys: app.permission_keys }, push, { + currentUserId: userProfile?.id, + resourceMaintainer: userProfile?.id, + workspacePermissionKeys, + isRbacEnabled, + }) + } } catch { toast.error(t('newApp.appCreateFailed', { ns: 'app' })) diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx index fb58e964b75..008225e9963 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx @@ -29,6 +29,8 @@ const hotkeyMocks = vi.hoisted(() => ({ })) let mockPlanUsage = 0 let mockPlanTotal = 10 +let mockWorkspacePermissionKeys: string[] = ['app.create_and_management'] +const mockUserProfile = { id: 'user-1' } vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -86,6 +88,8 @@ vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({ vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ isCurrentWorkspaceEditor: true, + userProfile: mockUserProfile, + workspacePermissionKeys: mockWorkspacePermissionKeys, }), })) @@ -125,6 +129,7 @@ describe('CreateFromDSLModal', () => { hotkeyMocks.handlers.clear() mockPlanUsage = 0 mockPlanTotal = 10 + mockWorkspacePermissionKeys = ['app.create_and_management'] localStorage.clear() class MockFileReader { @@ -247,7 +252,45 @@ describe('CreateFromDSLModal', () => { expect(mockGetRedirection).toHaveBeenCalledWith( { id: 'app-1', mode: 'chat', permission_keys: ['app.acl.view_layout'] }, mockPush, - { isRbacEnabled: false }, + { + currentUserId: 'user-1', + resourceMaintainer: 'user-1', + workspacePermissionKeys: ['app.create_and_management'], + isRbacEnabled: false, + }, + ) + }) + + it('should pass creator context when import response has no permission keys', async () => { + mockImportDSL.mockResolvedValue({ + id: 'import-no-permissions', + status: DSLImportStatus.COMPLETED, + app_id: 'app-no-permissions', + app_mode: AppModeEnum.WORKFLOW, + }) + + render( + , + ) + + await act(async () => { + fireEvent.click(getCreateButton()) + }) + + expect(mockGetRedirection).toHaveBeenCalledWith( + { id: 'app-no-permissions', mode: AppModeEnum.WORKFLOW, permission_keys: undefined }, + mockPush, + { + currentUserId: 'user-1', + resourceMaintainer: 'user-1', + workspacePermissionKeys: ['app.create_and_management'], + isRbacEnabled: false, + }, ) }) @@ -363,6 +406,16 @@ describe('CreateFromDSLModal', () => { source: 'studio_upload', appMode: AppModeEnum.WORKFLOW, }) + expect(mockGetRedirection).toHaveBeenCalledWith( + { id: 'app-3', mode: AppModeEnum.WORKFLOW, permission_keys: ['app.acl.view_layout'] }, + mockPush, + { + currentUserId: 'user-1', + resourceMaintainer: 'user-1', + workspacePermissionKeys: ['app.create_and_management'], + isRbacEnabled: false, + }, + ) }) it('should close the DSL mismatch modal when dialog requests close', async () => { diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 8414b7c6f4f..191e1727120 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -15,6 +15,7 @@ import { useSetNeedRefreshAppList } from '@/app/components/apps/storage' import Input from '@/app/components/base/input' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { @@ -59,6 +60,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS const setNeedRefresh = useSetNeedRefreshAppList() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const isRbacEnabled = systemFeatures.rbac_enabled + const { userProfile, workspacePermissionKeys } = useAppContext() const readFile = useCallback((file: File) => { const reader = new FileReader() @@ -133,7 +135,12 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS invalidateAppList() if (app_id) { await handleCheckPluginDependencies(app_id) - getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { isRbacEnabled }) + getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { + currentUserId: userProfile?.id, + resourceMaintainer: userProfile?.id, + workspacePermissionKeys, + isRbacEnabled, + }) } } else if (status === DSLImportStatus.PENDING) { @@ -187,8 +194,14 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS await handleCheckPluginDependencies(app_id) setNeedRefresh('1') invalidateAppList() - if (app_id) - getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { isRbacEnabled }) + if (app_id) { + getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { + currentUserId: userProfile?.id, + resourceMaintainer: userProfile?.id, + workspacePermissionKeys, + isRbacEnabled, + }) + } } else if (status === DSLImportStatus.FAILED) { toast.error(response.error || t('newApp.appCreateFailed', { ns: 'app' }))