fix(web): redirect imported apps with creator permissions (#38460)

This commit is contained in:
非法操作
2026-07-06 17:11:56 +08:00
committed by GitHub
parent 64d72c6fb9
commit 1247fa28f1
4 changed files with 112 additions and 8 deletions

View File

@ -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(<Apps />)
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 () => {

View File

@ -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' }))

View File

@ -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(
<CreateFromDSLModal
show
onClose={vi.fn()}
activeTab={CreateFromDSLModalTab.FROM_URL}
dslUrl="https://example.com/app.yml"
/>,
)
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 () => {

View File

@ -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' }))