mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 10:38:32 +08:00
refactor(web): clarify app context bootstrap graph (#38516)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
6edce14e88
commit
56f3d0a11e
@ -7031,11 +7031,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/access-control/__tests__/use-permission-keys.spec.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/access-control/__tests__/use-workspace-access-rules.spec.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -7056,11 +7051,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/access-control/use-permission-keys.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/access-control/use-workspace-access-rules.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -7228,17 +7218,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/use-common.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-empty-object-type": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/use-datasource.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { useAppContext, useSelector } from '../app-context'
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { Provider as JotaiProvider } from 'jotai'
|
||||
import { queryClientAtom } from 'jotai-tanstack-query'
|
||||
import { useHydrateAtoms } from 'jotai/react/utils'
|
||||
import { Suspense } from 'react'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { initialWorkspaceInfo, useAppContext, useSelector } from '../app-context'
|
||||
import { AppContextProvider } from '../app-context-provider'
|
||||
|
||||
const mockInvalidateQueries = vi.hoisted(() => vi.fn())
|
||||
const mockGetRequest = vi.hoisted(() => vi.fn())
|
||||
const mockPermissionKeysState = vi.hoisted(() => ({
|
||||
isPending: false,
|
||||
permissionKeys: ['app.create_and_management'],
|
||||
@ -19,55 +29,84 @@ const mockCurrentWorkspaceResponse = vi.hoisted(() => ({
|
||||
next_credit_reset_date: 1706745600,
|
||||
custom_config: {},
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: mockInvalidateQueries,
|
||||
}),
|
||||
useSuspenseQuery: (options: { queryKey?: readonly unknown[] }) => {
|
||||
if (options.queryKey?.[0] === 'system-features') {
|
||||
return {
|
||||
data: {
|
||||
branding: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
const mockCurrentWorkspaceQueryState = vi.hoisted(() => ({
|
||||
data: mockCurrentWorkspaceResponse as typeof mockCurrentWorkspaceResponse | undefined,
|
||||
isPending: false,
|
||||
}))
|
||||
const mockUserProfileResponseState = vi.hoisted(() => ({
|
||||
data: {
|
||||
profile: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: true,
|
||||
},
|
||||
meta: {
|
||||
currentVersion: '1.0.0',
|
||||
currentEnv: 'cloud',
|
||||
},
|
||||
} as {
|
||||
profile?: {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
avatar_url: string
|
||||
is_password_set: boolean
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
profile: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: true,
|
||||
},
|
||||
meta: {
|
||||
currentVersion: '1.0.0',
|
||||
currentEnv: 'cloud',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
currentVersion: string | null
|
||||
currentEnv: string | null
|
||||
}
|
||||
},
|
||||
useQuery: (options: { select?: (workspace: typeof mockCurrentWorkspaceResponse) => unknown }) => ({
|
||||
data: options.select ? options.select(mockCurrentWorkspaceResponse) : mockCurrentWorkspaceResponse,
|
||||
isFetching: false,
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
const mockSystemFeaturesState = vi.hoisted(() => ({
|
||||
data: {
|
||||
branding: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}))
|
||||
const mockLangGeniusVersionState = vi.hoisted(() => ({
|
||||
data: {
|
||||
version: '1.0.1',
|
||||
release_date: '',
|
||||
release_notes: '',
|
||||
can_auto_update: false,
|
||||
} as {
|
||||
version: string
|
||||
release_date: string
|
||||
release_notes: string
|
||||
can_auto_update: boolean
|
||||
} | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
ZENDESK_FIELD_IDS: {
|
||||
ENVIRONMENT: 'environment-field',
|
||||
VERSION: 'version-field',
|
||||
EMAIL: 'email-field',
|
||||
WORKSPACE_ID: 'workspace-id-field',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/features/system-features/client', () => ({
|
||||
systemFeaturesQueryOptions: () => ({
|
||||
queryKey: ['system-features'],
|
||||
queryFn: async () => mockSystemFeaturesState.data,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/account-profile/client', () => ({
|
||||
userProfileQueryOptions: () => ({
|
||||
queryKey: ['user-profile'],
|
||||
queryFn: async () => mockUserProfileResponseState.data,
|
||||
}),
|
||||
}))
|
||||
|
||||
@ -77,8 +116,16 @@ vi.mock('@/service/client', () => ({
|
||||
current: {
|
||||
post: {
|
||||
key: () => ['current-workspace'],
|
||||
queryOptions: (options: Record<string, unknown>) => ({
|
||||
queryOptions: (options: {
|
||||
select?: (workspace?: typeof mockCurrentWorkspaceResponse) => unknown
|
||||
}) => ({
|
||||
queryKey: ['current-workspace'],
|
||||
queryFn: async () => {
|
||||
if (mockCurrentWorkspaceQueryState.isPending)
|
||||
return new Promise(() => {})
|
||||
|
||||
return mockCurrentWorkspaceQueryState.data
|
||||
},
|
||||
...options,
|
||||
}),
|
||||
},
|
||||
@ -87,34 +134,9 @@ vi.mock('@/service/client', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/access-control/use-permission-keys', () => ({
|
||||
useWorkspacePermissionKeys: () => ({
|
||||
data: {
|
||||
workspace: {
|
||||
permission_keys: mockPermissionKeysState.permissionKeys,
|
||||
},
|
||||
app: {
|
||||
default_permission_keys: [],
|
||||
overrides: [],
|
||||
},
|
||||
dataset: {
|
||||
default_permission_keys: [],
|
||||
overrides: [],
|
||||
},
|
||||
},
|
||||
isPending: mockPermissionKeysState.isPending,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useLangGeniusVersion: () => ({
|
||||
data: {
|
||||
version: '1.0.1',
|
||||
release_date: '',
|
||||
release_notes: '',
|
||||
can_auto_update: false,
|
||||
},
|
||||
}),
|
||||
vi.mock('@/service/base', () => ({
|
||||
get: mockGetRequest,
|
||||
post: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
@ -145,35 +167,303 @@ function AppContextProbe() {
|
||||
{selectedWorkspacePermissionKeys.join(',')}
|
||||
</span>
|
||||
<span>
|
||||
loading:
|
||||
permission loading:
|
||||
{String(context.isLoadingWorkspacePermissionKeys)}
|
||||
</span>
|
||||
<span>
|
||||
workspace loading:
|
||||
{String(context.isLoadingCurrentWorkspace)}
|
||||
</span>
|
||||
<span>
|
||||
workspace validating:
|
||||
{String(context.isValidatingCurrentWorkspace)}
|
||||
</span>
|
||||
<span>
|
||||
user:
|
||||
{context.userProfile.email}
|
||||
</span>
|
||||
<span>
|
||||
workspace:
|
||||
{context.currentWorkspace.name}
|
||||
</span>
|
||||
<span>
|
||||
role:
|
||||
{context.currentWorkspace.role}
|
||||
</span>
|
||||
<span>
|
||||
manager:
|
||||
{String(context.isCurrentWorkspaceManager)}
|
||||
</span>
|
||||
<span>
|
||||
owner:
|
||||
{String(context.isCurrentWorkspaceOwner)}
|
||||
</span>
|
||||
<span>
|
||||
editor:
|
||||
{String(context.isCurrentWorkspaceEditor)}
|
||||
</span>
|
||||
<span>
|
||||
dataset operator:
|
||||
{String(context.isCurrentWorkspaceDatasetOperator)}
|
||||
</span>
|
||||
<span>
|
||||
version:
|
||||
{context.langGeniusVersionInfo.current_version}
|
||||
/
|
||||
{context.langGeniusVersionInfo.latest_version}
|
||||
/
|
||||
{context.langGeniusVersionInfo.current_env}
|
||||
</span>
|
||||
<button type="button" onClick={context.mutateUserProfile}>refresh user</button>
|
||||
<button type="button" onClick={context.mutateCurrentWorkspace}>refresh workspace</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function TestQueryClientHydrator({
|
||||
children,
|
||||
queryClient,
|
||||
}: {
|
||||
children: ReactNode
|
||||
queryClient: QueryClient
|
||||
}) {
|
||||
useHydrateAtoms(new Map([[queryClientAtom, queryClient]]))
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
function createTestQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function renderProvider() {
|
||||
const queryClient = createTestQueryClient()
|
||||
const view = render(
|
||||
<JotaiProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TestQueryClientHydrator queryClient={queryClient}>
|
||||
<Suspense fallback={<span>loading</span>}>
|
||||
<AppContextProvider>
|
||||
<AppContextProbe />
|
||||
</AppContextProvider>
|
||||
</Suspense>
|
||||
</TestQueryClientHydrator>
|
||||
</QueryClientProvider>
|
||||
</JotaiProvider>,
|
||||
)
|
||||
|
||||
return {
|
||||
...view,
|
||||
queryClient,
|
||||
}
|
||||
}
|
||||
|
||||
describe('AppContextProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPermissionKeysState.isPending = false
|
||||
mockPermissionKeysState.permissionKeys = ['app.create_and_management']
|
||||
mockCurrentWorkspaceQueryState.data = mockCurrentWorkspaceResponse
|
||||
mockCurrentWorkspaceQueryState.isPending = false
|
||||
mockUserProfileResponseState.data = {
|
||||
profile: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: true,
|
||||
},
|
||||
meta: {
|
||||
currentVersion: '1.0.0',
|
||||
currentEnv: 'cloud',
|
||||
},
|
||||
}
|
||||
mockSystemFeaturesState.data = {
|
||||
branding: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
mockLangGeniusVersionState.data = {
|
||||
version: '1.0.1',
|
||||
release_date: '',
|
||||
release_notes: '',
|
||||
can_auto_update: false,
|
||||
}
|
||||
mockGetRequest.mockImplementation((url: string) => {
|
||||
if (url === '/workspaces/current/rbac/my-permissions') {
|
||||
if (mockPermissionKeysState.isPending)
|
||||
return new Promise(() => {})
|
||||
|
||||
return Promise.resolve({
|
||||
workspace: {
|
||||
permission_keys: mockPermissionKeysState.permissionKeys,
|
||||
},
|
||||
app: {
|
||||
default_permission_keys: [],
|
||||
overrides: [],
|
||||
},
|
||||
dataset: {
|
||||
default_permission_keys: [],
|
||||
overrides: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (url === '/version')
|
||||
return Promise.resolve(mockLangGeniusVersionState.data)
|
||||
|
||||
return Promise.reject(new Error(`Unexpected GET ${url}`))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Workspace Permission Keys', () => {
|
||||
it('should provide current workspace permission keys from my-permissions', () => {
|
||||
render(
|
||||
<AppContextProvider>
|
||||
<AppContextProbe />
|
||||
</AppContextProvider>,
|
||||
)
|
||||
describe('Context compatibility values', () => {
|
||||
it('should provide profile, workspace, permissions, loading state, and version metadata', async () => {
|
||||
renderProvider()
|
||||
|
||||
expect(screen.getByText('keys:app.create_and_management')).toBeInTheDocument()
|
||||
expect(screen.getByText('loading:false')).toBeInTheDocument()
|
||||
expect(screen.getByText('role:editor')).toBeInTheDocument()
|
||||
expect(await screen.findByText('user:user@example.com')).toBeInTheDocument()
|
||||
expect(await screen.findByText('workspace:Workspace')).toBeInTheDocument()
|
||||
expect(await screen.findByText('keys:app.create_and_management')).toBeInTheDocument()
|
||||
expect(screen.getByText('permission loading:false')).toBeInTheDocument()
|
||||
expect(screen.getByText('workspace loading:false')).toBeInTheDocument()
|
||||
expect(screen.getByText('workspace validating:false')).toBeInTheDocument()
|
||||
expect(await screen.findByText('version:1.0.0/1.0.1/cloud')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should fall back to placeholder values when profile, workspace, permission, or version data is missing', async () => {
|
||||
mockUserProfileResponseState.data = {
|
||||
meta: {
|
||||
currentVersion: null,
|
||||
currentEnv: null,
|
||||
},
|
||||
}
|
||||
mockCurrentWorkspaceQueryState.data = undefined
|
||||
mockPermissionKeysState.permissionKeys = []
|
||||
mockLangGeniusVersionState.data = undefined
|
||||
|
||||
renderProvider()
|
||||
|
||||
expect(await screen.findByText('user:')).toBeInTheDocument()
|
||||
expect(screen.getByText(`workspace:${initialWorkspaceInfo.name}`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument()
|
||||
expect(screen.getByText('keys:')).toBeInTheDocument()
|
||||
expect(screen.getByText('version://')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should normalize invalid workspace roles to the initial workspace role', async () => {
|
||||
mockCurrentWorkspaceQueryState.data = {
|
||||
...mockCurrentWorkspaceResponse,
|
||||
role: 'unsupported-role',
|
||||
}
|
||||
|
||||
renderProvider()
|
||||
|
||||
expect(await screen.findByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should derive role flags from the current workspace role', async () => {
|
||||
mockCurrentWorkspaceQueryState.data = {
|
||||
...mockCurrentWorkspaceResponse,
|
||||
role: 'owner',
|
||||
}
|
||||
|
||||
renderProvider()
|
||||
|
||||
expect(await screen.findByText('manager:true')).toBeInTheDocument()
|
||||
expect(screen.getByText('owner:true')).toBeInTheDocument()
|
||||
expect(screen.getByText('editor:true')).toBeInTheDocument()
|
||||
expect(screen.getByText('dataset operator:false')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose query loading and validating state', async () => {
|
||||
mockPermissionKeysState.isPending = true
|
||||
mockCurrentWorkspaceQueryState.isPending = true
|
||||
|
||||
renderProvider()
|
||||
|
||||
expect(await screen.findByText('workspace loading:true')).toBeInTheDocument()
|
||||
expect(screen.getByText('workspace validating:true')).toBeInTheDocument()
|
||||
expect(screen.getByText('permission loading:true')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Refresh actions', () => {
|
||||
it('should invalidate the source queries when refresh actions are called', async () => {
|
||||
const { queryClient } = renderProvider()
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /refresh user/i }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /refresh workspace/i }))
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user-profile'] })
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['current-workspace'] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('External side effects', () => {
|
||||
it('should sync Zendesk fields and Amplitude identity when bootstrap data is available', async () => {
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setZendeskConversationFields).toHaveBeenCalledWith([{
|
||||
id: ZENDESK_FIELD_IDS.ENVIRONMENT,
|
||||
value: 'cloud',
|
||||
}])
|
||||
})
|
||||
expect(setZendeskConversationFields).toHaveBeenCalledWith([{
|
||||
id: ZENDESK_FIELD_IDS.VERSION,
|
||||
value: '1.0.1',
|
||||
}])
|
||||
expect(setZendeskConversationFields).toHaveBeenCalledWith([{
|
||||
id: ZENDESK_FIELD_IDS.EMAIL,
|
||||
value: 'user@example.com',
|
||||
}])
|
||||
await waitFor(() => {
|
||||
expect(setZendeskConversationFields).toHaveBeenCalledWith([{
|
||||
id: ZENDESK_FIELD_IDS.WORKSPACE_ID,
|
||||
value: 'workspace-1',
|
||||
}])
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(setUserId).toHaveBeenCalledWith('user@example.com')
|
||||
expect(setUserProperties).toHaveBeenCalledWith(expect.objectContaining({
|
||||
email: 'user@example.com',
|
||||
workspace_id: 'workspace-1',
|
||||
workspace_role: 'editor',
|
||||
}))
|
||||
expect(flushRegistrationSuccess).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not sync Amplitude identity when user id is missing', async () => {
|
||||
mockUserProfileResponseState.data = {
|
||||
profile: {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: false,
|
||||
},
|
||||
meta: {
|
||||
currentVersion: '1.0.0',
|
||||
currentEnv: 'cloud',
|
||||
},
|
||||
}
|
||||
|
||||
renderProvider()
|
||||
|
||||
await screen.findByText('user:')
|
||||
expect(setUserId).not.toHaveBeenCalled()
|
||||
expect(setUserProperties).not.toHaveBeenCalled()
|
||||
expect(flushRegistrationSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
82
web/context/app-context-effects.ts
Normal file
82
web/context/app-context-effects.ts
Normal file
@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useEffect } from 'react'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
langGeniusVersionInfoAtom,
|
||||
userProfileAtom,
|
||||
} from './app-context-state'
|
||||
|
||||
export function useSyncZendeskFields() {
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.ENVIRONMENT,
|
||||
value: langGeniusVersionInfo.current_env.toLowerCase(),
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.current_env])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.VERSION && langGeniusVersionInfo?.version) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.VERSION,
|
||||
value: langGeniusVersionInfo.version,
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.version])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.EMAIL && userProfile?.email) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.EMAIL,
|
||||
value: userProfile.email,
|
||||
}])
|
||||
}
|
||||
}, [userProfile?.email])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.WORKSPACE_ID && currentWorkspace?.id) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.WORKSPACE_ID,
|
||||
value: currentWorkspace.id,
|
||||
}])
|
||||
}
|
||||
}, [currentWorkspace?.id])
|
||||
}
|
||||
|
||||
export function useSyncAmplitudeIdentity() {
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile?.id) {
|
||||
setUserId(userProfile.email)
|
||||
const properties: Record<string, string | number | boolean> = {
|
||||
email: userProfile.email,
|
||||
name: userProfile.name,
|
||||
has_password: userProfile.is_password_set,
|
||||
}
|
||||
|
||||
if (currentWorkspace?.id) {
|
||||
properties.workspace_id = currentWorkspace.id
|
||||
properties.workspace_name = currentWorkspace.name
|
||||
properties.workspace_plan = currentWorkspace.plan
|
||||
properties.workspace_status = currentWorkspace.status
|
||||
properties.workspace_role = currentWorkspace.role
|
||||
}
|
||||
|
||||
setUserProperties(properties)
|
||||
flushRegistrationSuccess()
|
||||
}
|
||||
}, [userProfile, currentWorkspace])
|
||||
}
|
||||
78
web/context/app-context-normalizers.ts
Normal file
78
web/context/app-context-normalizers.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
|
||||
import { initialLangGeniusVersionInfo, initialWorkspaceInfo } from './app-context'
|
||||
|
||||
const workspaceRoles = new Set<ICurrentWorkspace['role']>(['owner', 'admin', 'editor', 'dataset_operator', 'normal'])
|
||||
|
||||
export const emptyWorkspacePermissionKeys: string[] = []
|
||||
|
||||
export type WorkspaceRoleFlags = {
|
||||
isCurrentWorkspaceManager: boolean
|
||||
isCurrentWorkspaceOwner: boolean
|
||||
isCurrentWorkspaceEditor: boolean
|
||||
isCurrentWorkspaceDatasetOperator: boolean
|
||||
}
|
||||
|
||||
export type ProfileMeta = {
|
||||
currentVersion: string | null
|
||||
currentEnv: string | null
|
||||
}
|
||||
|
||||
function resolveWorkspaceRole(role: PostWorkspacesCurrentResponse['role']): ICurrentWorkspace['role'] {
|
||||
if (role && workspaceRoles.has(role as ICurrentWorkspace['role']))
|
||||
return role as ICurrentWorkspace['role']
|
||||
|
||||
return initialWorkspaceInfo.role
|
||||
}
|
||||
|
||||
export function normalizeCurrentWorkspace(workspace?: PostWorkspacesCurrentResponse): ICurrentWorkspace {
|
||||
if (!workspace)
|
||||
return initialWorkspaceInfo
|
||||
|
||||
return {
|
||||
id: workspace.id,
|
||||
name: workspace.name ?? initialWorkspaceInfo.name,
|
||||
plan: workspace.plan ?? initialWorkspaceInfo.plan,
|
||||
status: workspace.status ?? initialWorkspaceInfo.status,
|
||||
created_at: workspace.created_at ?? initialWorkspaceInfo.created_at,
|
||||
role: resolveWorkspaceRole(workspace.role),
|
||||
providers: initialWorkspaceInfo.providers,
|
||||
trial_credits: workspace.trial_credits ?? initialWorkspaceInfo.trial_credits,
|
||||
trial_credits_used: workspace.trial_credits_used ?? initialWorkspaceInfo.trial_credits_used,
|
||||
next_credit_reset_date: workspace.next_credit_reset_date ?? initialWorkspaceInfo.next_credit_reset_date,
|
||||
trial_end_reason: workspace.trial_end_reason ?? undefined,
|
||||
custom_config: workspace.custom_config
|
||||
? {
|
||||
remove_webapp_brand: workspace.custom_config.remove_webapp_brand ?? undefined,
|
||||
replace_webapp_logo: workspace.custom_config.replace_webapp_logo ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceRoleFlags(currentWorkspace: ICurrentWorkspace): WorkspaceRoleFlags {
|
||||
return {
|
||||
isCurrentWorkspaceManager: ['owner', 'admin'].includes(currentWorkspace.role),
|
||||
isCurrentWorkspaceOwner: currentWorkspace.role === 'owner',
|
||||
isCurrentWorkspaceEditor: ['owner', 'admin', 'editor'].includes(currentWorkspace.role),
|
||||
isCurrentWorkspaceDatasetOperator: currentWorkspace.role === 'dataset_operator',
|
||||
}
|
||||
}
|
||||
|
||||
export function getLangGeniusVersionInfo({
|
||||
meta,
|
||||
versionData,
|
||||
}: {
|
||||
meta: ProfileMeta
|
||||
versionData?: Omit<LangGeniusVersionResponse, 'current_version' | 'latest_version' | 'current_env'>
|
||||
}): LangGeniusVersionResponse {
|
||||
if (!meta.currentVersion || !versionData)
|
||||
return initialLangGeniusVersionInfo
|
||||
|
||||
return {
|
||||
...versionData,
|
||||
current_version: meta.currentVersion,
|
||||
latest_version: versionData.version,
|
||||
current_env: meta.currentEnv || '',
|
||||
}
|
||||
}
|
||||
@ -1,193 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
|
||||
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import {
|
||||
AppContext,
|
||||
initialLangGeniusVersionInfo,
|
||||
initialWorkspaceInfo,
|
||||
userProfilePlaceholder,
|
||||
useSelector,
|
||||
} from '@/context/app-context'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useWorkspacePermissionKeys } from '@/service/access-control/use-permission-keys'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
useLangGeniusVersion,
|
||||
} from '@/service/use-common'
|
||||
currentWorkspaceAtom,
|
||||
currentWorkspaceLoadingAtom,
|
||||
currentWorkspaceValidatingAtom,
|
||||
langGeniusVersionInfoAtom,
|
||||
refreshCurrentWorkspaceAtom,
|
||||
refreshUserProfileAtom,
|
||||
userProfileAtom,
|
||||
workspacePermissionKeysAtom,
|
||||
workspacePermissionKeysLoadingAtom,
|
||||
workspaceRoleFlagsAtom,
|
||||
} from '@/context/app-context-state'
|
||||
import {
|
||||
useSyncAmplitudeIdentity,
|
||||
useSyncZendeskFields,
|
||||
} from './app-context-effects'
|
||||
|
||||
type AppContextProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const workspaceRoles = new Set<ICurrentWorkspace['role']>(['owner', 'admin', 'editor', 'dataset_operator', 'normal'])
|
||||
const emptyWorkspacePermissionKeys: string[] = []
|
||||
|
||||
const resolveWorkspaceRole = (role: PostWorkspacesCurrentResponse['role']): ICurrentWorkspace['role'] => {
|
||||
if (role && workspaceRoles.has(role as ICurrentWorkspace['role']))
|
||||
return role as ICurrentWorkspace['role']
|
||||
|
||||
return initialWorkspaceInfo.role
|
||||
}
|
||||
|
||||
const normalizeCurrentWorkspace = (workspace?: PostWorkspacesCurrentResponse): ICurrentWorkspace => {
|
||||
if (!workspace)
|
||||
return initialWorkspaceInfo
|
||||
|
||||
return {
|
||||
id: workspace.id,
|
||||
name: workspace.name ?? initialWorkspaceInfo.name,
|
||||
plan: workspace.plan ?? initialWorkspaceInfo.plan,
|
||||
status: workspace.status ?? initialWorkspaceInfo.status,
|
||||
created_at: workspace.created_at ?? initialWorkspaceInfo.created_at,
|
||||
role: resolveWorkspaceRole(workspace.role),
|
||||
providers: initialWorkspaceInfo.providers,
|
||||
trial_credits: workspace.trial_credits ?? initialWorkspaceInfo.trial_credits,
|
||||
trial_credits_used: workspace.trial_credits_used ?? initialWorkspaceInfo.trial_credits_used,
|
||||
next_credit_reset_date: workspace.next_credit_reset_date ?? initialWorkspaceInfo.next_credit_reset_date,
|
||||
trial_end_reason: workspace.trial_end_reason ?? undefined,
|
||||
custom_config: workspace.custom_config
|
||||
? {
|
||||
remove_webapp_brand: workspace.custom_config.remove_webapp_brand ?? undefined,
|
||||
replace_webapp_logo: workspace.custom_config.replace_webapp_logo ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function AppContextProvider({ children }: AppContextProviderProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions())
|
||||
const currentWorkspaceQuery = useQuery(consoleQuery.workspaces.current.post.queryOptions({
|
||||
select: normalizeCurrentWorkspace,
|
||||
}))
|
||||
const workspacePermissionKeysQuery = useWorkspacePermissionKeys()
|
||||
const langGeniusVersionQuery = useLangGeniusVersion(
|
||||
userProfileResp?.meta.currentVersion,
|
||||
!systemFeatures.branding.enabled,
|
||||
)
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const roleFlags = useAtomValue(workspaceRoleFlagsAtom)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
|
||||
const isValidatingCurrentWorkspace = useAtomValue(currentWorkspaceValidatingAtom)
|
||||
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
|
||||
|
||||
const userProfile = useMemo<GetAccountProfileResponse>(() => userProfileResp?.profile || userProfilePlaceholder, [userProfileResp?.profile])
|
||||
const currentWorkspace = currentWorkspaceQuery.data ?? initialWorkspaceInfo
|
||||
const langGeniusVersionInfo = useMemo<LangGeniusVersionResponse>(() => {
|
||||
if (!userProfileResp?.meta?.currentVersion || !langGeniusVersionQuery.data)
|
||||
return initialLangGeniusVersionInfo
|
||||
const refreshUserProfile = useSetAtom(refreshUserProfileAtom)
|
||||
const refreshCurrentWorkspace = useSetAtom(refreshCurrentWorkspaceAtom)
|
||||
|
||||
const current_version = userProfileResp.meta.currentVersion
|
||||
const current_env = userProfileResp.meta.currentEnv || ''
|
||||
const versionData = langGeniusVersionQuery.data
|
||||
return {
|
||||
...versionData,
|
||||
current_version,
|
||||
latest_version: versionData.version,
|
||||
current_env,
|
||||
}
|
||||
}, [langGeniusVersionQuery.data, userProfileResp?.meta])
|
||||
|
||||
const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
|
||||
const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
|
||||
const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
|
||||
const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role])
|
||||
|
||||
const mutateUserProfile = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: userProfileQueryOptions().queryKey })
|
||||
}, [queryClient])
|
||||
|
||||
const mutateCurrentWorkspace = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.post.key() })
|
||||
}, [queryClient])
|
||||
|
||||
// #region Zendesk conversation fields
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.ENVIRONMENT,
|
||||
value: langGeniusVersionInfo.current_env.toLowerCase(),
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.current_env])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.VERSION && langGeniusVersionInfo?.version) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.VERSION,
|
||||
value: langGeniusVersionInfo.version,
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.version])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.EMAIL && userProfile?.email) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.EMAIL,
|
||||
value: userProfile.email,
|
||||
}])
|
||||
}
|
||||
}, [userProfile?.email])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.WORKSPACE_ID && currentWorkspace?.id) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.WORKSPACE_ID,
|
||||
value: currentWorkspace.id,
|
||||
}])
|
||||
}
|
||||
}, [currentWorkspace?.id])
|
||||
// #endregion Zendesk conversation fields
|
||||
|
||||
useEffect(() => {
|
||||
// Report user and workspace info to Amplitude when loaded
|
||||
if (userProfile?.id) {
|
||||
setUserId(userProfile.email)
|
||||
const properties: Record<string, string | number | boolean> = {
|
||||
email: userProfile.email,
|
||||
name: userProfile.name,
|
||||
has_password: userProfile.is_password_set,
|
||||
}
|
||||
|
||||
if (currentWorkspace?.id) {
|
||||
properties.workspace_id = currentWorkspace.id
|
||||
properties.workspace_name = currentWorkspace.name
|
||||
properties.workspace_plan = currentWorkspace.plan
|
||||
properties.workspace_status = currentWorkspace.status
|
||||
properties.workspace_role = currentWorkspace.role
|
||||
}
|
||||
|
||||
setUserProperties(properties)
|
||||
|
||||
// The user ID is now attached, so replay any registration success event captured
|
||||
// at signup time. This makes it land on the identified Amplitude profile instead
|
||||
// of an anonymous one (no-op when nothing was deferred).
|
||||
flushRegistrationSuccess()
|
||||
}
|
||||
}, [userProfile, currentWorkspace])
|
||||
useSyncZendeskFields()
|
||||
useSyncAmplitudeIdentity()
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
userProfile,
|
||||
mutateUserProfile,
|
||||
mutateUserProfile: () => {
|
||||
refreshUserProfile()
|
||||
},
|
||||
langGeniusVersionInfo,
|
||||
useSelector,
|
||||
currentWorkspace,
|
||||
isCurrentWorkspaceManager,
|
||||
isCurrentWorkspaceOwner,
|
||||
isCurrentWorkspaceEditor,
|
||||
isCurrentWorkspaceDatasetOperator,
|
||||
mutateCurrentWorkspace,
|
||||
isLoadingCurrentWorkspace: currentWorkspaceQuery.isPending,
|
||||
isLoadingWorkspacePermissionKeys: workspacePermissionKeysQuery.isPending,
|
||||
isValidatingCurrentWorkspace: currentWorkspaceQuery.isFetching,
|
||||
workspacePermissionKeys: workspacePermissionKeysQuery.data?.workspace.permission_keys ?? emptyWorkspacePermissionKeys,
|
||||
...roleFlags,
|
||||
mutateCurrentWorkspace: () => {
|
||||
refreshCurrentWorkspace()
|
||||
},
|
||||
isLoadingCurrentWorkspace,
|
||||
isLoadingWorkspacePermissionKeys,
|
||||
isValidatingCurrentWorkspace,
|
||||
workspacePermissionKeys,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
116
web/context/app-context-state.ts
Normal file
116
web/context/app-context-state.ts
Normal file
@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
|
||||
import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen'
|
||||
import type { DefinedQueryObserverResult } from '@tanstack/react-query'
|
||||
import type { UserProfileWithMeta } from '@/features/account-profile/client'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithQuery, atomWithSuspenseQuery, queryClientAtom } from 'jotai-tanstack-query'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { workspacePermissionKeysQueryOptions } from '@/service/access-control/use-permission-keys'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { langGeniusVersionQueryOptions } from '@/service/lang-genius-version'
|
||||
import {
|
||||
initialLangGeniusVersionInfo,
|
||||
initialWorkspaceInfo,
|
||||
userProfilePlaceholder,
|
||||
} from './app-context'
|
||||
import {
|
||||
emptyWorkspacePermissionKeys,
|
||||
getLangGeniusVersionInfo,
|
||||
getWorkspaceRoleFlags,
|
||||
normalizeCurrentWorkspace,
|
||||
} from './app-context-normalizers'
|
||||
|
||||
type SuspenseQueryResult<T> = Omit<DefinedQueryObserverResult<T>, 'isPlaceholderData'>
|
||||
|
||||
const accountProfileQueryAtom = atomWithSuspenseQuery(() => userProfileQueryOptions())
|
||||
|
||||
const systemFeaturesQueryAtom = atomWithSuspenseQuery(() => systemFeaturesQueryOptions())
|
||||
|
||||
export const userProfileAtom = atom((get): GetAccountProfileResponse => {
|
||||
const accountProfileQuery = get(accountProfileQueryAtom) as SuspenseQueryResult<UserProfileWithMeta>
|
||||
|
||||
return accountProfileQuery.data?.profile || userProfilePlaceholder
|
||||
})
|
||||
|
||||
const profileMetaAtom = atom((get) => {
|
||||
const accountProfileQuery = get(accountProfileQueryAtom) as SuspenseQueryResult<UserProfileWithMeta>
|
||||
|
||||
return accountProfileQuery.data?.meta ?? {
|
||||
currentVersion: null,
|
||||
currentEnv: null,
|
||||
}
|
||||
})
|
||||
|
||||
const currentWorkspaceQueryAtom = atomWithQuery(() => {
|
||||
return consoleQuery.workspaces.current.post.queryOptions({
|
||||
select: normalizeCurrentWorkspace,
|
||||
})
|
||||
})
|
||||
|
||||
const normalizedCurrentWorkspaceAtom = atom((get) => {
|
||||
return get(currentWorkspaceQueryAtom).data ?? initialWorkspaceInfo
|
||||
})
|
||||
|
||||
export const currentWorkspaceAtom = atom((get) => {
|
||||
return get(normalizedCurrentWorkspaceAtom)
|
||||
})
|
||||
|
||||
export const workspaceRoleFlagsAtom = atom((get) => {
|
||||
return getWorkspaceRoleFlags(get(currentWorkspaceAtom))
|
||||
})
|
||||
|
||||
const workspacePermissionKeysQueryAtom = atomWithQuery((get) => {
|
||||
const workspaceId = get(currentWorkspaceAtom).id
|
||||
|
||||
return workspacePermissionKeysQueryOptions(workspaceId)
|
||||
})
|
||||
|
||||
export const workspacePermissionKeysAtom = atom((get) => {
|
||||
return get(workspacePermissionKeysQueryAtom).data?.workspace.permission_keys ?? emptyWorkspacePermissionKeys
|
||||
})
|
||||
|
||||
export const workspacePermissionKeysLoadingAtom = atom((get) => {
|
||||
return get(workspacePermissionKeysQueryAtom).isPending
|
||||
})
|
||||
|
||||
export const currentWorkspaceLoadingAtom = atom((get) => {
|
||||
return get(currentWorkspaceQueryAtom).isPending
|
||||
})
|
||||
|
||||
export const currentWorkspaceValidatingAtom = atom((get) => {
|
||||
return get(currentWorkspaceQueryAtom).isFetching
|
||||
})
|
||||
|
||||
const versionQueryAtom = atomWithQuery((get) => {
|
||||
const meta = get(profileMetaAtom)
|
||||
const systemFeaturesQuery = get(systemFeaturesQueryAtom) as SuspenseQueryResult<GetSystemFeaturesResponse>
|
||||
const enabled = Boolean(meta.currentVersion && !systemFeaturesQuery.data?.branding.enabled)
|
||||
|
||||
return langGeniusVersionQueryOptions(meta.currentVersion, enabled)
|
||||
})
|
||||
|
||||
export const langGeniusVersionInfoAtom = atom((get) => {
|
||||
const meta = get(profileMetaAtom)
|
||||
const versionData = get(versionQueryAtom).data
|
||||
|
||||
if (!versionData)
|
||||
return initialLangGeniusVersionInfo
|
||||
|
||||
return getLangGeniusVersionInfo({
|
||||
meta,
|
||||
versionData,
|
||||
})
|
||||
})
|
||||
|
||||
export const refreshUserProfileAtom = atom(null, (get) => {
|
||||
const queryClient = get(queryClientAtom)
|
||||
queryClient.invalidateQueries({ queryKey: userProfileQueryOptions().queryKey })
|
||||
})
|
||||
|
||||
export const refreshCurrentWorkspaceAtom = atom(null, (get) => {
|
||||
const queryClient = get(queryClientAtom)
|
||||
queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.post.key() })
|
||||
})
|
||||
@ -1,26 +1,13 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { get } from '@/service/base'
|
||||
import { useWorkspacePermissionKeys } from '../use-permission-keys'
|
||||
import { workspacePermissionKeysQueryOptions } from '../use-permission-keys'
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
get: vi.fn(),
|
||||
}))
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useWorkspacePermissionKeys', () => {
|
||||
describe('workspacePermissionKeysQueryOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(get).mockResolvedValue({
|
||||
@ -33,11 +20,15 @@ describe('useWorkspacePermissionKeys', () => {
|
||||
// Current-user permissions come from the my-permissions RBAC endpoint.
|
||||
describe('Queries', () => {
|
||||
it('should fetch workspace permission keys', async () => {
|
||||
renderHook(() => useWorkspacePermissionKeys(), { wrapper: createWrapper() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith('/workspaces/current/rbac/my-permissions')
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
await queryClient.fetchQuery(workspacePermissionKeysQueryOptions())
|
||||
|
||||
expect(get).toHaveBeenCalledWith('/workspaces/current/rbac/my-permissions')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import type { PermissionKeysResponse } from '@/models/access-control'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { get } from '../base'
|
||||
|
||||
const NAME_SPACE = 'workspace-permission-keys'
|
||||
|
||||
export const useWorkspacePermissionKeys = () => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE],
|
||||
const workspacePermissionKeysQueryKey = (workspaceId?: string) => {
|
||||
return workspaceId ? [NAME_SPACE, workspaceId] as const : [NAME_SPACE] as const
|
||||
}
|
||||
|
||||
export const workspacePermissionKeysQueryOptions = (workspaceId?: string) => {
|
||||
return queryOptions<PermissionKeysResponse>({
|
||||
queryKey: workspacePermissionKeysQueryKey(workspaceId),
|
||||
queryFn: () => get<PermissionKeysResponse>('/workspaces/current/rbac/my-permissions'),
|
||||
enabled: workspaceId === undefined || Boolean(workspaceId),
|
||||
})
|
||||
}
|
||||
|
||||
13
web/service/lang-genius-version.ts
Normal file
13
web/service/lang-genius-version.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { LangGeniusVersionResponse } from '@/models/common'
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { get } from './base'
|
||||
import { commonQueryKeys } from './use-common'
|
||||
|
||||
export const langGeniusVersionQueryOptions = (currentVersion?: string | null, enabled?: boolean) => {
|
||||
return queryOptions<LangGeniusVersionResponse>({
|
||||
queryKey: commonQueryKeys.langGeniusVersion(currentVersion || undefined),
|
||||
queryFn: () => get<LangGeniusVersionResponse>('/version', { params: { current_version: currentVersion } }),
|
||||
enabled: !!currentVersion && (enabled ?? true),
|
||||
})
|
||||
}
|
||||
@ -10,13 +10,13 @@ import type {
|
||||
CodeBasedExtension,
|
||||
CommonResponse,
|
||||
FileUploadConfigResponse,
|
||||
LangGeniusVersionResponse,
|
||||
Member,
|
||||
StructuredOutputRulesRequestBody,
|
||||
StructuredOutputRulesResponse,
|
||||
} from '@/models/common'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { get, post } from './base'
|
||||
|
||||
const NAME_SPACE = 'common'
|
||||
@ -54,14 +54,6 @@ export const useFileUploadConfig = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useLangGeniusVersion = (currentVersion?: string | null, enabled?: boolean) => {
|
||||
return useQuery<LangGeniusVersionResponse>({
|
||||
queryKey: commonQueryKeys.langGeniusVersion(currentVersion || undefined),
|
||||
queryFn: () => get<LangGeniusVersionResponse>('/version', { params: { current_version: currentVersion } }),
|
||||
enabled: !!currentVersion && (enabled ?? true),
|
||||
})
|
||||
}
|
||||
|
||||
export const useGenerateStructuredOutputRules = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'generate-structured-output-rules'],
|
||||
@ -95,7 +87,7 @@ export const useMailValidity = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export type MailRegisterResponse = { result: string, data: {} }
|
||||
export type MailRegisterResponse = { result: string, data: Record<string, never> }
|
||||
|
||||
export const useMailRegister = () => {
|
||||
return useMutation({
|
||||
@ -149,7 +141,7 @@ export const useFilePreview = (fileID: string) => {
|
||||
export type SchemaTypeDefinition = {
|
||||
name: string
|
||||
schema: {
|
||||
properties: Record<string, any>
|
||||
properties: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user