mirror of
https://github.com/langgenius/dify.git
synced 2026-05-30 21:57:46 +08:00
Compare commits
2 Commits
codex/fix-
...
fix/worksp
| Author | SHA1 | Date | |
|---|---|---|---|
| 58da51c1ba | |||
| 9d093f71ed |
@ -29,7 +29,7 @@ from controllers.console.wraps import (
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import TimestampField, to_timestamp
|
||||
from libs.helper import TimestampField, dump_response, to_timestamp
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models.account import Tenant, TenantCustomConfigDict, TenantStatus
|
||||
from services.account_service import TenantService
|
||||
@ -56,6 +56,11 @@ class WorkspaceCustomConfigPayload(BaseModel):
|
||||
replace_webapp_logo: str | None = None
|
||||
|
||||
|
||||
class WorkspaceCustomConfigResponse(ResponseModel):
|
||||
remove_webapp_brand: bool | None = None
|
||||
replace_webapp_logo: str | None = None
|
||||
|
||||
|
||||
class WorkspaceInfoPayload(BaseModel):
|
||||
name: str
|
||||
|
||||
@ -69,7 +74,7 @@ class TenantInfoResponse(ResponseModel):
|
||||
role: str | None = None
|
||||
in_trial: bool | None = None
|
||||
trial_end_reason: str | None = None
|
||||
custom_config: dict | None = None
|
||||
custom_config: WorkspaceCustomConfigResponse | None = None
|
||||
trial_credits: int | None = None
|
||||
trial_credits_used: int | None = None
|
||||
next_credit_reset_date: int | None = None
|
||||
@ -101,9 +106,13 @@ register_schema_models(
|
||||
SwitchWorkspacePayload,
|
||||
WorkspaceCustomConfigPayload,
|
||||
WorkspaceInfoPayload,
|
||||
TenantInfoResponse,
|
||||
)
|
||||
register_response_schema_models(console_ns, WorkspacePermissionResponse)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
TenantInfoResponse,
|
||||
WorkspaceCustomConfigResponse,
|
||||
WorkspacePermissionResponse,
|
||||
)
|
||||
|
||||
provider_fields = {
|
||||
"provider_name": fields.String,
|
||||
@ -238,13 +247,7 @@ class TenantApi(Resource):
|
||||
else:
|
||||
raise Unauthorized("workspace is archived")
|
||||
|
||||
return (
|
||||
TenantInfoResponse.model_validate(
|
||||
WorkspaceService.get_tenant_info(tenant),
|
||||
from_attributes=True,
|
||||
).model_dump(mode="json"),
|
||||
200,
|
||||
)
|
||||
return dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant)), 200
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/switch")
|
||||
|
||||
@ -15207,7 +15207,7 @@ Tag type
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| created_at | integer | | No |
|
||||
| custom_config | object | | No |
|
||||
| custom_config | [WorkspaceCustomConfigResponse](#workspacecustomconfigresponse) | | No |
|
||||
| id | string | | Yes |
|
||||
| in_trial | boolean | | No |
|
||||
| name | string | | No |
|
||||
@ -16330,6 +16330,13 @@ Workflow tool configuration
|
||||
| remove_webapp_brand | boolean | | No |
|
||||
| replace_webapp_logo | string | | No |
|
||||
|
||||
#### WorkspaceCustomConfigResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| remove_webapp_brand | boolean | | No |
|
||||
| replace_webapp_logo | string | | No |
|
||||
|
||||
#### WorkspaceInfoPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@ -435,6 +435,23 @@ class TestTenantInfoResponse:
|
||||
assert payload["plan"] == "team"
|
||||
assert payload["created_at"] == int(created_at.timestamp())
|
||||
|
||||
def test_tenant_info_response_has_typed_custom_config(self):
|
||||
payload = TenantInfoResponse.model_validate(
|
||||
{
|
||||
"id": "t1",
|
||||
"custom_config": {
|
||||
"remove_webapp_brand": True,
|
||||
"replace_webapp_logo": "logo-file-id",
|
||||
"ignored": "value",
|
||||
},
|
||||
}
|
||||
).model_dump(mode="json")
|
||||
|
||||
assert payload["custom_config"] == {
|
||||
"remove_webapp_brand": True,
|
||||
"replace_webapp_logo": "logo-file-id",
|
||||
}
|
||||
|
||||
|
||||
class TestSwitchWorkspaceApi:
|
||||
def test_switch_success(self, app: Flask):
|
||||
|
||||
@ -4904,11 +4904,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/device/page.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/education-apply/hooks.ts": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 5
|
||||
|
||||
@ -4,16 +4,8 @@ import { oc } from '@orpc/contract'
|
||||
|
||||
import { zPostInfoResponse } from './zod.gen'
|
||||
|
||||
/**
|
||||
* Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const post = oc
|
||||
.route({
|
||||
deprecated: true,
|
||||
description:
|
||||
'Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postInfo',
|
||||
|
||||
@ -6,9 +6,7 @@ export type ClientOptions = {
|
||||
|
||||
export type TenantInfoResponse = {
|
||||
created_at?: number | null
|
||||
custom_config?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
custom_config?: WorkspaceCustomConfigResponse
|
||||
id: string
|
||||
in_trial?: boolean | null
|
||||
name?: string | null
|
||||
@ -21,6 +19,11 @@ export type TenantInfoResponse = {
|
||||
trial_end_reason?: string | null
|
||||
}
|
||||
|
||||
export type WorkspaceCustomConfigResponse = {
|
||||
remove_webapp_brand?: boolean | null
|
||||
replace_webapp_logo?: string | null
|
||||
}
|
||||
|
||||
export type PostInfoData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
@ -2,12 +2,20 @@
|
||||
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* WorkspaceCustomConfigResponse
|
||||
*/
|
||||
export const zWorkspaceCustomConfigResponse = z.object({
|
||||
remove_webapp_brand: z.boolean().nullish(),
|
||||
replace_webapp_logo: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* TenantInfoResponse
|
||||
*/
|
||||
export const zTenantInfoResponse = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
custom_config: z.record(z.string(), z.unknown()).nullish(),
|
||||
custom_config: zWorkspaceCustomConfigResponse.optional(),
|
||||
id: z.string(),
|
||||
in_trial: z.boolean().nullish(),
|
||||
name: z.string().nullish(),
|
||||
|
||||
@ -3682,16 +3682,8 @@ export const triggers = {
|
||||
get: get58,
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const post63 = oc
|
||||
.route({
|
||||
deprecated: true,
|
||||
description:
|
||||
'Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postWorkspacesCurrent',
|
||||
|
||||
@ -6,9 +6,7 @@ export type ClientOptions = {
|
||||
|
||||
export type TenantInfoResponse = {
|
||||
created_at?: number | null
|
||||
custom_config?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
custom_config?: WorkspaceCustomConfigResponse
|
||||
id: string
|
||||
in_trial?: boolean | null
|
||||
name?: string | null
|
||||
@ -500,6 +498,11 @@ export type SwitchWorkspacePayload = {
|
||||
tenant_id: string
|
||||
}
|
||||
|
||||
export type WorkspaceCustomConfigResponse = {
|
||||
remove_webapp_brand?: boolean | null
|
||||
replace_webapp_logo?: string | null
|
||||
}
|
||||
|
||||
export type AccountWithRole = {
|
||||
avatar?: string | null
|
||||
created_at?: number | null
|
||||
|
||||
@ -2,24 +2,6 @@
|
||||
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* TenantInfoResponse
|
||||
*/
|
||||
export const zTenantInfoResponse = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
custom_config: z.record(z.string(), z.unknown()).nullish(),
|
||||
id: z.string(),
|
||||
in_trial: z.boolean().nullish(),
|
||||
name: z.string().nullish(),
|
||||
next_credit_reset_date: z.int().nullish(),
|
||||
plan: z.string().nullish(),
|
||||
role: z.string().nullish(),
|
||||
status: z.string().nullish(),
|
||||
trial_credits: z.int().nullish(),
|
||||
trial_credits_used: z.int().nullish(),
|
||||
trial_end_reason: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* SimpleResultResponse
|
||||
*/
|
||||
@ -455,6 +437,32 @@ export const zSwitchWorkspacePayload = z.object({
|
||||
tenant_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkspaceCustomConfigResponse
|
||||
*/
|
||||
export const zWorkspaceCustomConfigResponse = z.object({
|
||||
remove_webapp_brand: z.boolean().nullish(),
|
||||
replace_webapp_logo: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* TenantInfoResponse
|
||||
*/
|
||||
export const zTenantInfoResponse = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
custom_config: zWorkspaceCustomConfigResponse.optional(),
|
||||
id: z.string(),
|
||||
in_trial: z.boolean().nullish(),
|
||||
name: z.string().nullish(),
|
||||
next_credit_reset_date: z.int().nullish(),
|
||||
plan: z.string().nullish(),
|
||||
role: z.string().nullish(),
|
||||
status: z.string().nullish(),
|
||||
trial_credits: z.int().nullish(),
|
||||
trial_credits_used: z.int().nullish(),
|
||||
trial_end_reason: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AccountWithRole
|
||||
*/
|
||||
|
||||
@ -68,7 +68,7 @@ describe('auth refresh route', () => {
|
||||
const fetchHeaders = fetchMock.mock.calls[0]?.[1]?.headers as Headers
|
||||
expect(fetchHeaders.get('cookie')).toBe('refresh_token=old-refresh')
|
||||
expect(response.status).toBe(303)
|
||||
expect(response.headers.get('location')).toBe('/apps?category=workflow')
|
||||
expect(response.headers.get('location')).toBe('http://localhost:3000/apps?category=workflow')
|
||||
expect(getSetCookieHeaders(response.headers)).toEqual([
|
||||
'access_token=new-access; Path=/; HttpOnly',
|
||||
'refresh_token=new-refresh; Path=/; HttpOnly',
|
||||
@ -85,7 +85,7 @@ describe('auth refresh route', () => {
|
||||
))
|
||||
|
||||
expect(response.status).toBe(303)
|
||||
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2Fapps')
|
||||
expect(response.headers.get('location')).toBe('http://localhost:3000/signin?redirect_url=%2Fapps')
|
||||
})
|
||||
|
||||
it('should ignore cross-origin redirect targets', async () => {
|
||||
@ -99,19 +99,6 @@ describe('auth refresh route', () => {
|
||||
))
|
||||
|
||||
expect(response.status).toBe(303)
|
||||
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2Fapps')
|
||||
})
|
||||
|
||||
it('should not leak internal request origin when redirecting to signin', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 })))
|
||||
const { GET } = await import('../route')
|
||||
|
||||
const response = await GET(createRequest(
|
||||
'http://internal-service:3000/auth/refresh?redirect_url=%2F',
|
||||
'refresh_token=expired',
|
||||
))
|
||||
|
||||
expect(response.status).toBe(303)
|
||||
expect(response.headers.get('location')).toBe('/signin?redirect_url=%2F')
|
||||
expect(response.headers.get('location')).toBe('http://localhost:3000/signin?redirect_url=%2Fapps')
|
||||
})
|
||||
})
|
||||
|
||||
@ -18,10 +18,11 @@ const resolveAbsoluteUrlPrefix = (value: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const resolveServerConsoleApiUrl = (pathname: string) => {
|
||||
const resolveServerConsoleApiUrl = (pathname: string, requestUrl: URL) => {
|
||||
const requestPath = withoutLeadingSlash(pathname)
|
||||
const apiPrefix = SERVER_CONSOLE_API_PREFIX
|
||||
|| resolveAbsoluteUrlPrefix(API_PREFIX)
|
||||
|| new URL(API_PREFIX, requestUrl.origin).toString()
|
||||
|
||||
if (!apiPrefix)
|
||||
return null
|
||||
@ -64,10 +65,10 @@ const getSetCookieHeaders = (headers: Headers) => {
|
||||
return setCookie ? [setCookie] : []
|
||||
}
|
||||
|
||||
const createRedirectResponse = (pathname: string, setCookies: string[] = []) => {
|
||||
const createRedirectResponse = (request: Request, pathname: string, setCookies: string[] = []) => {
|
||||
const headers = new Headers({
|
||||
'Cache-Control': 'no-store',
|
||||
'Location': pathname,
|
||||
'Location': new URL(pathname, request.url).toString(),
|
||||
})
|
||||
|
||||
for (const cookie of setCookies)
|
||||
@ -79,16 +80,17 @@ const createRedirectResponse = (pathname: string, setCookies: string[] = []) =>
|
||||
})
|
||||
}
|
||||
|
||||
const createSigninRedirectResponse = (redirectPath: string) =>
|
||||
createRedirectResponse(`${basePath}/signin?redirect_url=${encodeURIComponent(redirectPath)}`)
|
||||
const createSigninRedirectResponse = (request: Request, redirectPath: string) =>
|
||||
createRedirectResponse(request, `${basePath}/signin?redirect_url=${encodeURIComponent(redirectPath)}`)
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const requestUrl = new URL(request.url)
|
||||
const redirectPath = resolveSafeRedirectPath(request)
|
||||
const refreshUrl = resolveServerConsoleApiUrl(REFRESH_TOKEN_PATH)
|
||||
const refreshUrl = resolveServerConsoleApiUrl(REFRESH_TOKEN_PATH, requestUrl)
|
||||
const cookie = request.headers.get('cookie')
|
||||
|
||||
if (!refreshUrl || !cookie)
|
||||
return createSigninRedirectResponse(redirectPath)
|
||||
return createSigninRedirectResponse(request, redirectPath)
|
||||
|
||||
try {
|
||||
const response = await fetch(refreshUrl, {
|
||||
@ -101,11 +103,11 @@ export async function GET(request: Request) {
|
||||
})
|
||||
|
||||
if (!response.ok)
|
||||
return createSigninRedirectResponse(redirectPath)
|
||||
return createSigninRedirectResponse(request, redirectPath)
|
||||
|
||||
return createRedirectResponse(redirectPath, getSetCookieHeaders(response.headers))
|
||||
return createRedirectResponse(request, redirectPath, getSetCookieHeaders(response.headers))
|
||||
}
|
||||
catch {
|
||||
return createSigninRedirectResponse(redirectPath)
|
||||
return createSigninRedirectResponse(request, redirectPath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,14 +131,6 @@ vi.mock('@/service/use-common', () => ({
|
||||
],
|
||||
},
|
||||
}),
|
||||
useCurrentWorkspace: () => ({
|
||||
data: {
|
||||
trial_credits: 1000,
|
||||
trial_credits_used: 100,
|
||||
next_credit_reset_date: undefined,
|
||||
},
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
|
||||
@ -7,11 +7,11 @@ import QuotaPanel from '../quota-panel'
|
||||
let mockWorkspaceData: {
|
||||
trial_credits: number
|
||||
trial_credits_used: number
|
||||
next_credit_reset_date: string
|
||||
next_credit_reset_date: number
|
||||
} | undefined = {
|
||||
trial_credits: 100,
|
||||
trial_credits_used: 30,
|
||||
next_credit_reset_date: '2024-12-31',
|
||||
next_credit_reset_date: 1735603200,
|
||||
}
|
||||
let mockWorkspaceIsPending = false
|
||||
let mockTrialModels: string[] | undefined = ['langgenius/openai/openai']
|
||||
@ -32,11 +32,18 @@ vi.mock('@/app/components/base/icons/src/public/llm', () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useCurrentWorkspace: () => ({
|
||||
data: mockWorkspaceData,
|
||||
isPending: mockWorkspaceIsPending,
|
||||
}),
|
||||
vi.mock('../use-trial-credits', () => ({
|
||||
useTrialCredits: () => {
|
||||
const totalCredits = mockWorkspaceData?.trial_credits ?? 0
|
||||
const credits = Math.max(totalCredits - (mockWorkspaceData?.trial_credits_used ?? 0), 0)
|
||||
return {
|
||||
credits,
|
||||
totalCredits,
|
||||
isExhausted: credits <= 0,
|
||||
isLoading: mockWorkspaceIsPending && !mockWorkspaceData,
|
||||
nextCreditResetDate: mockWorkspaceData?.next_credit_reset_date,
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const renderQuotaPanel = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||
@ -78,7 +85,7 @@ describe('QuotaPanel', () => {
|
||||
mockWorkspaceData = {
|
||||
trial_credits: 100,
|
||||
trial_credits_used: 30,
|
||||
next_credit_reset_date: '2024-12-31',
|
||||
next_credit_reset_date: 1735603200,
|
||||
}
|
||||
mockWorkspaceIsPending = false
|
||||
mockTrialModels = ['langgenius/openai/openai']
|
||||
@ -118,7 +125,7 @@ describe('QuotaPanel', () => {
|
||||
mockWorkspaceData = {
|
||||
trial_credits: 10,
|
||||
trial_credits_used: 999,
|
||||
next_credit_reset_date: '',
|
||||
next_credit_reset_date: 0,
|
||||
}
|
||||
|
||||
renderQuotaPanel(<QuotaPanel providers={mockProviders} />)
|
||||
|
||||
@ -1,20 +1,34 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useTrialCredits } from '../use-trial-credits'
|
||||
|
||||
const mockUseCurrentWorkspace = vi.fn()
|
||||
const { mockUseQuery } = vi.hoisted(() => ({
|
||||
mockUseQuery: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useCurrentWorkspace: () => mockUseCurrentWorkspace(),
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: () => mockUseQuery(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
workspaces: {
|
||||
current: {
|
||||
post: {
|
||||
queryOptions: () => ({ queryKey: ['console', 'workspaces', 'current', 'post'] }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useTrialCredits', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
trial_credits: 100,
|
||||
trial_credits_used: 40,
|
||||
next_credit_reset_date: '2026-04-01',
|
||||
next_credit_reset_date: 1775001600,
|
||||
},
|
||||
isPending: false,
|
||||
})
|
||||
@ -29,16 +43,16 @@ describe('useTrialCredits', () => {
|
||||
totalCredits: 100,
|
||||
isExhausted: false,
|
||||
isLoading: false,
|
||||
nextCreditResetDate: '2026-04-01',
|
||||
nextCreditResetDate: 1775001600,
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep the hook out of loading state during a background refetch', () => {
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
trial_credits: 80,
|
||||
trial_credits_used: 20,
|
||||
next_credit_reset_date: '2026-05-01',
|
||||
next_credit_reset_date: 1777593600,
|
||||
},
|
||||
isPending: true,
|
||||
})
|
||||
@ -53,7 +67,7 @@ describe('useTrialCredits', () => {
|
||||
|
||||
describe('when workspace data is missing or exhausted', () => {
|
||||
it('should report loading while the first workspace request is pending', () => {
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: undefined,
|
||||
isPending: true,
|
||||
})
|
||||
@ -70,7 +84,7 @@ describe('useTrialCredits', () => {
|
||||
})
|
||||
|
||||
it('should clamp negative remaining credits to zero', () => {
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
mockUseQuery.mockReturnValue({
|
||||
data: {
|
||||
trial_credits: 10,
|
||||
trial_credits_used: 99,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import CreditsExhaustedAlert from './credits-exhausted-alert'
|
||||
|
||||
const baseWorkspace: ICurrentWorkspace = {
|
||||
@ -20,7 +21,7 @@ function createSeededQueryClient(overrides?: Partial<ICurrentWorkspace>) {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { refetchOnWindowFocus: false, retry: false } },
|
||||
})
|
||||
qc.setQueryData(['common', 'current-workspace'], { ...baseWorkspace, ...overrides })
|
||||
qc.setQueryData(consoleQuery.workspaces.current.post.queryKey(), { ...baseWorkspace, ...overrides })
|
||||
return qc
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useCurrentWorkspace } from '@/service/use-common'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
export const useTrialCredits = () => {
|
||||
const { data: currentWorkspace, isPending } = useCurrentWorkspace()
|
||||
const { data: currentWorkspace, isPending } = useQuery(consoleQuery.workspaces.current.post.queryOptions())
|
||||
const totalCredits = currentWorkspace?.trial_credits ?? 0
|
||||
const credits = Math.max(totalCredits - (currentWorkspace?.trial_credits_used ?? 0), 0)
|
||||
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { usePathname, useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { post } from '@/service/base'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { deviceLookup } from '@/service/device-flow'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { commonQueryKeys } from '@/service/use-common'
|
||||
import AuthorizeAccount from './components/authorize-account'
|
||||
import AuthorizeSSO from './components/authorize-sso'
|
||||
import Chooser from './components/chooser'
|
||||
@ -52,9 +50,8 @@ export default function DevicePage() {
|
||||
refetchOnMount: false,
|
||||
})
|
||||
const account = userResp?.profile
|
||||
const { data: currentWorkspace } = useQuery<ICurrentWorkspace>({
|
||||
queryKey: commonQueryKeys.currentWorkspace,
|
||||
queryFn: () => post<ICurrentWorkspace>('/workspaces/current'),
|
||||
const { data: currentWorkspace } = useQuery({
|
||||
...consoleQuery.workspaces.current.post.queryOptions(),
|
||||
enabled: !!account && !profileErr,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
@ -174,7 +171,7 @@ export default function DevicePage() {
|
||||
accountEmail={account?.email}
|
||||
accountName={account?.name}
|
||||
accountAvatarUrl={account?.avatar_url ?? null}
|
||||
defaultWorkspace={currentWorkspace?.name}
|
||||
defaultWorkspace={currentWorkspace?.name ?? undefined}
|
||||
onApproved={() => setView({ kind: 'success' })}
|
||||
onDenied={() => setView({ kind: 'error_expired' })}
|
||||
onError={e => setErrMsg(e)}
|
||||
@ -196,7 +193,7 @@ export default function DevicePage() {
|
||||
<h1 className="text-xl font-semibold text-text-primary">You're signed in</h1>
|
||||
<p className="text-sm text-text-secondary">Return to your terminal to continue.</p>
|
||||
<Divider className="my-3" />
|
||||
<Button variant="ghost" className="w-full" onClick={() => router.push('/')}>
|
||||
<Button variant="ghost" className="w-full" onClick={() => router.push('/apps')}>
|
||||
Go to Dify console →
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
} from '@/next/navigation'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { switchWorkspace } from '@/service/common'
|
||||
import { commonQueryKeys } from '@/service/use-common'
|
||||
import {
|
||||
@ -129,7 +129,7 @@ const EducationApplyAgeContent = () => {
|
||||
try {
|
||||
await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id: tenantId } })
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: commonQueryKeys.currentWorkspace }),
|
||||
queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.post.key() }),
|
||||
queryClient.invalidateQueries({ queryKey: commonQueryKeys.workspaces }),
|
||||
])
|
||||
onPlanInfoChanged()
|
||||
|
||||
@ -59,7 +59,7 @@ describe('InstallForm', () => {
|
||||
expect(mockSetup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should submit and redirect to the console root on successful login', async () => {
|
||||
it('should submit and redirect to apps on successful login', async () => {
|
||||
mockSetup.mockResolvedValue({ result: 'success' } as any)
|
||||
mockLogin.mockResolvedValue({ result: 'success', data: { access_token: 'token' } } as any)
|
||||
|
||||
@ -96,7 +96,7 @@ describe('InstallForm', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/')
|
||||
expect(mockReplace).toHaveBeenCalledWith('/apps')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -18,7 +17,6 @@ import { LICENSE_LINK } from '@/constants/link'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
||||
import { encryptPassword as encodePassword } from '@/utils/encryption'
|
||||
import Loading from '../components/base/loading'
|
||||
@ -40,7 +38,6 @@ const InstallForm = () => {
|
||||
useDocumentTitle('')
|
||||
const { t, i18n } = useTranslation()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const [showPassword, setShowPassword] = React.useState(false)
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
|
||||
@ -71,10 +68,9 @@ const InstallForm = () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Store tokens and redirect if login successful
|
||||
// Store tokens and redirect to apps if login successful
|
||||
if (loginRes.result === 'success') {
|
||||
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
|
||||
router.replace('/')
|
||||
router.replace('/apps')
|
||||
}
|
||||
else {
|
||||
// Fallback to signin page if auto-login fails
|
||||
|
||||
@ -3,7 +3,6 @@ import type { FormEvent } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
@ -12,7 +11,6 @@ import Countdown from '@/app/components/signin/countdown'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
||||
import { encryptVerificationCode } from '@/utils/encryption'
|
||||
import { getBrowserTimezone } from '@/utils/timezone'
|
||||
@ -21,7 +19,6 @@ import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
export default function CheckCode() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const searchParams = useSearchParams()
|
||||
const email = decodeURIComponent(searchParams.get('email') as string)
|
||||
const token = decodeURIComponent(searchParams.get('token') as string)
|
||||
@ -61,9 +58,8 @@ export default function CheckCode() {
|
||||
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||
}
|
||||
else {
|
||||
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/')
|
||||
router.replace(redirectUrl || '/apps')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { ResponseError } from '@/service/fetch'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -11,7 +10,6 @@ import { emailRegex } from '@/config'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { login } from '@/service/common'
|
||||
import { setWebAppAccessToken } from '@/service/webapp-auth'
|
||||
import { encryptPassword } from '@/utils/encryption'
|
||||
@ -27,7 +25,6 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const searchParams = useSearchParams()
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
|
||||
@ -78,9 +75,8 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||
}
|
||||
else {
|
||||
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/')
|
||||
router.replace(redirectUrl || '/apps')
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@ -12,9 +12,6 @@ vi.mock('@tanstack/react-query', async () => {
|
||||
const actual = await vi.importActual<typeof import('@tanstack/react-query')>('@tanstack/react-query')
|
||||
return {
|
||||
...actual,
|
||||
useQueryClient: vi.fn(() => ({
|
||||
resetQueries: vi.fn(),
|
||||
})),
|
||||
useSuspenseQuery: vi.fn(() => ({
|
||||
data: {
|
||||
branding: {
|
||||
|
||||
@ -4,7 +4,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiAccountCircleLine } from '@remixicon/react'
|
||||
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -16,7 +16,6 @@ import { i18n, setLocaleOnClient } from '@/i18n-config'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { activateMember } from '@/service/common'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useInvitationCheck } from '@/service/use-common'
|
||||
@ -56,7 +55,6 @@ export default function InviteSettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const searchParams = useSearchParams()
|
||||
const token = decodeURIComponent(searchParams.get('invite_token') as string)
|
||||
const locale = useLocale()
|
||||
@ -104,15 +102,14 @@ export default function InviteSettingsPage() {
|
||||
if (res.result === 'success') {
|
||||
// Tokens are now stored in cookies by the backend
|
||||
await setLocaleOnClient(language!, false)
|
||||
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/')
|
||||
router.replace(redirectUrl || '/apps')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
recheck()
|
||||
}
|
||||
}, [language, name, queryClient, recheck, searchParams, timezone, token, router, t])
|
||||
}, [language, name, recheck, timezone, token, router, t])
|
||||
|
||||
if (!checkRes)
|
||||
return <Loading />
|
||||
|
||||
@ -50,7 +50,7 @@ const NormalForm = () => {
|
||||
if (isLoggedIn) {
|
||||
setIsRedirecting(true)
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/')
|
||||
router.replace(redirectUrl || '/apps')
|
||||
return
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ const NormalForm = () => {
|
||||
setAllMethodsAreDisabled(true)
|
||||
}
|
||||
finally { setInitCheckLoading(false) }
|
||||
}, [isLoggedIn, message, router, searchParams, invite_token, isInviteLink, systemFeatures])
|
||||
}, [isLoggedIn, message, router, invite_token, isInviteLink, systemFeatures])
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [init])
|
||||
|
||||
@ -4,14 +4,12 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useReducer } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LICENSE_LINK } from '@/constants/link'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useOneMoreStep } from '@/service/use-common'
|
||||
import { timezones } from '@/utils/timezone'
|
||||
import Input from '../components/base/input'
|
||||
@ -68,7 +66,6 @@ const hasStatus = (error: unknown): error is { status: number } => {
|
||||
const OneMoreStep = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
@ -101,8 +98,7 @@ const OneMoreStep = () => {
|
||||
interface_language: state.interface_language,
|
||||
timezone: state.timezone,
|
||||
})
|
||||
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
|
||||
router.replace('/')
|
||||
router.push('/apps')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (hasStatus(error) && error.status === 400)
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { MockedFunction } from 'vitest'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
@ -47,20 +45,6 @@ const mockUseRouter = useRouter as unknown as MockedFunction<typeof useRouter>
|
||||
const mockUseMailRegister = useMailRegister as unknown as MockedFunction<typeof useMailRegister>
|
||||
const mockGetBrowserTimezone = getBrowserTimezone as unknown as MockedFunction<typeof getBrowserTimezone>
|
||||
|
||||
const renderWithQueryClient = (ui: ReactElement) => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ui}
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('Signup Set Password Page', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -77,7 +61,7 @@ describe('Signup Set Password Page', () => {
|
||||
|
||||
describe('Registration payload', () => {
|
||||
it('should submit locale and browser timezone when setting password', async () => {
|
||||
renderWithQueryClient(<ChangePasswordForm />)
|
||||
render(<ChangePasswordForm />)
|
||||
|
||||
fireEvent.change(screen.getByLabelText('common.account.newPassword'), {
|
||||
target: { value: 'ValidPass123!' },
|
||||
|
||||
@ -3,7 +3,6 @@ import type { MailRegisterResponse } from '@/service/use-common'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import Cookies from 'js-cookie'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -12,7 +11,6 @@ import Input from '@/app/components/base/input'
|
||||
import { validPassword } from '@/config'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useMailRegister } from '@/service/use-common'
|
||||
import { rememberCreateAppExternalAttribution } from '@/utils/create-app-tracking'
|
||||
import { sendGAEvent } from '@/utils/gtag'
|
||||
@ -34,7 +32,6 @@ const parseUtmInfo = () => {
|
||||
const ChangePasswordForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const searchParams = useSearchParams()
|
||||
const token = decodeURIComponent(searchParams.get('token') || '')
|
||||
const locale = useLocale()
|
||||
@ -90,14 +87,13 @@ const ChangePasswordForm = () => {
|
||||
Cookies.remove('utm_info') // Clean up: remove utm_info cookie
|
||||
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
await queryClient.resetQueries({ queryKey: consoleQuery.account.profile.get.key() })
|
||||
router.replace('/')
|
||||
router.replace('/apps')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, [password, token, valid, confirmPassword, register, locale, queryClient, router, t])
|
||||
}, [password, token, valid, confirmPassword, register, locale])
|
||||
|
||||
return (
|
||||
<div className={
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
|
||||
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
@ -17,9 +18,9 @@ import {
|
||||
} from '@/context/app-context'
|
||||
import { env } from '@/env'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import {
|
||||
useCurrentWorkspace,
|
||||
useLangGeniusVersion,
|
||||
} from '@/service/use-common'
|
||||
|
||||
@ -27,18 +28,52 @@ type AppContextProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const workspaceRoles = new Set<ICurrentWorkspace['role']>(['owner', 'admin', 'editor', 'dataset_operator', 'normal'])
|
||||
|
||||
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 const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
|
||||
const queryClient = useQueryClient()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions())
|
||||
const { data: currentWorkspaceResp, isPending: isLoadingCurrentWorkspace, isFetching: isValidatingCurrentWorkspace } = useCurrentWorkspace()
|
||||
const { data: currentWorkspaceResp, isPending: isLoadingCurrentWorkspace, isFetching: isValidatingCurrentWorkspace } = useQuery(consoleQuery.workspaces.current.post.queryOptions())
|
||||
const langGeniusVersionQuery = useLangGeniusVersion(
|
||||
userProfileResp?.meta.currentVersion,
|
||||
!systemFeatures.branding.enabled,
|
||||
)
|
||||
|
||||
const userProfile = useMemo<UserProfileResponse>(() => userProfileResp?.profile || userProfilePlaceholder, [userProfileResp?.profile])
|
||||
const currentWorkspace = useMemo<ICurrentWorkspace>(() => currentWorkspaceResp || initialWorkspaceInfo, [currentWorkspaceResp])
|
||||
const currentWorkspace = useMemo<ICurrentWorkspace>(() => normalizeCurrentWorkspace(currentWorkspaceResp), [currentWorkspaceResp])
|
||||
const langGeniusVersionInfo = useMemo<LangGeniusVersionResponse>(() => {
|
||||
if (!userProfileResp?.meta?.currentVersion || !langGeniusVersionQuery.data)
|
||||
return initialLangGeniusVersionInfo
|
||||
@ -64,7 +99,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
||||
}, [queryClient])
|
||||
|
||||
const mutateCurrentWorkspace = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['common', 'current-workspace'] })
|
||||
queryClient.invalidateQueries({ queryKey: consoleQuery.workspaces.current.post.key() })
|
||||
}, [queryClient])
|
||||
|
||||
// #region Zendesk conversation fields
|
||||
|
||||
@ -10,7 +10,6 @@ import type {
|
||||
CodeBasedExtension,
|
||||
CommonResponse,
|
||||
FileUploadConfigResponse,
|
||||
ICurrentWorkspace,
|
||||
IWorkspace,
|
||||
LangGeniusVersionResponse,
|
||||
Member,
|
||||
@ -26,7 +25,6 @@ const NAME_SPACE = 'common'
|
||||
|
||||
export const commonQueryKeys = {
|
||||
fileUploadConfig: [NAME_SPACE, 'file-upload-config'] as const,
|
||||
currentWorkspace: [NAME_SPACE, 'current-workspace'] as const,
|
||||
workspaces: [NAME_SPACE, 'workspaces'] as const,
|
||||
members: [NAME_SPACE, 'members'] as const,
|
||||
filePreview: (fileID: string) => [NAME_SPACE, 'file-preview', fileID] as const,
|
||||
@ -68,13 +66,6 @@ export const useLangGeniusVersion = (currentVersion?: string | null, enabled?: b
|
||||
})
|
||||
}
|
||||
|
||||
export const useCurrentWorkspace = () => {
|
||||
return useQuery<ICurrentWorkspace>({
|
||||
queryKey: commonQueryKeys.currentWorkspace,
|
||||
queryFn: () => post<ICurrentWorkspace>('/workspaces/current'),
|
||||
})
|
||||
}
|
||||
|
||||
export const useWorkspaces = () => {
|
||||
return useQuery<{ workspaces: IWorkspace[] }>({
|
||||
queryKey: commonQueryKeys.workspaces,
|
||||
|
||||
Reference in New Issue
Block a user