mirror of
https://mirror.skon.top/github.com/langgenius/dify.git
synced 2026-04-20 15:20:15 +08:00
refactor(web): unify app-shell bootstrap on TanStack Query + Next.js route conventions (#35394)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -59,7 +59,7 @@
|
|||||||
},
|
},
|
||||||
"web/__tests__/embedded-user-id-store.test.tsx": {
|
"web/__tests__/embedded-user-id-store.test.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 3
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/__tests__/goto-anything/command-selector.test.tsx": {
|
"web/__tests__/goto-anything/command-selector.test.tsx": {
|
||||||
@@ -6399,11 +6399,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/context/global-public-context.tsx": {
|
|
||||||
"react-refresh/only-export-components": {
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/context/hooks/use-trigger-events-limit-modal.ts": {
|
"web/context/hooks/use-trigger-events-limit-modal.ts": {
|
||||||
"react/set-state-in-effect": {
|
"react/set-state-in-effect": {
|
||||||
"count": 3
|
"count": 3
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import AppPublisher from '@/app/components/app/app-publisher'
|
import AppPublisher from '@/app/components/app/app-publisher'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
@@ -24,27 +24,15 @@ let mockAppDetail: {
|
|||||||
}
|
}
|
||||||
} | null = null
|
} | null = null
|
||||||
|
|
||||||
const createTestQueryClient = () =>
|
const renderWithQueryClient = (ui: React.ReactElement) =>
|
||||||
new QueryClient({
|
renderWithSystemFeatures(ui, {
|
||||||
defaultOptions: {
|
systemFeatures: {
|
||||||
queries: {
|
webapp_auth: {
|
||||||
retry: false,
|
enabled: true,
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
retry: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderWithQueryClient = (ui: React.ReactElement) => {
|
|
||||||
const queryClient = createTestQueryClient()
|
|
||||||
return render(
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
{ui}
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.mock('react-i18next', () => ({
|
vi.mock('react-i18next', () => ({
|
||||||
useTranslation: () => ({
|
useTranslation: () => ({
|
||||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||||
@@ -58,16 +46,6 @@ vi.mock('@/app/components/app/store', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
webapp_auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||||
useFormatTimeFromNow: () => ({
|
useFormatTimeFromNow: () => ({
|
||||||
formatTimeFromNow: (value: number) => `ago:${value}`,
|
formatTimeFromNow: (value: number) => `ago:${value}`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import AppPublisher from '@/app/components/app/app-publisher'
|
import AppPublisher from '@/app/components/app/app-publisher'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
@@ -28,27 +28,15 @@ let mockAppDetail: {
|
|||||||
}
|
}
|
||||||
} | null = null
|
} | null = null
|
||||||
|
|
||||||
const createTestQueryClient = () =>
|
const renderWithQueryClient = (ui: React.ReactElement) =>
|
||||||
new QueryClient({
|
renderWithSystemFeatures(ui, {
|
||||||
defaultOptions: {
|
systemFeatures: {
|
||||||
queries: {
|
webapp_auth: {
|
||||||
retry: false,
|
enabled: true,
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
retry: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderWithQueryClient = (ui: React.ReactElement) => {
|
|
||||||
const queryClient = createTestQueryClient()
|
|
||||||
return render(
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
{ui}
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.mock('react-i18next', () => ({
|
vi.mock('react-i18next', () => ({
|
||||||
useTranslation: () => ({
|
useTranslation: () => ({
|
||||||
t: (key: string) => key,
|
t: (key: string) => key,
|
||||||
@@ -66,16 +54,6 @@ vi.mock('@/app/components/app/store', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
webapp_auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||||
useFormatTimeFromNow: () => ({
|
useFormatTimeFromNow: () => ({
|
||||||
formatTimeFromNow: (value: number) => `ago:${value}`,
|
formatTimeFromNow: (value: number) => `ago:${value}`,
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
* - Access mode icons
|
* - Access mode icons
|
||||||
*/
|
*/
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import AppCard from '@/app/components/apps/app-card'
|
import AppCard from '@/app/components/apps/app-card'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { exportAppConfig, updateAppInfo } from '@/service/apps'
|
import { exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||||
@@ -96,15 +97,6 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector?: (state: Record<string, unknown>) => unknown) => {
|
|
||||||
const state = { systemFeatures: mockSystemFeatures }
|
|
||||||
if (typeof selector === 'function')
|
|
||||||
return selector(state)
|
|
||||||
return mockSystemFeatures
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => ({
|
useProviderContext: () => ({
|
||||||
onPlanInfoChanged: mockOnPlanInfoChanged,
|
onPlanInfoChanged: mockOnPlanInfoChanged,
|
||||||
@@ -255,7 +247,10 @@ const createMockApp = (overrides: Partial<App> = {}): App => ({
|
|||||||
const mockOnRefresh = vi.fn()
|
const mockOnRefresh = vi.fn()
|
||||||
|
|
||||||
const renderAppCard = (app?: Partial<App>) => {
|
const renderAppCard = (app?: Partial<App>) => {
|
||||||
return render(<AppCard app={createMockApp(app)} onRefresh={mockOnRefresh} />)
|
return renderWithSystemFeatures(
|
||||||
|
<AppCard app={createMockApp(app)} onRefresh={mockOnRefresh} />,
|
||||||
|
{ systemFeatures: mockSystemFeatures },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openOperationsMenu = () => {
|
const openOperationsMenu = () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
/**
|
/**
|
||||||
* Integration test: App List Browsing Flow
|
* Integration test: App List Browsing Flow
|
||||||
*
|
*
|
||||||
@@ -8,11 +9,12 @@
|
|||||||
*/
|
*/
|
||||||
import type { AppListResponse } from '@/models/app'
|
import type { AppListResponse } from '@/models/app'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { fireEvent, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
|
||||||
import List from '@/app/components/apps/list'
|
import List from '@/app/components/apps/list'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
|
|
||||||
let mockIsCurrentWorkspaceEditor = true
|
let mockIsCurrentWorkspaceEditor = true
|
||||||
@@ -64,13 +66,6 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector?: (state: Record<string, unknown>) => unknown) => {
|
|
||||||
const state = { systemFeatures: mockSystemFeatures }
|
|
||||||
return selector ? selector(state) : state
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => ({
|
useProviderContext: () => ({
|
||||||
onPlanInfoChanged: vi.fn(),
|
onPlanInfoChanged: vi.fn(),
|
||||||
@@ -168,11 +163,21 @@ const createPage = (apps: App[], hasMore = false, page = 1): AppListResponse =>
|
|||||||
total: apps.length,
|
total: apps.length,
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderList = (searchParams?: Record<string, string>) => {
|
const renderListUI = (ui: ReactElement, searchParams?: Record<string, string>) => {
|
||||||
return renderWithNuqs(
|
const { wrapper: SysWrapper } = createSystemFeaturesWrapper({
|
||||||
<List controlRefreshList={0} />,
|
systemFeatures: mockSystemFeatures,
|
||||||
{ searchParams },
|
})
|
||||||
|
const { wrapper: NuqsWrapper, onUrlUpdate } = createNuqsTestWrapper({ searchParams })
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<NuqsWrapper>
|
||||||
|
<SysWrapper>{children}</SysWrapper>
|
||||||
|
</NuqsWrapper>
|
||||||
)
|
)
|
||||||
|
return { ...render(ui, { wrapper: Wrapper }), onUrlUpdate }
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderList = (searchParams?: Record<string, string>) => {
|
||||||
|
return renderListUI(<List controlRefreshList={0} />, searchParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('App List Browsing Flow', () => {
|
describe('App List Browsing Flow', () => {
|
||||||
@@ -216,7 +221,7 @@ describe('App List Browsing Flow', () => {
|
|||||||
|
|
||||||
it('should transition from loading to content when data loads', () => {
|
it('should transition from loading to content when data loads', () => {
|
||||||
mockIsLoading = true
|
mockIsLoading = true
|
||||||
const { rerender } = renderWithNuqs(<List controlRefreshList={0} />)
|
const { rerender } = renderListUI(<List controlRefreshList={0} />)
|
||||||
|
|
||||||
const skeletonCards = document.querySelectorAll('.animate-pulse')
|
const skeletonCards = document.querySelectorAll('.animate-pulse')
|
||||||
expect(skeletonCards.length).toBeGreaterThan(0)
|
expect(skeletonCards.length).toBeGreaterThan(0)
|
||||||
@@ -423,7 +428,7 @@ describe('App List Browsing Flow', () => {
|
|||||||
it('should call refetch when controlRefreshList increments', () => {
|
it('should call refetch when controlRefreshList increments', () => {
|
||||||
mockPages = [createPage([createMockApp()])]
|
mockPages = [createPage([createMockApp()])]
|
||||||
|
|
||||||
const { rerender } = renderWithNuqs(<List controlRefreshList={0} />)
|
const { rerender } = renderListUI(<List controlRefreshList={0} />)
|
||||||
|
|
||||||
rerender(<List controlRefreshList={1} />)
|
rerender(<List controlRefreshList={1} />)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
/**
|
/**
|
||||||
* Integration test: Create App Flow
|
* Integration test: Create App Flow
|
||||||
*
|
*
|
||||||
@@ -9,11 +10,12 @@
|
|||||||
*/
|
*/
|
||||||
import type { AppListResponse } from '@/models/app'
|
import type { AppListResponse } from '@/models/app'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
|
||||||
import List from '@/app/components/apps/list'
|
import List from '@/app/components/apps/list'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
|
|
||||||
let mockIsCurrentWorkspaceEditor = true
|
let mockIsCurrentWorkspaceEditor = true
|
||||||
@@ -51,13 +53,6 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector?: (state: Record<string, unknown>) => unknown) => {
|
|
||||||
const state = { systemFeatures: mockSystemFeatures }
|
|
||||||
return selector ? selector(state) : state
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => ({
|
useProviderContext: () => ({
|
||||||
onPlanInfoChanged: mockOnPlanInfoChanged,
|
onPlanInfoChanged: mockOnPlanInfoChanged,
|
||||||
@@ -222,7 +217,16 @@ const createPage = (apps: App[]): AppListResponse => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const renderList = () => {
|
const renderList = () => {
|
||||||
return renderWithNuqs(<List controlRefreshList={0} />)
|
const { wrapper: SysWrapper } = createSystemFeaturesWrapper({
|
||||||
|
systemFeatures: mockSystemFeatures,
|
||||||
|
})
|
||||||
|
const { wrapper: NuqsWrapper, onUrlUpdate } = createNuqsTestWrapper()
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<NuqsWrapper>
|
||||||
|
<SysWrapper>{children}</SysWrapper>
|
||||||
|
</NuqsWrapper>
|
||||||
|
)
|
||||||
|
return { ...render(<List controlRefreshList={0} />, { wrapper: Wrapper }), onUrlUpdate }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Create App Flow', () => {
|
describe('Create App Flow', () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import type { ChatConfig } from '@/app/components/base/chat/types'
|
import type { ChatConfig } from '@/app/components/base/chat/types'
|
||||||
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
|
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
|
||||||
import { fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, renderHook, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import ChatWithHistory from '@/app/components/base/chat/chat-with-history'
|
import ChatWithHistory from '@/app/components/base/chat/chat-with-history'
|
||||||
import { useChatWithHistory } from '@/app/components/base/chat/chat-with-history/hooks'
|
import { useChatWithHistory } from '@/app/components/base/chat/chat-with-history/hooks'
|
||||||
import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context'
|
import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react'
|
import { screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import WebAppStoreProvider, { useWebAppStore } from '@/context/web-app-context'
|
import WebAppStoreProvider, { useWebAppStore } from '@/context/web-app-context'
|
||||||
|
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
@@ -19,44 +20,12 @@ vi.mock('@/service/use-share', () => ({
|
|||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Store the mock implementation in a way that survives hoisting
|
|
||||||
const mockGetProcessedSystemVariablesFromUrlParams = vi.fn()
|
const mockGetProcessedSystemVariablesFromUrlParams = vi.fn()
|
||||||
|
|
||||||
vi.mock('@/app/components/base/chat/utils', () => ({
|
vi.mock('@/app/components/base/chat/utils', () => ({
|
||||||
getProcessedSystemVariablesFromUrlParams: (...args: any[]) => mockGetProcessedSystemVariablesFromUrlParams(...args),
|
getProcessedSystemVariablesFromUrlParams: (...args: any[]) => mockGetProcessedSystemVariablesFromUrlParams(...args),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Use vi.hoisted to define mock state before vi.mock hoisting
|
|
||||||
const { mockGlobalStoreState } = vi.hoisted(() => ({
|
|
||||||
mockGlobalStoreState: {
|
|
||||||
isGlobalPending: false,
|
|
||||||
setIsGlobalPending: vi.fn(),
|
|
||||||
systemFeatures: {},
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => {
|
|
||||||
const useGlobalPublicStore = Object.assign(
|
|
||||||
(selector?: (state: typeof mockGlobalStoreState) => any) =>
|
|
||||||
selector ? selector(mockGlobalStoreState) : mockGlobalStoreState,
|
|
||||||
{
|
|
||||||
setState: (updater: any) => {
|
|
||||||
if (typeof updater === 'function')
|
|
||||||
Object.assign(mockGlobalStoreState, updater(mockGlobalStoreState) ?? {})
|
|
||||||
|
|
||||||
else
|
|
||||||
Object.assign(mockGlobalStoreState, updater)
|
|
||||||
},
|
|
||||||
__mockState: mockGlobalStoreState,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
useGlobalPublicStore,
|
|
||||||
useIsSystemFeaturesPending: () => false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const TestConsumer = () => {
|
const TestConsumer = () => {
|
||||||
const embeddedUserId = useWebAppStore(state => state.embeddedUserId)
|
const embeddedUserId = useWebAppStore(state => state.embeddedUserId)
|
||||||
const embeddedConversationId = useWebAppStore(state => state.embeddedConversationId)
|
const embeddedConversationId = useWebAppStore(state => state.embeddedConversationId)
|
||||||
@@ -91,7 +60,6 @@ const initialWebAppStore = (() => {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGlobalStoreState.isGlobalPending = false
|
|
||||||
mockGetProcessedSystemVariablesFromUrlParams.mockReset()
|
mockGetProcessedSystemVariablesFromUrlParams.mockReset()
|
||||||
useWebAppStore.setState(initialWebAppStore, true)
|
useWebAppStore.setState(initialWebAppStore, true)
|
||||||
})
|
})
|
||||||
@@ -103,7 +71,7 @@ describe('WebAppStoreProvider embedded user id handling', () => {
|
|||||||
conversation_id: 'conversation-456',
|
conversation_id: 'conversation-456',
|
||||||
})
|
})
|
||||||
|
|
||||||
render(
|
renderWithSystemFeatures(
|
||||||
<WebAppStoreProvider>
|
<WebAppStoreProvider>
|
||||||
<TestConsumer />
|
<TestConsumer />
|
||||||
</WebAppStoreProvider>,
|
</WebAppStoreProvider>,
|
||||||
@@ -125,7 +93,7 @@ describe('WebAppStoreProvider embedded user id handling', () => {
|
|||||||
}))
|
}))
|
||||||
mockGetProcessedSystemVariablesFromUrlParams.mockResolvedValue({})
|
mockGetProcessedSystemVariablesFromUrlParams.mockResolvedValue({})
|
||||||
|
|
||||||
render(
|
renderWithSystemFeatures(
|
||||||
<WebAppStoreProvider>
|
<WebAppStoreProvider>
|
||||||
<TestConsumer />
|
<TestConsumer />
|
||||||
</WebAppStoreProvider>,
|
</WebAppStoreProvider>,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
import type { Mock } from 'vitest'
|
import type { Mock } from 'vitest'
|
||||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||||
import type { App } from '@/models/explore'
|
import type { App } from '@/models/explore'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import AppList from '@/app/components/explore/app-list'
|
import AppList from '@/app/components/explore/app-list'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { fetchAppDetail } from '@/service/explore'
|
import { fetchAppDetail } from '@/service/explore'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import AccountDropdown from '@/app/components/header/account-dropdown'
|
import AccountDropdown from '@/app/components/header/account-dropdown'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
@@ -52,20 +52,6 @@ vi.mock('@/context/provider-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector?: (state: Record<string, unknown>) => unknown) => {
|
|
||||||
const state = {
|
|
||||||
systemFeatures: {
|
|
||||||
branding: {
|
|
||||||
enabled: false,
|
|
||||||
workspace_logo: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return selector ? selector(state) : state
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/modal-context', () => ({
|
vi.mock('@/context/modal-context', () => ({
|
||||||
useModalContext: () => ({
|
useModalContext: () => ({
|
||||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||||
@@ -108,18 +94,14 @@ vi.mock('@/next/link', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const renderAccountDropdown = () => {
|
const renderAccountDropdown = () => {
|
||||||
const queryClient = new QueryClient({
|
return renderWithSystemFeatures(<AccountDropdown />, {
|
||||||
defaultOptions: {
|
systemFeatures: {
|
||||||
queries: { retry: false },
|
branding: {
|
||||||
mutations: { retry: false },
|
enabled: false,
|
||||||
|
workspace_logo: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<AccountDropdown />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Header Account Dropdown Flow', () => {
|
describe('Header Account Dropdown Flow', () => {
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { pluginInstallLimit } from '@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit'
|
import { pluginInstallLimit } from '@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit'
|
||||||
import { InstallationScope } from '@/types/feature'
|
import { InstallationScope } from '@/types/feature'
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: () => ({
|
|
||||||
plugin_installation_permission: {
|
|
||||||
restrict_to_marketplace_only: false,
|
|
||||||
plugin_installation_scope: InstallationScope.ALL,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('Plugin Marketplace to Install Flow', () => {
|
describe('Plugin Marketplace to Install Flow', () => {
|
||||||
describe('install permission validation pipeline', () => {
|
describe('install permission validation pipeline', () => {
|
||||||
const systemFeaturesAll = {
|
const systemFeaturesAll = {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
import type { ReactNode } from 'react'
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
|
||||||
import PluginPage from '@/app/components/plugins/plugin-page'
|
import PluginPage from '@/app/components/plugins/plugin-page'
|
||||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||||
|
|
||||||
const mockFetchManifestFromMarketPlace = vi.fn()
|
const mockFetchManifestFromMarketPlace = vi.fn()
|
||||||
|
|
||||||
@@ -35,17 +37,6 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
enable_marketplace: true,
|
|
||||||
plugin_installation_permission: {
|
|
||||||
restrict_to_marketplace_only: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/service/use-plugins', () => ({
|
vi.mock('@/service/use-plugins', () => ({
|
||||||
useReferenceSettings: () => ({
|
useReferenceSettings: () => ({
|
||||||
data: {
|
data: {
|
||||||
@@ -104,13 +95,30 @@ vi.mock('@/app/components/plugins/install-plugin/install-from-marketplace', () =
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const renderPluginPage = (searchParams = '') => {
|
const renderPluginPage = (searchParams = '') => {
|
||||||
return renderWithNuqs(
|
const { wrapper: SysWrapper } = createSystemFeaturesWrapper({
|
||||||
<PluginPage
|
systemFeatures: {
|
||||||
plugins={<div data-testid="plugins-view">plugins view</div>}
|
enable_marketplace: true,
|
||||||
marketplace={<div data-testid="marketplace-view">marketplace view</div>}
|
plugin_installation_permission: {
|
||||||
/>,
|
restrict_to_marketplace_only: false,
|
||||||
{ searchParams },
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { wrapper: NuqsWrapper, onUrlUpdate } = createNuqsTestWrapper({ searchParams })
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<NuqsWrapper>
|
||||||
|
<SysWrapper>{children}</SysWrapper>
|
||||||
|
</NuqsWrapper>
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
...render(
|
||||||
|
<PluginPage
|
||||||
|
plugins={<div data-testid="plugins-view">plugins view</div>}
|
||||||
|
marketplace={<div data-testid="marketplace-view">marketplace view</div>}
|
||||||
|
/>,
|
||||||
|
{ wrapper: Wrapper },
|
||||||
|
),
|
||||||
|
onUrlUpdate,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Plugin Page Shell Flow', () => {
|
describe('Plugin Page Shell Flow', () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AccessMode } from '@/models/access-control'
|
import type { AccessMode } from '@/models/access-control'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import TextGeneration from '@/app/components/share/text-generation'
|
import TextGeneration from '@/app/components/share/text-generation'
|
||||||
|
|
||||||
const useSearchParamsMock = vi.fn(() => new URLSearchParams())
|
const useSearchParamsMock = vi.fn(() => new URLSearchParams())
|
||||||
@@ -117,7 +118,7 @@ vi.mock('@/service/share', async () => {
|
|||||||
const mockSystemFeatures = {
|
const mockSystemFeatures = {
|
||||||
branding: {
|
branding: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
workspace_logo: null,
|
workspace_logo: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,11 +171,6 @@ const mockWebAppState = {
|
|||||||
webAppAccessMode: 'public' as AccessMode,
|
webAppAccessMode: 'public' as AccessMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: { systemFeatures: typeof mockSystemFeatures }) => unknown) =>
|
|
||||||
selector({ systemFeatures: mockSystemFeatures }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/web-app-context', () => ({
|
vi.mock('@/context/web-app-context', () => ({
|
||||||
useWebAppStore: (selector: (state: typeof mockWebAppState) => unknown) => selector(mockWebAppState),
|
useWebAppStore: (selector: (state: typeof mockWebAppState) => unknown) => selector(mockWebAppState),
|
||||||
}))
|
}))
|
||||||
@@ -189,7 +185,7 @@ describe('TextGeneration', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should switch between create, batch, and saved tabs after app state loads', async () => {
|
it('should switch between create, batch, and saved tabs after app state loads', async () => {
|
||||||
render(<TextGeneration />)
|
renderWithSystemFeatures(<TextGeneration />, { systemFeatures: mockSystemFeatures })
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('run-once-mock')).toBeInTheDocument()
|
expect(screen.getByTestId('run-once-mock')).toBeInTheDocument()
|
||||||
@@ -212,7 +208,7 @@ describe('TextGeneration', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should wire single-run stop control and clear it when batch execution starts', async () => {
|
it('should wire single-run stop control and clear it when batch execution starts', async () => {
|
||||||
render(<TextGeneration />)
|
renderWithSystemFeatures(<TextGeneration />, { systemFeatures: mockSystemFeatures })
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('run-once-mock')).toBeInTheDocument()
|
expect(screen.getByTestId('run-once-mock')).toBeInTheDocument()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
import type { ReactNode } from 'react'
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
|
||||||
import ProviderList from '@/app/components/tools/provider-list'
|
import ProviderList from '@/app/components/tools/provider-list'
|
||||||
import { CollectionType } from '@/app/components/tools/types'
|
import { CollectionType } from '@/app/components/tools/types'
|
||||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||||
|
|
||||||
const mockInvalidateInstalledPluginList = vi.fn()
|
const mockInvalidateInstalledPluginList = vi.fn()
|
||||||
|
|
||||||
@@ -12,14 +14,6 @@ vi.mock('react-i18next', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
enable_marketplace: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/plugins/hooks', () => ({
|
vi.mock('@/app/components/plugins/hooks', () => ({
|
||||||
useTags: () => ({
|
useTags: () => ({
|
||||||
getTagLabel: (name: string) => name,
|
getTagLabel: (name: string) => name,
|
||||||
@@ -159,7 +153,16 @@ vi.mock('@/app/components/tools/mcp', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const renderProviderList = (searchParams = '') => {
|
const renderProviderList = (searchParams = '') => {
|
||||||
return renderWithNuqs(<ProviderList />, { searchParams })
|
const { wrapper: SysWrapper } = createSystemFeaturesWrapper({
|
||||||
|
systemFeatures: { enable_marketplace: true },
|
||||||
|
})
|
||||||
|
const { wrapper: NuqsWrapper, onUrlUpdate } = createNuqsTestWrapper({ searchParams })
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<NuqsWrapper>
|
||||||
|
<SysWrapper>{children}</SysWrapper>
|
||||||
|
</NuqsWrapper>
|
||||||
|
)
|
||||||
|
return { ...render(<ProviderList />, { wrapper: Wrapper }), onUrlUpdate }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Tool Provider List Shell Flow', () => {
|
describe('Tool Provider List Shell Flow', () => {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import type { Collection } from '@/app/components/tools/types'
|
|||||||
* Input (search), and card rendering. Verifies that tab switching, keyword
|
* Input (search), and card rendering. Verifies that tab switching, keyword
|
||||||
* filtering, and label filtering work together correctly.
|
* filtering, and label filtering work together correctly.
|
||||||
*/
|
*/
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
||||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
|
||||||
import { CollectionType } from '@/app/components/tools/types'
|
import { CollectionType } from '@/app/components/tools/types'
|
||||||
|
|
||||||
// ---- Mocks ----
|
// ---- Mocks ----
|
||||||
@@ -36,10 +36,6 @@ vi.mock('nuqs', async (importOriginal) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: () => ({ enable_marketplace: false }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/plugins/hooks', () => ({
|
vi.mock('@/app/components/plugins/hooks', () => ({
|
||||||
useTags: () => ({
|
useTags: () => ({
|
||||||
getTagLabel: (key: string) => key,
|
getTagLabel: (key: string) => key,
|
||||||
@@ -237,12 +233,10 @@ vi.mock('@/app/components/workflow/block-selector/types', () => ({
|
|||||||
const { default: ProviderList } = await import('@/app/components/tools/provider-list')
|
const { default: ProviderList } = await import('@/app/components/tools/provider-list')
|
||||||
|
|
||||||
const createWrapper = () => {
|
const createWrapper = () => {
|
||||||
const queryClient = new QueryClient({
|
const { wrapper } = createSystemFeaturesWrapper({
|
||||||
defaultOptions: { queries: { retry: false } },
|
systemFeatures: { enable_marketplace: false },
|
||||||
})
|
})
|
||||||
return ({ children }: { children: React.ReactNode }) => (
|
return wrapper
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Tool Browsing & Filtering Integration', () => {
|
describe('Tool Browsing & Filtering Integration', () => {
|
||||||
|
|||||||
127
web/__tests__/utils/mock-system-features.tsx
Normal file
127
web/__tests__/utils/mock-system-features.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import type { RenderHookOptions, RenderHookResult, RenderOptions, RenderResult } from '@testing-library/react'
|
||||||
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { render, renderHook } from '@testing-library/react'
|
||||||
|
import { consoleQuery } from '@/service/client'
|
||||||
|
import { defaultSystemFeatures } from '@/types/feature'
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Array<infer U>
|
||||||
|
? Array<U>
|
||||||
|
: T extends object
|
||||||
|
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: T
|
||||||
|
|
||||||
|
const buildSystemFeatures = (
|
||||||
|
overrides: DeepPartial<SystemFeatures> = {},
|
||||||
|
): SystemFeatures => {
|
||||||
|
const o = overrides as Partial<SystemFeatures>
|
||||||
|
return {
|
||||||
|
...defaultSystemFeatures,
|
||||||
|
...o,
|
||||||
|
branding: {
|
||||||
|
...defaultSystemFeatures.branding,
|
||||||
|
...(o.branding ?? {}),
|
||||||
|
},
|
||||||
|
webapp_auth: {
|
||||||
|
...defaultSystemFeatures.webapp_auth,
|
||||||
|
...(o.webapp_auth ?? {}),
|
||||||
|
sso_config: {
|
||||||
|
...defaultSystemFeatures.webapp_auth.sso_config,
|
||||||
|
...(o.webapp_auth?.sso_config ?? {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugin_installation_permission: {
|
||||||
|
...defaultSystemFeatures.plugin_installation_permission,
|
||||||
|
...(o.plugin_installation_permission ?? {}),
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
...defaultSystemFeatures.license,
|
||||||
|
...(o.license ?? {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a QueryClient suitable for tests. Any unseeded query stays in the
|
||||||
|
* "pending" state forever because the default queryFn never resolves; this
|
||||||
|
* mirrors the behaviour of an in-flight network request without touching the
|
||||||
|
* real fetch layer.
|
||||||
|
*/
|
||||||
|
export const createTestQueryClient = (): QueryClient =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
gcTime: 0,
|
||||||
|
staleTime: Infinity,
|
||||||
|
queryFn: () => new Promise(() => {}),
|
||||||
|
},
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const seedSystemFeatures = (
|
||||||
|
queryClient: QueryClient,
|
||||||
|
overrides: DeepPartial<SystemFeatures> = {},
|
||||||
|
): SystemFeatures => {
|
||||||
|
const data = buildSystemFeatures(overrides)
|
||||||
|
queryClient.setQueryData(consoleQuery.systemFeatures.queryKey(), data)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemFeaturesTestOptions = {
|
||||||
|
/**
|
||||||
|
* Partial overrides for the systemFeatures payload. When omitted, the cache
|
||||||
|
* is seeded with `defaultSystemFeatures` so consumers using
|
||||||
|
* `useSuspenseQuery` resolve immediately. Pass `null` to skip seeding and
|
||||||
|
* keep the systemFeatures query in the pending state.
|
||||||
|
*/
|
||||||
|
systemFeatures?: DeepPartial<SystemFeatures> | null
|
||||||
|
queryClient?: QueryClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemFeaturesWrapper = {
|
||||||
|
queryClient: QueryClient
|
||||||
|
systemFeatures: SystemFeatures | null
|
||||||
|
wrapper: (props: { children: ReactNode }) => ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSystemFeaturesWrapper = (
|
||||||
|
options: SystemFeaturesTestOptions = {},
|
||||||
|
): SystemFeaturesWrapper => {
|
||||||
|
const queryClient = options.queryClient ?? createTestQueryClient()
|
||||||
|
const systemFeatures = options.systemFeatures === null
|
||||||
|
? null
|
||||||
|
: seedSystemFeatures(queryClient, options.systemFeatures)
|
||||||
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, systemFeatures, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderWithSystemFeatures = (
|
||||||
|
ui: ReactElement,
|
||||||
|
options: SystemFeaturesTestOptions & Omit<RenderOptions, 'wrapper'> = {},
|
||||||
|
): RenderResult & { queryClient: QueryClient, systemFeatures: SystemFeatures | null } => {
|
||||||
|
const { systemFeatures: sf, queryClient: qc, ...renderOptions } = options
|
||||||
|
const { wrapper, queryClient, systemFeatures } = createSystemFeaturesWrapper({
|
||||||
|
systemFeatures: sf,
|
||||||
|
queryClient: qc,
|
||||||
|
})
|
||||||
|
const rendered = render(ui, { wrapper, ...renderOptions })
|
||||||
|
return { ...rendered, queryClient, systemFeatures }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderHookWithSystemFeatures = <Result, Props = void>(
|
||||||
|
callback: (props: Props) => Result,
|
||||||
|
options: SystemFeaturesTestOptions & Omit<RenderHookOptions<Props>, 'wrapper'> = {},
|
||||||
|
): RenderHookResult<Result, Props> & { queryClient: QueryClient, systemFeatures: SystemFeatures | null } => {
|
||||||
|
const { systemFeatures: sf, queryClient: qc, ...hookOptions } = options
|
||||||
|
const { wrapper, queryClient, systemFeatures } = createSystemFeaturesWrapper({
|
||||||
|
systemFeatures: sf,
|
||||||
|
queryClient: qc,
|
||||||
|
})
|
||||||
|
const rendered = renderHook(callback, { wrapper, ...hookOptions })
|
||||||
|
return { ...rendered, queryClient, systemFeatures }
|
||||||
|
}
|
||||||
33
web/app/(commonLayout)/error.tsx
Normal file
33
web/app/(commonLayout)/error.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import RootLoading from '@/app/loading'
|
||||||
|
import { isLegacyBase401 } from '@/service/use-common'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error: Error & { digest?: string }
|
||||||
|
unstable_retry: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CommonLayoutError({ error, unstable_retry }: Props) {
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// 401 already triggered jumpTo(/signin) inside service/base.ts. Render Loading
|
||||||
|
// until the browser navigation completes, matching main's Splash behavior.
|
||||||
|
// Showing the "Try again" button here would just flash for a few frames before
|
||||||
|
// the page navigates away, and clicking it would 401 again anyway.
|
||||||
|
if (isLegacyBase401(error))
|
||||||
|
return <RootLoading />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen flex-col items-center justify-center gap-4 bg-background-body">
|
||||||
|
<div className="system-sm-regular text-text-tertiary">
|
||||||
|
{t('errorBoundary.message')}
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="secondary" onClick={() => unstable_retry()}>
|
||||||
|
{t('errorBoundary.tryAgain')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
|||||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||||
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
||||||
import PartnerStack from '../components/billing/partner-stack'
|
import PartnerStack from '../components/billing/partner-stack'
|
||||||
import Splash from '../components/splash'
|
|
||||||
import RoleRouteGuard from './role-route-guard'
|
import RoleRouteGuard from './role-route-guard'
|
||||||
|
|
||||||
const Layout = ({ children }: { children: ReactNode }) => {
|
const Layout = ({ children }: { children: ReactNode }) => {
|
||||||
@@ -37,7 +36,6 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
<PartnerStack />
|
<PartnerStack />
|
||||||
<ReadmePanel />
|
<ReadmePanel />
|
||||||
<GotoAnything />
|
<GotoAnything />
|
||||||
<Splash />
|
|
||||||
</ModalContextProvider>
|
</ModalContextProvider>
|
||||||
</ProviderContextProvider>
|
</ProviderContextProvider>
|
||||||
</EventEmitterContextProvider>
|
</EventEmitterContextProvider>
|
||||||
|
|||||||
9
web/app/(commonLayout)/loading.tsx
Normal file
9
web/app/(commonLayout)/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
|
||||||
|
export default function CommonLayoutLoading() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen items-center justify-center bg-background-body">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import Header from '@/app/signin/_header'
|
import Header from '@/app/signin/_header'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
|
|
||||||
export default function SignInLayout({ children }: any) {
|
export default function SignInLayout({ children }: any) {
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||||
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
|
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { SSOProtocol } from '@/types/feature'
|
import { SSOProtocol } from '@/types/feature'
|
||||||
|
|
||||||
const ExternalMemberSSOAuth = () => {
|
const ExternalMemberSSOAuth = () => {
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
|
|
||||||
export default function SignInLayout({ children }: PropsWithChildren) {
|
export default function SignInLayout({ children }: PropsWithChildren) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
useDocumentTitle(t('webapp.login', { ns: 'login' }))
|
useDocumentTitle(t('webapp.login', { ns: 'login' }))
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
|
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { IS_CE_EDITION } from '@/config'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { LicenseStatus } from '@/types/feature'
|
import { LicenseStatus } from '@/types/feature'
|
||||||
import MailAndCodeAuth from './components/mail-and-code-auth'
|
import MailAndCodeAuth from './components/mail-and-code-auth'
|
||||||
import MailAndPasswordAuth from './components/mail-and-password-auth'
|
import MailAndPasswordAuth from './components/mail-and-password-auth'
|
||||||
@@ -17,7 +18,7 @@ const NormalForm = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
|
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
|
||||||
const [showORLine, setShowORLine] = useState(false)
|
const [showORLine, setShowORLine] = useState(false)
|
||||||
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
|
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useWebAppStore } from '@/context/web-app-context'
|
import { useWebAppStore } from '@/context/web-app-context'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { webAppLogout } from '@/service/webapp-auth'
|
import { webAppLogout } from '@/service/webapp-auth'
|
||||||
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
|
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
|
||||||
import NormalForm from './normalForm'
|
import NormalForm from './normalForm'
|
||||||
|
|
||||||
const WebSSOForm: FC = () => {
|
const WebSSOForm: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
|
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { toast } from '@langgenius/dify-ui/toast'
|
|||||||
import {
|
import {
|
||||||
RiGraduationCapFill,
|
RiGraduationCapFill,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
@@ -15,11 +15,11 @@ import Input from '@/app/components/base/input'
|
|||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||||
import { IS_CE_EDITION, validPassword } from '@/config'
|
import { IS_CE_EDITION, validPassword } from '@/config'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { updateUserProfile } from '@/service/common'
|
import { updateUserProfile } from '@/service/common'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useAppList } from '@/service/use-apps'
|
import { useAppList } from '@/service/use-apps'
|
||||||
import { commonQueryKeys, useUserProfile } from '@/service/use-common'
|
import { commonQueryKeys, userProfileQueryOptions } from '@/service/use-common'
|
||||||
import DeleteAccount from '../delete-account'
|
import DeleteAccount from '../delete-account'
|
||||||
|
|
||||||
import AvatarWithEdit from './AvatarWithEdit'
|
import AvatarWithEdit from './AvatarWithEdit'
|
||||||
@@ -34,12 +34,13 @@ const descriptionClassName = `
|
|||||||
|
|
||||||
export default function AccountPage() {
|
export default function AccountPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
|
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
|
||||||
const apps = appList?.data || []
|
const apps = appList?.data || []
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { data: userProfileResp } = useUserProfile()
|
// Cache is warmed by AppContextProvider's useSuspenseQuery; this hits cache synchronously.
|
||||||
const userProfile = userProfileResp?.profile
|
const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions())
|
||||||
|
const userProfile = userProfileResp.profile
|
||||||
const mutateUserProfile = () => queryClient.invalidateQueries({ queryKey: commonQueryKeys.userProfile })
|
const mutateUserProfile = () => queryClient.invalidateQueries({ queryKey: commonQueryKeys.userProfile })
|
||||||
const { isEducationAccount } = useProviderContext()
|
const { isEducationAccount } = useProviderContext()
|
||||||
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Avatar } from '@langgenius/dify-ui/avatar'
|
|||||||
import {
|
import {
|
||||||
RiGraduationCapFill,
|
RiGraduationCapFill,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||||
@@ -11,13 +12,14 @@ import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
|||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useRouter } from '@/next/navigation'
|
import { useRouter } from '@/next/navigation'
|
||||||
import { useLogout, useUserProfile } from '@/service/use-common'
|
import { useLogout, userProfileQueryOptions } from '@/service/use-common'
|
||||||
|
|
||||||
export default function AppSelector() {
|
export default function AppSelector() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data: userProfileResp } = useUserProfile()
|
// Cache is warmed by AppContextProvider's useSuspenseQuery; this hits cache synchronously.
|
||||||
const userProfile = userProfileResp?.profile
|
const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions())
|
||||||
|
const userProfile = userProfileResp.profile
|
||||||
const { isEducationAccount } = useProviderContext()
|
const { isEducationAccount } = useProviderContext()
|
||||||
|
|
||||||
const { mutateAsync: logout } = useLogout()
|
const { mutateAsync: logout } = useLogout()
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
|
import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useRouter } from '@/next/navigation'
|
import { useRouter } from '@/next/navigation'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import Avatar from './avatar'
|
import Avatar from './avatar'
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
const goToStudio = useCallback(() => {
|
const goToStudio = useCallback(() => {
|
||||||
router.push('/apps')
|
router.push('/apps')
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import Header from '@/app/signin/_header'
|
import Header from '@/app/signin/_header'
|
||||||
import { AppContextProvider } from '@/context/app-context-provider'
|
import { AppContextProvider } from '@/context/app-context-provider'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { useIsLogin } from '@/service/use-common'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
|
import { isLegacyBase401, userProfileQueryOptions } from '@/service/use-common'
|
||||||
|
|
||||||
export default function SignInLayout({ children }: any) {
|
export default function SignInLayout({ children }: any) {
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
useDocumentTitle('')
|
useDocumentTitle('')
|
||||||
const { isLoading, data: loginData } = useIsLogin()
|
// Probe login state. 401 stays as `error` (not thrown) so this layout can render
|
||||||
const isLoggedIn = loginData?.logged_in
|
// the signin/oauth UI for unauthenticated users; other errors bubble to error.tsx.
|
||||||
|
// (When unauthenticated, service/base.ts's auto-redirect to /signin still fires.)
|
||||||
|
const { isPending, data: userResp, error } = useQuery({
|
||||||
|
...userProfileQueryOptions(),
|
||||||
|
throwOnError: err => !isLegacyBase401(err),
|
||||||
|
})
|
||||||
|
const isLoggedIn = !!userResp && !error
|
||||||
|
|
||||||
if (isLoading) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen w-full justify-center bg-background-default-burn">
|
<div className="flex min-h-screen w-full justify-center bg-background-default-burn">
|
||||||
<Loading />
|
<Loading />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
RiMailLine,
|
RiMailLine,
|
||||||
RiTranslate2,
|
RiTranslate2,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -17,7 +18,7 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
|
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
|
||||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||||
import { useIsLogin, useUserProfile } from '@/service/use-common'
|
import { isLegacyBase401, userProfileQueryOptions } from '@/service/use-common'
|
||||||
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
||||||
|
|
||||||
function buildReturnUrl(pathname: string, search: string) {
|
function buildReturnUrl(pathname: string, search: string) {
|
||||||
@@ -61,15 +62,20 @@ export default function OAuthAuthorize() {
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const client_id = decodeURIComponent(searchParams.get('client_id') || '')
|
const client_id = decodeURIComponent(searchParams.get('client_id') || '')
|
||||||
const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '')
|
const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '')
|
||||||
const { data: userProfileResp } = useUserProfile()
|
// Probe user profile. 401 stays as `error` (legitimate "not logged in" state),
|
||||||
|
// other errors throw to the nearest error.tsx; jumpTo same-pathname guard in
|
||||||
|
// service/base.ts prevents a redirect loop here.
|
||||||
|
const { data: userProfileResp, isPending: isProfileLoading, error: profileError } = useQuery({
|
||||||
|
...userProfileQueryOptions(),
|
||||||
|
throwOnError: err => !isLegacyBase401(err),
|
||||||
|
})
|
||||||
|
const isLoggedIn = !!userProfileResp && !profileError
|
||||||
const userProfile = userProfileResp?.profile
|
const userProfile = userProfileResp?.profile
|
||||||
const { data: authAppInfo, isLoading: isOAuthLoading, isError } = useOAuthAppInfo(client_id, redirect_uri)
|
const { data: authAppInfo, isLoading: isOAuthLoading, isError } = useOAuthAppInfo(client_id, redirect_uri)
|
||||||
const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
|
const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
|
||||||
const hasNotifiedRef = useRef(false)
|
const hasNotifiedRef = useRef(false)
|
||||||
|
|
||||||
const { isLoading: isIsLoginLoading, data: loginData } = useIsLogin()
|
const isLoading = isOAuthLoading || isProfileLoading
|
||||||
const isLoggedIn = loginData?.logged_in
|
|
||||||
const isLoading = isOAuthLoading || isIsLoginLoading
|
|
||||||
const onLoginSwitchClick = () => {
|
const onLoginSwitchClick = () => {
|
||||||
try {
|
try {
|
||||||
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
|
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import Header from '../signin/_header'
|
import Header from '../signin/_header'
|
||||||
import ActivateForm from './activateForm'
|
import ActivateForm from './activateForm'
|
||||||
|
|
||||||
const Activate = () => {
|
const Activate = () => {
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||||
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import type { MockedFunction } from 'vitest'
|
|
||||||
import { render, screen } from '@testing-library/react'
|
|
||||||
import { useUserProfile } from '@/service/use-common'
|
|
||||||
import Splash from '../splash'
|
|
||||||
|
|
||||||
vi.mock('@/service/use-common', () => ({
|
|
||||||
useUserProfile: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const mockUseUserProfile = useUserProfile as MockedFunction<typeof useUserProfile>
|
|
||||||
|
|
||||||
describe('Splash', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render the loading indicator while the profile query is pending', () => {
|
|
||||||
mockUseUserProfile.mockReturnValue({
|
|
||||||
isPending: true,
|
|
||||||
isError: false,
|
|
||||||
data: undefined,
|
|
||||||
} as ReturnType<typeof useUserProfile>)
|
|
||||||
|
|
||||||
render(<Splash />)
|
|
||||||
|
|
||||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not render the loading indicator when the profile query succeeds', () => {
|
|
||||||
mockUseUserProfile.mockReturnValue({
|
|
||||||
isPending: false,
|
|
||||||
isError: false,
|
|
||||||
data: {
|
|
||||||
profile: { id: 'user-1' },
|
|
||||||
meta: {
|
|
||||||
currentVersion: '1.13.3',
|
|
||||||
currentEnv: 'DEVELOPMENT',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as ReturnType<typeof useUserProfile>)
|
|
||||||
|
|
||||||
render(<Splash />)
|
|
||||||
|
|
||||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should stop rendering the loading indicator when the profile query errors', () => {
|
|
||||||
mockUseUserProfile.mockReturnValue({
|
|
||||||
isPending: false,
|
|
||||||
isError: true,
|
|
||||||
data: undefined,
|
|
||||||
error: new Error('profile request failed'),
|
|
||||||
} as ReturnType<typeof useUserProfile>)
|
|
||||||
|
|
||||||
render(<Splash />)
|
|
||||||
|
|
||||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||||
} from '@/app/education-apply/constants'
|
} from '@/app/education-apply/constants'
|
||||||
|
import RootLoading from '@/app/loading'
|
||||||
import { usePathname, useRouter, useSearchParams } from '@/next/navigation'
|
import { usePathname, useRouter, useSearchParams } from '@/next/navigation'
|
||||||
import { sendGAEvent } from '@/utils/gtag'
|
import { sendGAEvent } from '@/utils/gtag'
|
||||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||||
@@ -98,5 +99,5 @@ export const AppInitializer = ({
|
|||||||
})()
|
})()
|
||||||
}, [isSetupFinished, router, pathname, searchParams, oauthNewUser])
|
}, [isSetupFinished, router, pathname, searchParams, oauthNewUser])
|
||||||
|
|
||||||
return init ? children : null
|
return init ? children : <RootLoading />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control'
|
import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import useAccessControlStore from '@/context/access-control-store'
|
import useAccessControlStore from '@/context/access-control-store'
|
||||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||||
import AccessControlDialog from '../access-control-dialog'
|
import AccessControlDialog from '../access-control-dialog'
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import useAccessControlStore from '@/context/access-control-store'
|
import useAccessControlStore from '@/context/access-control-store'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import AccessControl from '../index'
|
import AccessControl from '../index'
|
||||||
|
|
||||||
|
let mockWebappAuth = {
|
||||||
|
enabled: true,
|
||||||
|
allow_sso: true,
|
||||||
|
allow_email_password_login: false,
|
||||||
|
allow_email_code_login: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: { webapp_auth: mockWebappAuth },
|
||||||
|
})
|
||||||
|
|
||||||
const mockMutateAsync = vi.fn()
|
const mockMutateAsync = vi.fn()
|
||||||
const mockUseUpdateAccessMode = vi.fn(() => ({
|
const mockUseUpdateAccessMode = vi.fn(() => ({
|
||||||
isPending: false,
|
isPending: false,
|
||||||
@@ -12,20 +25,6 @@ const mockUseUpdateAccessMode = vi.fn(() => ({
|
|||||||
}))
|
}))
|
||||||
const mockUseAppWhiteListSubjects = vi.fn()
|
const mockUseAppWhiteListSubjects = vi.fn()
|
||||||
const mockUseSearchForWhiteListCandidates = vi.fn()
|
const mockUseSearchForWhiteListCandidates = vi.fn()
|
||||||
let mockWebappAuth = {
|
|
||||||
enabled: true,
|
|
||||||
allow_sso: true,
|
|
||||||
allow_email_password_login: false,
|
|
||||||
allow_email_code_login: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: { systemFeatures: { webapp_auth: typeof mockWebappAuth } }) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
webapp_auth: mockWebappAuth,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/service/access-control', () => ({
|
vi.mock('@/service/access-control', () => ({
|
||||||
useAppWhiteListSubjects: (...args: unknown[]) => mockUseAppWhiteListSubjects(...args),
|
useAppWhiteListSubjects: (...args: unknown[]) => mockUseAppWhiteListSubjects(...args),
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { Description as DialogDescription, DialogTitle } from '@headlessui/react
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
|
import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||||
import { useUpdateAccessMode } from '@/service/access-control'
|
import { useUpdateAccessMode } from '@/service/access-control'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import useAccessControlStore from '../../../../context/access-control-store'
|
import useAccessControlStore from '../../../../context/access-control-store'
|
||||||
import AccessControlDialog from './access-control-dialog'
|
import AccessControlDialog from './access-control-dialog'
|
||||||
import AccessControlItem from './access-control-item'
|
import AccessControlItem from './access-control-item'
|
||||||
@@ -24,7 +25,7 @@ type AccessControlProps = {
|
|||||||
export default function AccessControl(props: AccessControlProps) {
|
export default function AccessControl(props: AccessControlProps) {
|
||||||
const { app, onClose, onConfirm } = props
|
const { app, onClose, onConfirm } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const setAppId = useAccessControlStore(s => s.setAppId)
|
const setAppId = useAccessControlStore(s => s.setAppId)
|
||||||
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
const specificGroups = useAccessControlStore(s => s.specificGroups)
|
||||||
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
const specificMembers = useAccessControlStore(s => s.specificMembers)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
/* eslint-disable ts/no-explicit-any */
|
/* eslint-disable ts/no-explicit-any */
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
import AppPublisher from '../index'
|
import AppPublisher from '../index'
|
||||||
|
|
||||||
|
const render = (ui: React.ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: { webapp_auth: { enabled: true } },
|
||||||
|
})
|
||||||
|
|
||||||
const mockOnPublish = vi.fn()
|
const mockOnPublish = vi.fn()
|
||||||
const mockOnToggle = vi.fn()
|
const mockOnToggle = vi.fn()
|
||||||
const mockSetAppDetail = vi.fn()
|
const mockSetAppDetail = vi.fn()
|
||||||
@@ -49,16 +54,6 @@ vi.mock('@/app/components/app/store', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: { systemFeatures: { webapp_auth: { enabled: boolean } } }) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
webapp_auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||||
useFormatTimeFromNow: () => ({
|
useFormatTimeFromNow: () => ({
|
||||||
formatTimeFromNow: () => 'moments ago',
|
formatTimeFromNow: () => 'moments ago',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { PublishWorkflowParams } from '@/types/workflow'
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useKeyPress } from 'ahooks'
|
import { useKeyPress } from 'ahooks'
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
@@ -21,13 +22,13 @@ import { trackEvent } from '@/app/components/base/amplitude'
|
|||||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
||||||
import { fetchAppDetailDirect } from '@/service/apps'
|
import { fetchAppDetailDirect } from '@/service/apps'
|
||||||
import { fetchInstalledAppList } from '@/service/explore'
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useInvalidateAppWorkflow } from '@/service/use-workflow'
|
import { useInvalidateAppWorkflow } from '@/service/use-workflow'
|
||||||
import { fetchPublishedWorkflow } from '@/service/workflow'
|
import { fetchPublishedWorkflow } from '@/service/workflow'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
@@ -103,7 +104,7 @@ const AppPublisher = ({
|
|||||||
const workflowStore = useContext(WorkflowContext)
|
const workflowStore = useContext(WorkflowContext)
|
||||||
const appDetail = useAppStore(state => state.appDetail)
|
const appDetail = useAppStore(state => state.appDetail)
|
||||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable ts/no-explicit-any */
|
/* eslint-disable ts/no-explicit-any */
|
||||||
import type { App } from '@/models/explore'
|
import type { App } from '@/models/explore'
|
||||||
import type { AppIconType } from '@/types/app'
|
import type { AppIconType } from '@/types/app'
|
||||||
import { render, screen, within } from '@testing-library/react'
|
import { screen, within } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
import AppListContext from '@/context/app-list-context'
|
import AppListContext from '@/context/app-list-context'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { PlusIcon } from '@heroicons/react/20/solid'
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { RiInformation2Line } from '@remixicon/react'
|
import { RiInformation2Line } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useContextSelector } from 'use-context-selector'
|
import { useContextSelector } from 'use-context-selector'
|
||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import AppListContext from '@/context/app-list-context'
|
import AppListContext from '@/context/app-list-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
|
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
|
||||||
|
|
||||||
type AppCardProps = {
|
type AppCardProps = {
|
||||||
@@ -26,7 +27,7 @@ const AppCard = ({
|
|||||||
}: AppCardProps) => {
|
}: AppCardProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { app: appBasicInfo } = app
|
const { app: appBasicInfo } = app
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
||||||
const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel)
|
const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel)
|
||||||
const handleShowTryAppPanel = useCallback(() => {
|
const handleShowTryAppPanel = useCallback(() => {
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
import type { AppDetailResponse } from '@/models/app'
|
import type { AppDetailResponse } from '@/models/app'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
import AppCard from '../app-card'
|
import AppCard from '../app-card'
|
||||||
|
|
||||||
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: { webapp_auth: { enabled: true } },
|
||||||
|
})
|
||||||
|
|
||||||
const mockFetchAppDetailDirect = vi.fn()
|
const mockFetchAppDetailDirect = vi.fn()
|
||||||
const mockPush = vi.fn()
|
const mockPush = vi.fn()
|
||||||
const mockSetAppDetail = vi.fn()
|
const mockSetAppDetail = vi.fn()
|
||||||
@@ -36,16 +41,6 @@ vi.mock('@/context/i18n', () => ({
|
|||||||
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: { systemFeatures: { webapp_auth: { enabled: boolean } } }) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
webapp_auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/app/store', () => ({
|
vi.mock('@/app/components/app/store', () => ({
|
||||||
useStore: (selector: (state: { appDetail: AppDetailResponse, setAppDetail: typeof mockSetAppDetail }) => unknown) => selector({
|
useStore: (selector: (state: { appDetail: AppDetailResponse, setAppDetail: typeof mockSetAppDetail }) => unknown) => selector({
|
||||||
appDetail: mockAppDetail as AppDetailResponse,
|
appDetail: mockAppDetail as AppDetailResponse,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ConfigParams } from './settings'
|
|||||||
import type { AppDetailResponse } from '@/models/app'
|
import type { AppDetailResponse } from '@/models/app'
|
||||||
import type { AppSSO } from '@/types/app'
|
import type { AppSSO } from '@/types/app'
|
||||||
import { Switch } from '@langgenius/dify-ui/switch'
|
import { Switch } from '@langgenius/dify-ui/switch'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -12,12 +13,12 @@ import Tooltip from '@/app/components/base/tooltip'
|
|||||||
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
|
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
|
||||||
import Indicator from '@/app/components/header/indicator'
|
import Indicator from '@/app/components/header/indicator'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { usePathname, useRouter } from '@/next/navigation'
|
import { usePathname, useRouter } from '@/next/navigation'
|
||||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||||
import { fetchAppDetailDirect } from '@/service/apps'
|
import { fetchAppDetailDirect } from '@/service/apps'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useAppWorkflow } from '@/service/use-workflow'
|
import { useAppWorkflow } from '@/service/use-workflow'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { asyncRunSafe } from '@/utils'
|
import { asyncRunSafe } from '@/utils'
|
||||||
@@ -73,7 +74,7 @@ function AppCard({
|
|||||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||||
const [showAccessControl, setShowAccessControl] = useState(false)
|
const [showAccessControl, setShowAccessControl] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { data: appAccessSubjects } = useAppWhiteListSubjects(
|
const { data: appAccessSubjects } = useAppWhiteListSubjects(
|
||||||
appDetail?.id,
|
appDetail?.id,
|
||||||
systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { Mock } from 'vitest'
|
import type { Mock } from 'vitest'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import * as appsService from '@/service/apps'
|
import * as appsService from '@/service/apps'
|
||||||
import * as exploreService from '@/service/explore'
|
import * as exploreService from '@/service/explore'
|
||||||
@@ -9,6 +10,15 @@ import * as workflowService from '@/service/workflow'
|
|||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import AppCard from '../app-card'
|
import AppCard from '../app-card'
|
||||||
|
|
||||||
|
let mockWebappAuthEnabled = false
|
||||||
|
|
||||||
|
const render = (ui: React.ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: {
|
||||||
|
webapp_auth: { enabled: mockWebappAuthEnabled },
|
||||||
|
branding: { enabled: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Mock next/navigation
|
// Mock next/navigation
|
||||||
const mockPush = vi.fn()
|
const mockPush = vi.fn()
|
||||||
vi.mock('@/next/navigation', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
@@ -65,16 +75,7 @@ vi.mock('@/context/provider-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock global public store - allow dynamic configuration
|
// systemFeatures is seeded into the QueryClient via the local render helper.
|
||||||
let mockWebappAuthEnabled = false
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (s: Record<string, unknown>) => unknown) => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
webapp_auth: { enabled: mockWebappAuthEnabled },
|
|
||||||
branding: { enabled: false },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/service/apps', () => ({
|
vi.mock('@/service/apps', () => ({
|
||||||
deleteApp: vi.fn(() => Promise.resolve()),
|
deleteApp: vi.fn(() => Promise.resolve()),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { act, fireEvent, screen } from '@testing-library/react'
|
import { act, fireEvent, screen } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
|
||||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
import { renderWithNuqs } from '@/test/nuqs-testing'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
@@ -22,14 +23,6 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: () => ({
|
|
||||||
systemFeatures: {
|
|
||||||
branding: { enabled: false },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const mockSetQuery = vi.fn()
|
const mockSetQuery = vi.fn()
|
||||||
const mockQueryState = {
|
const mockQueryState = {
|
||||||
tagIDs: [] as string[],
|
tagIDs: [] as string[],
|
||||||
@@ -192,9 +185,13 @@ beforeAll(() => {
|
|||||||
} as unknown as typeof IntersectionObserver
|
} as unknown as typeof IntersectionObserver
|
||||||
})
|
})
|
||||||
|
|
||||||
// Render helper wrapping with shared nuqs testing helper.
|
// Render helper wrapping with shared nuqs testing helper plus a seeded
|
||||||
|
// systemFeatures cache so List can resolve its useSuspenseQuery.
|
||||||
const renderList = (searchParams = '') => {
|
const renderList = (searchParams = '') => {
|
||||||
return renderWithNuqs(<List />, { searchParams })
|
const { wrapper: SystemFeaturesWrapper } = createSystemFeaturesWrapper({
|
||||||
|
systemFeatures: { branding: { enabled: false } },
|
||||||
|
})
|
||||||
|
return renderWithNuqs(<SystemFeaturesWrapper><List /></SystemFeaturesWrapper>, { searchParams })
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('List', () => {
|
describe('List', () => {
|
||||||
@@ -390,7 +387,7 @@ describe('List', () => {
|
|||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
it('should handle multiple renders without issues', () => {
|
it('should handle multiple renders without issues', () => {
|
||||||
const { unmount } = renderWithNuqs(<List />)
|
const { unmount } = renderList()
|
||||||
expect(screen.getByText('app.types.all'))!.toBeInTheDocument()
|
expect(screen.getByText('app.types.all'))!.toBeInTheDocument()
|
||||||
|
|
||||||
unmount()
|
unmount()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@langgenius/dify-ui/dropdown-menu'
|
} from '@langgenius/dify-ui/dropdown-menu'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useId, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useId, useMemo, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
@@ -35,7 +36,6 @@ import Tooltip from '@/app/components/base/tooltip'
|
|||||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
@@ -44,6 +44,7 @@ import { useRouter } from '@/next/navigation'
|
|||||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||||
import { copyApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
import { copyApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||||
import { fetchInstalledAppList } from '@/service/explore'
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useDeleteAppMutation } from '@/service/use-apps'
|
import { useDeleteAppMutation } from '@/service/use-apps'
|
||||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
@@ -182,7 +183,7 @@ const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
|||||||
type AppCardOperationsMenuContentProps = Omit<AppCardOperationsMenuProps, 'shouldShowOpenInExploreOption'>
|
type AppCardOperationsMenuContentProps = Omit<AppCardOperationsMenuProps, 'shouldShowOpenInExploreOption'>
|
||||||
|
|
||||||
const AppCardOperationsMenuContent: React.FC<AppCardOperationsMenuContentProps> = (props) => {
|
const AppCardOperationsMenuContent: React.FC<AppCardOperationsMenuContentProps> = (props) => {
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({
|
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({
|
||||||
appId: props.app.id,
|
appId: props.app.id,
|
||||||
enabled: systemFeatures.webapp_auth.enabled,
|
enabled: systemFeatures.webapp_auth.enabled,
|
||||||
@@ -205,7 +206,7 @@ const AppCardOperationsMenuContent: React.FC<AppCardOperationsMenuContentProps>
|
|||||||
const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const deleteAppNameInputId = useId()
|
const deleteAppNameInputId = useId()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||||
const { onPlanInfoChanged } = useProviderContext()
|
const { onPlanInfoChanged } = useProviderContext()
|
||||||
const { push } = useRouter()
|
const { push } = useRouter()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { WorkflowOnlineUser } from '@/models/app'
|
import type { WorkflowOnlineUser } from '@/models/app'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@@ -14,10 +15,10 @@ import TagFilter from '@/app/components/base/tag-management/filter'
|
|||||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { CheckModal } from '@/hooks/use-pay'
|
import { CheckModal } from '@/hooks/use-pay'
|
||||||
import dynamic from '@/next/dynamic'
|
import dynamic from '@/next/dynamic'
|
||||||
import { fetchWorkflowOnlineUsers } from '@/service/apps'
|
import { fetchWorkflowOnlineUsers } from '@/service/apps'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useInfiniteAppList } from '@/service/use-apps'
|
import { useInfiniteAppList } from '@/service/use-apps'
|
||||||
import { AppModeEnum, AppModes } from '@/types/app'
|
import { AppModeEnum, AppModes } from '@/types/app'
|
||||||
import AppCard from './app-card'
|
import AppCard from './app-card'
|
||||||
@@ -54,7 +55,7 @@ const List: FC<Props> = ({
|
|||||||
controlRefreshList = 0,
|
controlRefreshList = 0,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
|
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
|
||||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||||
const [activeTab, setActiveTab] = useQueryState(
|
const [activeTab, setActiveTab] = useQueryState(
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import type { i18n } from 'i18next'
|
|||||||
import type { ChatConfig } from '../../types'
|
import type { ChatConfig } from '../../types'
|
||||||
import type { ChatWithHistoryContextValue } from '../context'
|
import type { ChatWithHistoryContextValue } from '../context'
|
||||||
import type { AppData, AppMeta } from '@/models/share'
|
import type { AppData, AppMeta } from '@/models/share'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import * as ReactI18next from 'react-i18next'
|
import * as ReactI18next from 'react-i18next'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import { useChatWithHistoryContext } from '../context'
|
import { useChatWithHistoryContext } from '../context'
|
||||||
import HeaderInMobile from '../header-in-mobile'
|
import HeaderInMobile from '../header-in-mobile'
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { RefObject } from 'react'
|
|||||||
import type { ChatConfig } from '../../types'
|
import type { ChatConfig } from '../../types'
|
||||||
import type { InstalledApp } from '@/models/explore'
|
import type { InstalledApp } from '@/models/explore'
|
||||||
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
|
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { useChatWithHistory } from '../hooks'
|
import { useChatWithHistory } from '../hooks'
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { ChatWithHistoryContextValue } from '../../context'
|
import type { ChatWithHistoryContextValue } from '../../context'
|
||||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
import { screen, waitFor, within } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as ReactI18next from 'react-i18next'
|
import * as ReactI18next from 'react-i18next'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { useChatWithHistoryContext } from '../../context'
|
import { useChatWithHistoryContext } from '../../context'
|
||||||
import Sidebar from '../index'
|
import Sidebar from '../index'
|
||||||
import RenameModal from '../rename-modal'
|
import RenameModal from '../rename-modal'
|
||||||
|
|
||||||
// Type for mocking the global public store selector
|
let mockBranding: { enabled: boolean, workspace_logo: string } = { enabled: false, workspace_logo: '' }
|
||||||
type GlobalPublicStoreMock = {
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
systemFeatures: {
|
systemFeatures: { branding: { ...mockBranding } },
|
||||||
branding: {
|
})
|
||||||
enabled: boolean
|
|
||||||
workspace_logo: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSystemFeatures?: (features: unknown) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function mockUseTranslationWithEmptyKeys(emptyKeys: string[]) {
|
function mockUseTranslationWithEmptyKeys(emptyKeys: string[]) {
|
||||||
const originalUseTranslation = ReactI18next.useTranslation
|
const originalUseTranslation = ReactI18next.useTranslation
|
||||||
@@ -38,19 +33,6 @@ function mockUseTranslationWithEmptyKeys(emptyKeys: string[]) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to create properly-typed mock store state
|
|
||||||
function createMockStoreState(overrides: Partial<GlobalPublicStoreMock>): GlobalPublicStoreMock {
|
|
||||||
return {
|
|
||||||
systemFeatures: {
|
|
||||||
branding: {
|
|
||||||
enabled: false,
|
|
||||||
workspace_logo: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock List to allow us to trigger operations
|
// Mock List to allow us to trigger operations
|
||||||
vi.mock('../list', () => ({
|
vi.mock('../list', () => ({
|
||||||
default: ({ list, onOperate, title, isPin }: { list: Array<{ id: string, name: string }>, onOperate: (type: string, item: { id: string, name: string }) => void, title?: string, isPin?: boolean }) => (
|
default: ({ list, onOperate, title, isPin }: { list: Array<{ id: string, name: string }>, onOperate: (type: string, item: { id: string, name: string }) => void, title?: string, isPin?: boolean }) => (
|
||||||
@@ -74,18 +56,6 @@ vi.mock('../../context', () => ({
|
|||||||
useChatWithHistoryContext: vi.fn(),
|
useChatWithHistoryContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock global public store
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(selector => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
branding: {
|
|
||||||
enabled: false,
|
|
||||||
workspace_logo: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock next/navigation
|
// Mock next/navigation
|
||||||
vi.mock('@/next/navigation', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
useRouter: () => ({ push: vi.fn() }),
|
useRouter: () => ({ push: vi.fn() }),
|
||||||
@@ -139,8 +109,8 @@ describe('Sidebar Index', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockBranding = { enabled: false, workspace_logo: '' }
|
||||||
vi.mocked(useChatWithHistoryContext).mockReturnValue(mockContextValue)
|
vi.mocked(useChatWithHistoryContext).mockReturnValue(mockContextValue)
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector(createMockStoreState({}) as never))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Basic Rendering', () => {
|
describe('Basic Rendering', () => {
|
||||||
@@ -658,17 +628,7 @@ describe('Sidebar Index', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should use system branding logo when enabled', () => {
|
it('should use system branding logo when enabled', () => {
|
||||||
const mockStoreState = createMockStoreState({
|
mockBranding = { enabled: true, workspace_logo: 'http://example.com/workspace-logo.png' }
|
||||||
systemFeatures: {
|
|
||||||
branding: {
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: 'http://example.com/workspace-logo.png',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mocked(useGlobalPublicStore).mockClear()
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector(mockStoreState as never))
|
|
||||||
|
|
||||||
vi.mocked(useChatWithHistoryContext).mockReturnValue({
|
vi.mocked(useChatWithHistoryContext).mockReturnValue({
|
||||||
...mockContextValue,
|
...mockContextValue,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
RiExpandRightLine,
|
RiExpandRightLine,
|
||||||
RiLayoutLeft2Line,
|
RiLayoutLeft2Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useState,
|
useState,
|
||||||
@@ -26,7 +27,7 @@ import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
|
|||||||
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
|
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
|
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useChatWithHistoryContext } from '../context'
|
import { useChatWithHistoryContext } from '../context'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -55,7 +56,7 @@ const Sidebar = ({ isPanel, panelVisible }: Props) => {
|
|||||||
isResponding,
|
isResponding,
|
||||||
} = useChatWithHistoryContext()
|
} = useChatWithHistoryContext()
|
||||||
const isSidebarCollapsed = sidebarCollapseState
|
const isSidebarCollapsed = sidebarCollapseState
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
|
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
|
||||||
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
|
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import type { RefObject } from 'react'
|
import type { ReactElement, RefObject } from 'react'
|
||||||
import type { ChatConfig } from '../../types'
|
import type { ChatConfig } from '../../types'
|
||||||
import type { AppData, AppMeta, ConversationItem } from '@/models/share'
|
import type { AppData, AppMeta, ConversationItem } from '@/models/share'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
|
||||||
import { useEmbeddedChatbot } from '../hooks'
|
import { useEmbeddedChatbot } from '../hooks'
|
||||||
import EmbeddedChatbot from '../index'
|
import EmbeddedChatbot from '../index'
|
||||||
|
|
||||||
|
let mockBrandingWorkspaceLogo = ''
|
||||||
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: {
|
||||||
|
branding: { enabled: true, workspace_logo: mockBrandingWorkspaceLogo },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../hooks', () => ({
|
vi.mock('../hooks', () => ({
|
||||||
useEmbeddedChatbot: vi.fn(),
|
useEmbeddedChatbot: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -26,10 +32,6 @@ vi.mock('@/hooks/use-document-title', () => ({
|
|||||||
default: vi.fn(),
|
default: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('../chat-wrapper', () => ({
|
vi.mock('../chat-wrapper', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: () => <div>chat area</div>,
|
default: () => <div>chat area</div>,
|
||||||
@@ -125,19 +127,9 @@ const createHookReturn = (overrides: Partial<EmbeddedChatbotHookReturn> = {}): E
|
|||||||
describe('EmbeddedChatbot index', () => {
|
describe('EmbeddedChatbot index', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockBrandingWorkspaceLogo = ''
|
||||||
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)
|
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)
|
||||||
vi.mocked(useEmbeddedChatbot).mockReturnValue(createHookReturn())
|
vi.mocked(useEmbeddedChatbot).mockReturnValue(createHookReturn())
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
|
||||||
systemFeatures: {
|
|
||||||
...defaultSystemFeatures,
|
|
||||||
branding: {
|
|
||||||
...defaultSystemFeatures.branding,
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Loading and chat content', () => {
|
describe('Loading and chat content', () => {
|
||||||
@@ -159,17 +151,7 @@ describe('EmbeddedChatbot index', () => {
|
|||||||
|
|
||||||
describe('Powered by branding', () => {
|
describe('Powered by branding', () => {
|
||||||
it('should show workspace logo on mobile when branding is enabled', () => {
|
it('should show workspace logo on mobile when branding is enabled', () => {
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
mockBrandingWorkspaceLogo = 'https://example.com/workspace-logo.png'
|
||||||
systemFeatures: {
|
|
||||||
...defaultSystemFeatures,
|
|
||||||
branding: {
|
|
||||||
...defaultSystemFeatures.branding,
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: 'https://example.com/workspace-logo.png',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
render(<EmbeddedChatbot />)
|
render(<EmbeddedChatbot />)
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,25 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { EmbeddedChatbotContextValue } from '../../context'
|
import type { EmbeddedChatbotContextValue } from '../../context'
|
||||||
import type { AppData } from '@/models/share'
|
import type { AppData } from '@/models/share'
|
||||||
import type { SystemFeatures } from '@/types/feature'
|
import { act, screen, waitFor } from '@testing-library/react'
|
||||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { InstallationScope, LicenseStatus } from '@/types/feature'
|
|
||||||
import { useEmbeddedChatbotContext } from '../../context'
|
import { useEmbeddedChatbotContext } from '../../context'
|
||||||
import Header from '../index'
|
import Header from '../index'
|
||||||
|
|
||||||
|
let mockBranding = { enabled: true, workspace_logo: '' }
|
||||||
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: { branding: { ...mockBranding } },
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../../context', () => ({
|
vi.mock('../../context', () => ({
|
||||||
useEmbeddedChatbotContext: vi.fn(),
|
useEmbeddedChatbotContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown', () => ({
|
vi.mock('@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown', () => ({
|
||||||
default: () => <div data-testid="view-form-dropdown" />,
|
default: () => <div data-testid="view-form-dropdown" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
type GlobalPublicStoreMock = {
|
|
||||||
systemFeatures: SystemFeatures
|
|
||||||
setSystemFeatures: (systemFeatures: SystemFeatures) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('EmbeddedChatbot Header', () => {
|
describe('EmbeddedChatbot Header', () => {
|
||||||
const defaultAppData: AppData = {
|
const defaultAppData: AppData = {
|
||||||
app_id: 'test-app-id',
|
app_id: 'test-app-id',
|
||||||
@@ -47,48 +42,6 @@ describe('EmbeddedChatbot Header', () => {
|
|||||||
allInputsHidden: false,
|
allInputsHidden: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSystemFeatures: SystemFeatures = {
|
|
||||||
app_dsl_version: '',
|
|
||||||
trial_models: [],
|
|
||||||
plugin_installation_permission: {
|
|
||||||
plugin_installation_scope: InstallationScope.ALL,
|
|
||||||
restrict_to_marketplace_only: false,
|
|
||||||
},
|
|
||||||
sso_enforced_for_signin: false,
|
|
||||||
sso_enforced_for_signin_protocol: '',
|
|
||||||
sso_enforced_for_web: false,
|
|
||||||
sso_enforced_for_web_protocol: '',
|
|
||||||
enable_marketplace: false,
|
|
||||||
enable_change_email: false,
|
|
||||||
enable_email_code_login: false,
|
|
||||||
enable_email_password_login: false,
|
|
||||||
enable_social_oauth_login: false,
|
|
||||||
is_allow_create_workspace: false,
|
|
||||||
is_allow_register: false,
|
|
||||||
is_email_setup: false,
|
|
||||||
license: {
|
|
||||||
status: LicenseStatus.NONE,
|
|
||||||
expired_at: '',
|
|
||||||
},
|
|
||||||
branding: {
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: '',
|
|
||||||
login_page_logo: '',
|
|
||||||
favicon: '',
|
|
||||||
application_title: '',
|
|
||||||
},
|
|
||||||
webapp_auth: {
|
|
||||||
enabled: false,
|
|
||||||
allow_sso: false,
|
|
||||||
sso_config: { protocol: '' },
|
|
||||||
allow_email_code_login: false,
|
|
||||||
allow_email_password_login: false,
|
|
||||||
},
|
|
||||||
enable_collaboration_mode: false,
|
|
||||||
enable_trial_app: false,
|
|
||||||
enable_explore_banner: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupIframe = () => {
|
const setupIframe = () => {
|
||||||
const mockPostMessage = vi.fn()
|
const mockPostMessage = vi.fn()
|
||||||
const mockTop = { postMessage: mockPostMessage }
|
const mockTop = { postMessage: mockPostMessage }
|
||||||
@@ -100,11 +53,8 @@ describe('EmbeddedChatbot Header', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockBranding = { enabled: true, workspace_logo: '' }
|
||||||
vi.mocked(useEmbeddedChatbotContext).mockReturnValue(defaultContext as EmbeddedChatbotContextValue)
|
vi.mocked(useEmbeddedChatbotContext).mockReturnValue(defaultContext as EmbeddedChatbotContextValue)
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: (s: GlobalPublicStoreMock) => unknown) => selector({
|
|
||||||
systemFeatures: defaultSystemFeatures,
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
Object.defineProperty(window, 'self', { value: window, configurable: true })
|
Object.defineProperty(window, 'self', { value: window, configurable: true })
|
||||||
Object.defineProperty(window, 'top', { value: window, configurable: true })
|
Object.defineProperty(window, 'top', { value: window, configurable: true })
|
||||||
@@ -149,16 +99,7 @@ describe('EmbeddedChatbot Header', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render workspace logo when branding is enabled and logo exists', () => {
|
it('should render workspace logo when branding is enabled and logo exists', () => {
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: (s: GlobalPublicStoreMock) => unknown) => selector({
|
mockBranding = { enabled: true, workspace_logo: 'https://example.com/workspace.png' }
|
||||||
systemFeatures: {
|
|
||||||
...defaultSystemFeatures,
|
|
||||||
branding: {
|
|
||||||
...defaultSystemFeatures.branding,
|
|
||||||
workspace_logo: 'https://example.com/workspace.png',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
render(<Header title="Test Chatbot" />)
|
render(<Header title="Test Chatbot" />)
|
||||||
|
|
||||||
@@ -167,32 +108,13 @@ describe('EmbeddedChatbot Header', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render Dify logo by default when branding enabled is true but no logo provided', () => {
|
it('should render Dify logo by default when branding enabled is true but no logo provided', () => {
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: (s: GlobalPublicStoreMock) => unknown) => selector({
|
mockBranding = { enabled: true, workspace_logo: '' }
|
||||||
systemFeatures: {
|
|
||||||
...defaultSystemFeatures,
|
|
||||||
branding: {
|
|
||||||
...defaultSystemFeatures.branding,
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
render(<Header title="Test Chatbot" />)
|
render(<Header title="Test Chatbot" />)
|
||||||
expect(screen.getByAltText('Dify logo')).toBeInTheDocument()
|
expect(screen.getByAltText('Dify logo')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render Dify logo when branding is disabled', () => {
|
it('should render Dify logo when branding is disabled', () => {
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: (s: GlobalPublicStoreMock) => unknown) => selector({
|
mockBranding = { enabled: false, workspace_logo: '' }
|
||||||
systemFeatures: {
|
|
||||||
...defaultSystemFeatures,
|
|
||||||
branding: {
|
|
||||||
...defaultSystemFeatures.branding,
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
render(<Header title="Test Chatbot" />)
|
render(<Header title="Test Chatbot" />)
|
||||||
expect(screen.getByAltText('Dify logo')).toBeInTheDocument()
|
expect(screen.getByAltText('Dify logo')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { Theme } from '../theme/theme-context'
|
import type { Theme } from '../theme/theme-context'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -9,7 +10,7 @@ import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs
|
|||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { isClient } from '@/utils/client'
|
import { isClient } from '@/utils/client'
|
||||||
import {
|
import {
|
||||||
useEmbeddedChatbotContext,
|
useEmbeddedChatbotContext,
|
||||||
@@ -44,7 +45,7 @@ const Header: FC<IHeaderProps> = ({
|
|||||||
const [parentOrigin, setParentOrigin] = useState('')
|
const [parentOrigin, setParentOrigin] = useState('')
|
||||||
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
|
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
const handleMessageReceived = useCallback((event: MessageEvent) => {
|
const handleMessageReceived = useCallback((event: MessageEvent) => {
|
||||||
let currentParentOrigin = parentOrigin
|
let currentParentOrigin = parentOrigin
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { AppData } from '@/models/share'
|
import type { AppData } from '@/models/share'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
@@ -10,10 +11,10 @@ import Header from '@/app/components/base/chat/embedded-chatbot/header'
|
|||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
|
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { AppSourceType } from '@/service/share'
|
import { AppSourceType } from '@/service/share'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import {
|
import {
|
||||||
EmbeddedChatbotContext,
|
EmbeddedChatbotContext,
|
||||||
useEmbeddedChatbotContext,
|
useEmbeddedChatbotContext,
|
||||||
@@ -34,7 +35,7 @@ const Chatbot = () => {
|
|||||||
themeBuilder,
|
themeBuilder,
|
||||||
} = useEmbeddedChatbotContext()
|
} = useEmbeddedChatbotContext()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
const customConfig = appData?.custom_config
|
const customConfig = appData?.custom_config
|
||||||
const site = appData?.site
|
const site = appData?.site
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { AppContextValue } from '@/context/app-context'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import type { SystemFeatures } from '@/types/feature'
|
import { screen } from '@testing-library/react'
|
||||||
import { render, screen } from '@testing-library/react'
|
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { contactSalesUrl, defaultPlan } from '@/app/components/billing/config'
|
import { contactSalesUrl, defaultPlan } from '@/app/components/billing/config'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import {
|
import {
|
||||||
@@ -12,12 +13,19 @@ import {
|
|||||||
useAppContext,
|
useAppContext,
|
||||||
userProfilePlaceholder,
|
userProfilePlaceholder,
|
||||||
} from '@/context/app-context'
|
} from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
|
||||||
import CustomPage from '../index'
|
import CustomPage from '../index'
|
||||||
|
|
||||||
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: {
|
||||||
|
branding: {
|
||||||
|
enabled: true,
|
||||||
|
workspace_logo: 'https://example.com/workspace-logo.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const { mockToast } = vi.hoisted(() => {
|
const { mockToast } = vi.hoisted(() => {
|
||||||
const mockToast = Object.assign(vi.fn(), {
|
const mockToast = Object.assign(vi.fn(), {
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
@@ -44,9 +52,6 @@ vi.mock('@/context/app-context', async (importOriginal) => {
|
|||||||
useAppContext: vi.fn(),
|
useAppContext: vi.fn(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||||
toast: mockToast,
|
toast: mockToast,
|
||||||
}))
|
}))
|
||||||
@@ -54,7 +59,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
|||||||
const mockUseProviderContext = vi.mocked(useProviderContext)
|
const mockUseProviderContext = vi.mocked(useProviderContext)
|
||||||
const mockUseModalContext = vi.mocked(useModalContext)
|
const mockUseModalContext = vi.mocked(useModalContext)
|
||||||
const mockUseAppContext = vi.mocked(useAppContext)
|
const mockUseAppContext = vi.mocked(useAppContext)
|
||||||
const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
|
|
||||||
|
|
||||||
const createProviderContext = ({
|
const createProviderContext = ({
|
||||||
enableBilling = false,
|
enableBilling = false,
|
||||||
@@ -93,15 +97,6 @@ const createAppContextValue = (): AppContextValue => ({
|
|||||||
isValidatingCurrentWorkspace: false,
|
isValidatingCurrentWorkspace: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const createSystemFeatures = (): SystemFeatures => ({
|
|
||||||
...defaultSystemFeatures,
|
|
||||||
branding: {
|
|
||||||
...defaultSystemFeatures.branding,
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: 'https://example.com/workspace-logo.png',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('CustomPage', () => {
|
describe('CustomPage', () => {
|
||||||
const setShowPricingModal = vi.fn()
|
const setShowPricingModal = vi.fn()
|
||||||
|
|
||||||
@@ -113,10 +108,6 @@ describe('CustomPage', () => {
|
|||||||
setShowPricingModal,
|
setShowPricingModal,
|
||||||
} as unknown as ReturnType<typeof useModalContext>)
|
} as unknown as ReturnType<typeof useModalContext>)
|
||||||
mockUseAppContext.mockReturnValue(createAppContextValue())
|
mockUseAppContext.mockReturnValue(createAppContextValue())
|
||||||
mockUseGlobalPublicStore.mockImplementation(selector => selector({
|
|
||||||
systemFeatures: createSystemFeatures(),
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Integration coverage for the page and its child custom brand section.
|
// Integration coverage for the page and its child custom brand section.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { ChangeEvent } from 'react'
|
import type { ChangeEvent } from 'react'
|
||||||
import type { AppContextValue } from '@/context/app-context'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import type { SystemFeatures } from '@/types/feature'
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
import { act, renderHook } from '@testing-library/react'
|
import { act } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
|
import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||||
import { defaultPlan } from '@/app/components/billing/config'
|
import { defaultPlan } from '@/app/components/billing/config'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
@@ -13,12 +14,22 @@ import {
|
|||||||
useAppContext,
|
useAppContext,
|
||||||
userProfilePlaceholder,
|
userProfilePlaceholder,
|
||||||
} from '@/context/app-context'
|
} from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { updateCurrentWorkspace } from '@/service/common'
|
import { updateCurrentWorkspace } from '@/service/common'
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
|
||||||
import useWebAppBrand from '../use-web-app-brand'
|
import useWebAppBrand from '../use-web-app-brand'
|
||||||
|
|
||||||
|
let currentBrandingOverrides: Partial<SystemFeatures['branding']> = {}
|
||||||
|
const renderHook = <Result, Props = void>(callback: (props: Props) => Result) =>
|
||||||
|
renderHookWithSystemFeatures(callback, {
|
||||||
|
systemFeatures: {
|
||||||
|
branding: {
|
||||||
|
enabled: true,
|
||||||
|
workspace_logo: 'https://example.com/workspace-logo.png',
|
||||||
|
...currentBrandingOverrides,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const { mockNotify, mockToast } = vi.hoisted(() => {
|
const { mockNotify, mockToast } = vi.hoisted(() => {
|
||||||
const mockNotify = vi.fn()
|
const mockNotify = vi.fn()
|
||||||
const mockToast = Object.assign(mockNotify, {
|
const mockToast = Object.assign(mockNotify, {
|
||||||
@@ -49,9 +60,6 @@ vi.mock('@/context/app-context', async (importOriginal) => {
|
|||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: vi.fn(),
|
useProviderContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
vi.mock('@/app/components/base/image-uploader/utils', () => ({
|
vi.mock('@/app/components/base/image-uploader/utils', () => ({
|
||||||
imageUpload: vi.fn(),
|
imageUpload: vi.fn(),
|
||||||
getImageUploadErrorMessage: vi.fn(),
|
getImageUploadErrorMessage: vi.fn(),
|
||||||
@@ -60,7 +68,6 @@ vi.mock('@/app/components/base/image-uploader/utils', () => ({
|
|||||||
const mockUpdateCurrentWorkspace = vi.mocked(updateCurrentWorkspace)
|
const mockUpdateCurrentWorkspace = vi.mocked(updateCurrentWorkspace)
|
||||||
const mockUseAppContext = vi.mocked(useAppContext)
|
const mockUseAppContext = vi.mocked(useAppContext)
|
||||||
const mockUseProviderContext = vi.mocked(useProviderContext)
|
const mockUseProviderContext = vi.mocked(useProviderContext)
|
||||||
const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
|
|
||||||
const mockImageUpload = vi.mocked(imageUpload)
|
const mockImageUpload = vi.mocked(imageUpload)
|
||||||
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
|
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
|
||||||
|
|
||||||
@@ -80,16 +87,6 @@ const createProviderContext = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSystemFeatures = (brandingOverrides: Partial<SystemFeatures['branding']> = {}): SystemFeatures => ({
|
|
||||||
...defaultSystemFeatures,
|
|
||||||
branding: {
|
|
||||||
...defaultSystemFeatures.branding,
|
|
||||||
enabled: true,
|
|
||||||
workspace_logo: 'https://example.com/workspace-logo.png',
|
|
||||||
...brandingOverrides,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const createAppContextValue = (overrides: Partial<AppContextValue> = {}): AppContextValue => {
|
const createAppContextValue = (overrides: Partial<AppContextValue> = {}): AppContextValue => {
|
||||||
const { currentWorkspace: currentWorkspaceOverride, ...restOverrides } = overrides
|
const { currentWorkspace: currentWorkspaceOverride, ...restOverrides } = overrides
|
||||||
const workspaceOverrides: Partial<AppContextValue['currentWorkspace']> = currentWorkspaceOverride ?? {}
|
const workspaceOverrides: Partial<AppContextValue['currentWorkspace']> = currentWorkspaceOverride ?? {}
|
||||||
@@ -122,21 +119,16 @@ const createAppContextValue = (overrides: Partial<AppContextValue> = {}): AppCon
|
|||||||
|
|
||||||
describe('useWebAppBrand', () => {
|
describe('useWebAppBrand', () => {
|
||||||
let appContextValue: AppContextValue
|
let appContextValue: AppContextValue
|
||||||
let systemFeatures: SystemFeatures
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
appContextValue = createAppContextValue()
|
appContextValue = createAppContextValue()
|
||||||
systemFeatures = createSystemFeatures()
|
currentBrandingOverrides = {}
|
||||||
|
|
||||||
mockUpdateCurrentWorkspace.mockResolvedValue(appContextValue.currentWorkspace)
|
mockUpdateCurrentWorkspace.mockResolvedValue(appContextValue.currentWorkspace)
|
||||||
mockUseAppContext.mockImplementation(() => appContextValue)
|
mockUseAppContext.mockImplementation(() => appContextValue)
|
||||||
mockUseProviderContext.mockReturnValue(createProviderContext())
|
mockUseProviderContext.mockReturnValue(createProviderContext())
|
||||||
mockUseGlobalPublicStore.mockImplementation(selector => selector({
|
|
||||||
systemFeatures,
|
|
||||||
setSystemFeatures: vi.fn(),
|
|
||||||
}))
|
|
||||||
mockGetImageUploadErrorMessage.mockReturnValue('upload error')
|
mockGetImageUploadErrorMessage.mockReturnValue('upload error')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -174,10 +166,7 @@ describe('useWebAppBrand', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fall back to an empty workspace logo when branding is disabled', () => {
|
it('should fall back to an empty workspace logo when branding is disabled', () => {
|
||||||
systemFeatures = createSystemFeatures({
|
currentBrandingOverrides = { enabled: false, workspace_logo: '' }
|
||||||
enabled: false,
|
|
||||||
workspace_logo: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useWebAppBrand())
|
const { result } = renderHook(() => useWebAppBrand())
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { ChangeEvent } from 'react'
|
import type { ChangeEvent } from 'react'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { updateCurrentWorkspace } from '@/service/common'
|
import { updateCurrentWorkspace } from '@/service/common'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
|
|
||||||
const MAX_LOGO_FILE_SIZE = 5 * 1024 * 1024
|
const MAX_LOGO_FILE_SIZE = 5 * 1024 * 1024
|
||||||
const CUSTOM_CONFIG_URL = '/workspaces/custom-config'
|
const CUSTOM_CONFIG_URL = '/workspaces/custom-config'
|
||||||
@@ -19,7 +20,7 @@ const useWebAppBrand = () => {
|
|||||||
const [fileId, setFileId] = useState('')
|
const [fileId, setFileId] = useState('')
|
||||||
const [imgKey, setImgKey] = useState(() => Date.now())
|
const [imgKey, setImgKey] = useState(() => Date.now())
|
||||||
const [uploadProgress, setUploadProgress] = useState(0)
|
const [uploadProgress, setUploadProgress] = useState(0)
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const isSandbox = enableBilling && plan.type === Plan.sandbox
|
const isSandbox = enableBilling && plan.type === Plan.sandbox
|
||||||
const uploading = uploadProgress > 0 && uploadProgress < 100
|
const uploading = uploadProgress > 0 && uploadProgress < 100
|
||||||
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
|
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import type { ReactElement } from 'react'
|
||||||
|
import { screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
|
|
||||||
import BuiltInPipelineList from '../built-in-pipeline-list'
|
import BuiltInPipelineList from '../built-in-pipeline-list'
|
||||||
|
|
||||||
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: { enable_marketplace: true },
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../create-card', () => ({
|
vi.mock('../create-card', () => ({
|
||||||
default: () => <div data-testid="create-card">CreateCard</div>,
|
default: () => <div data-testid="create-card">CreateCard</div>,
|
||||||
}))
|
}))
|
||||||
@@ -22,13 +28,6 @@ vi.mock('@/context/i18n', () => ({
|
|||||||
useLocale: () => mockLocale,
|
useLocale: () => mockLocale,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn((selector) => {
|
|
||||||
const state = { systemFeatures: { enable_marketplace: true } }
|
|
||||||
return selector(state)
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const mockUsePipelineTemplateList = vi.fn()
|
const mockUsePipelineTemplateList = vi.fn()
|
||||||
vi.mock('@/service/use-pipeline', () => ({
|
vi.mock('@/service/use-pipeline', () => ({
|
||||||
usePipelineTemplateList: (...args: unknown[]) => mockUsePipelineTemplateList(...args),
|
usePipelineTemplateList: (...args: unknown[]) => mockUsePipelineTemplateList(...args),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { LanguagesSupported } from '@/i18n-config/language'
|
import { LanguagesSupported } from '@/i18n-config/language'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { usePipelineTemplateList } from '@/service/use-pipeline'
|
import { usePipelineTemplateList } from '@/service/use-pipeline'
|
||||||
import CreateCard from './create-card'
|
import CreateCard from './create-card'
|
||||||
import TemplateCard from './template-card'
|
import TemplateCard from './template-card'
|
||||||
@@ -13,7 +14,10 @@ const BuiltInPipelineList = () => {
|
|||||||
return locale
|
return locale
|
||||||
return LanguagesSupported[0]
|
return LanguagesSupported[0]
|
||||||
}, [locale])
|
}, [locale])
|
||||||
const enableMarketplace = useGlobalPublicStore(s => s.systemFeatures.enable_marketplace)
|
const { data: enableMarketplace } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: s => s.enable_marketplace,
|
||||||
|
})
|
||||||
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'built-in', language }, enableMarketplace)
|
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'built-in', language }, enableMarketplace)
|
||||||
const list = pipelineList?.pipeline_templates || []
|
const list = pipelineList?.pipeline_templates || []
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import type { ReactElement } from 'react'
|
||||||
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import List from '../index'
|
import List from '../index'
|
||||||
|
|
||||||
|
let mockBrandingEnabled = false
|
||||||
|
const render = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
|
systemFeatures: { branding: { enabled: mockBrandingEnabled } },
|
||||||
|
})
|
||||||
|
|
||||||
const mockPush = vi.fn()
|
const mockPush = vi.fn()
|
||||||
const mockReplace = vi.fn()
|
const mockReplace = vi.fn()
|
||||||
vi.mock('@/next/navigation', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
@@ -20,15 +27,6 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
useSelector: () => true,
|
useSelector: () => true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock global public context
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: () => ({
|
|
||||||
systemFeatures: {
|
|
||||||
branding: { enabled: false },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock external api panel context
|
// Mock external api panel context
|
||||||
const mockSetShowExternalApiPanel = vi.fn()
|
const mockSetShowExternalApiPanel = vi.fn()
|
||||||
vi.mock('@/context/external-api-panel-context', () => ({
|
vi.mock('@/context/external-api-panel-context', () => ({
|
||||||
@@ -133,6 +131,7 @@ vi.mock('@/app/components/datasets/create/website/base/checkbox-with-label', ()
|
|||||||
describe('List', () => {
|
describe('List', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockBrandingEnabled = false
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
@@ -319,18 +318,9 @@ describe('List', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not show DatasetFooter when branding is enabled', async () => {
|
it('should not show DatasetFooter when branding is enabled', async () => {
|
||||||
vi.doMock('@/context/global-public-context', () => ({
|
mockBrandingEnabled = true
|
||||||
useGlobalPublicStore: () => ({
|
|
||||||
systemFeatures: {
|
|
||||||
branding: { enabled: true },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.resetModules()
|
render(<List />)
|
||||||
const { default: ListComponent } = await import('../index')
|
|
||||||
|
|
||||||
render(<ListComponent />)
|
|
||||||
|
|
||||||
expect(screen.queryByTestId('dataset-footer')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('dataset-footer')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useBoolean, useDebounceFn } from 'ahooks'
|
import { useBoolean, useDebounceFn } from 'ahooks'
|
||||||
|
|
||||||
// Libraries
|
// Libraries
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import TagManagementModal from '@/app/components/base/tag-management'
|
import TagManagementModal from '@/app/components/base/tag-management'
|
||||||
@@ -14,9 +15,9 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
|
|||||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||||
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
|
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
|
||||||
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { useDatasetApiBaseUrl } from '@/service/knowledge/use-dataset'
|
import { useDatasetApiBaseUrl } from '@/service/knowledge/use-dataset'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
// Components
|
// Components
|
||||||
import ExternalAPIPanel from '../external-api/external-api-panel'
|
import ExternalAPIPanel from '../external-api/external-api-panel'
|
||||||
import ServiceApi from '../extra-info/service-api'
|
import ServiceApi from '../extra-info/service-api'
|
||||||
@@ -25,7 +26,7 @@ import Datasets from './datasets'
|
|||||||
|
|
||||||
const List = () => {
|
const List = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { isCurrentWorkspaceOwner } = useAppContext()
|
const { isCurrentWorkspaceOwner } = useAppContext()
|
||||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||||
const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
|
const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function ReactScanLoader() {
|
|||||||
<Script
|
<Script
|
||||||
src="//unpkg.com/react-scan/dist/auto.global.js"
|
src="//unpkg.com/react-scan/dist/auto.global.js"
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
strategy="afterInteractive"
|
strategy="beforeInteractive"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { AppCardProps } from '../index'
|
import type { AppCardProps } from '../index'
|
||||||
import type { App } from '@/models/explore'
|
import type { App } from '@/models/explore'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import AppCard from '../index'
|
import AppCard from '../index'
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { PlusIcon } from '@heroicons/react/20/solid'
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { RiInformation2Line } from '@remixicon/react'
|
import { RiInformation2Line } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { AppTypeIcon } from '../../app/type-selector'
|
import { AppTypeIcon } from '../../app/type-selector'
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ const AppCard = ({
|
|||||||
}: AppCardProps) => {
|
}: AppCardProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { app: appBasicInfo } = app
|
const { app: appBasicInfo } = app
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
||||||
const handleTryApp = () => {
|
const handleTryApp = () => {
|
||||||
trackEvent('preview_template', {
|
trackEvent('preview_template', {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
import type { Mock } from 'vitest'
|
import type { Mock } from 'vitest'
|
||||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||||
import type { App } from '@/models/explore'
|
import type { App } from '@/models/explore'
|
||||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { fetchAppDetail } from '@/service/explore'
|
import { fetchAppDetail } from '@/service/explore'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
import { renderWithNuqs } from '@/test/nuqs-testing'
|
||||||
@@ -134,12 +135,28 @@ const mockMemberRole = (hasEditPermission: boolean) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAppList = (hasEditPermission = false, onSuccess?: () => void, searchParams?: Record<string, string>) => {
|
type RenderOptions = {
|
||||||
|
enableExploreBanner?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderAppList = (
|
||||||
|
hasEditPermission = false,
|
||||||
|
onSuccess?: () => void,
|
||||||
|
searchParams?: Record<string, string>,
|
||||||
|
options: RenderOptions = {},
|
||||||
|
) => {
|
||||||
mockMemberRole(hasEditPermission)
|
mockMemberRole(hasEditPermission)
|
||||||
return renderWithNuqs(
|
const { wrapper: SystemFeaturesWrapper, queryClient } = createSystemFeaturesWrapper({
|
||||||
<AppList onSuccess={onSuccess} />,
|
systemFeatures: { enable_explore_banner: options.enableExploreBanner ?? false },
|
||||||
|
})
|
||||||
|
const Wrapped = ({ children }: { children: ReactNode }) => (
|
||||||
|
<SystemFeaturesWrapper>{children}</SystemFeaturesWrapper>
|
||||||
|
)
|
||||||
|
const rendered = renderWithNuqs(
|
||||||
|
<Wrapped><AppList onSuccess={onSuccess} /></Wrapped>,
|
||||||
{ searchParams },
|
{ searchParams },
|
||||||
)
|
)
|
||||||
|
return { ...rendered, queryClient }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('AppList', () => {
|
describe('AppList', () => {
|
||||||
@@ -435,18 +452,12 @@ describe('AppList', () => {
|
|||||||
|
|
||||||
describe('Banner', () => {
|
describe('Banner', () => {
|
||||||
it('should render banner when enable_explore_banner is true', () => {
|
it('should render banner when enable_explore_banner is true', () => {
|
||||||
useGlobalPublicStore.setState({
|
|
||||||
systemFeatures: {
|
|
||||||
...useGlobalPublicStore.getState().systemFeatures,
|
|
||||||
enable_explore_banner: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
mockExploreData = {
|
mockExploreData = {
|
||||||
categories: ['Writing'],
|
categories: ['Writing'],
|
||||||
allList: [createApp()],
|
allList: [createApp()],
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAppList()
|
renderAppList(false, undefined, undefined, { enableExploreBanner: true })
|
||||||
|
|
||||||
expect(screen.getByTestId('explore-banner')).toBeInTheDocument()
|
expect(screen.getByTestId('explore-banner')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { App } from '@/models/explore'
|
|||||||
import type { TryAppSelection } from '@/types/try-app'
|
import type { TryAppSelection } from '@/types/try-app'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
import { useQueryState } from 'nuqs'
|
import { useQueryState } from 'nuqs'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
@@ -18,12 +19,12 @@ import Banner from '@/app/components/explore/banner/banner'
|
|||||||
import Category from '@/app/components/explore/category'
|
import Category from '@/app/components/explore/category'
|
||||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||||
import {
|
import {
|
||||||
DSLImportMode,
|
DSLImportMode,
|
||||||
} from '@/models/app'
|
} from '@/models/app'
|
||||||
import { fetchAppDetail } from '@/service/explore'
|
import { fetchAppDetail } from '@/service/explore'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import { useExploreAppList } from '@/service/use-explore'
|
import { useExploreAppList } from '@/service/use-explore'
|
||||||
import { trackCreateApp } from '@/utils/create-app-tracking'
|
import { trackCreateApp } from '@/utils/create-app-tracking'
|
||||||
@@ -39,7 +40,7 @@ const Apps = ({
|
|||||||
}: AppsProps) => {
|
}: AppsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userProfile } = useAppContext()
|
const { userProfile } = useAppContext()
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { data: membersData } = useMembers()
|
const { data: membersData } = useMembers()
|
||||||
const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' })
|
const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' })
|
||||||
const userAccount = membersData?.accounts?.find(account => account.id === userProfile.id)
|
const userAccount = membersData?.accounts?.find(account => account.id === userProfile.id)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TryAppInfo } from '@/service/try-app'
|
import type { TryAppInfo } from '@/service/try-app'
|
||||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import TryApp from '../index'
|
import TryApp from '../index'
|
||||||
import { TypeEnum } from '../tab'
|
import { TypeEnum } from '../tab'
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { App as AppType } from '@/models/explore'
|
import type { App as AppType } from '@/models/explore'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import Modal from '@/app/components/base/modal/index'
|
import Modal from '@/app/components/base/modal/index'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useGetTryAppInfo } from '@/service/use-try-app'
|
import { useGetTryAppInfo } from '@/service/use-try-app'
|
||||||
import App from './app'
|
import App from './app'
|
||||||
import AppInfo from './app-info'
|
import AppInfo from './app-info'
|
||||||
@@ -31,7 +32,7 @@ const TryApp: FC<Props> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onCreate,
|
onCreate,
|
||||||
}) => {
|
}) => {
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const isTrialApp = !!(app && app.can_trial && systemFeatures.enable_trial_app)
|
const isTrialApp = !!(app && app.can_trial && systemFeatures.enable_trial_app)
|
||||||
const canUseTryTab = IS_CLOUD_EDITION && (app ? isTrialApp : true)
|
const canUseTryTab = IS_CLOUD_EDITION && (app ? isTrialApp : true)
|
||||||
const [type, setType] = useState<TypeEnum>(() => (canUseTryTab ? TypeEnum.TRY : TypeEnum.DETAIL))
|
const [type, setType] = useState<TypeEnum>(() => (canUseTryTab ? TypeEnum.TRY : TypeEnum.DETAIL))
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import type { ReactElement } from 'react'
|
||||||
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import Header from '../index'
|
import Header from '../index'
|
||||||
|
|
||||||
function createMockComponent(testId: string) {
|
function createMockComponent(testId: string) {
|
||||||
@@ -93,21 +95,16 @@ vi.mock('@/context/modal-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => {
|
const renderHeader = (ui: ReactElement = <Header />) =>
|
||||||
type SystemFeatures = { branding: { enabled: boolean, application_title: string | null, workspace_logo: string | null } }
|
renderWithSystemFeatures(ui, {
|
||||||
return {
|
systemFeatures: {
|
||||||
useGlobalPublicStore: (selector: (s: { systemFeatures: SystemFeatures }) => SystemFeatures) =>
|
branding: {
|
||||||
selector({
|
enabled: mockBrandingEnabled,
|
||||||
systemFeatures: {
|
application_title: mockBrandingTitle ?? '',
|
||||||
branding: {
|
workspace_logo: mockBrandingLogo ?? '',
|
||||||
enabled: mockBrandingEnabled,
|
},
|
||||||
application_title: mockBrandingTitle,
|
},
|
||||||
workspace_logo: mockBrandingLogo,
|
})
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -123,7 +120,7 @@ describe('Header', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render header with main nav components', () => {
|
it('should render header with main nav components', () => {
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
|
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('workplace-selector')).toBeInTheDocument()
|
expect(screen.getByTestId('workplace-selector')).toBeInTheDocument()
|
||||||
@@ -133,7 +130,7 @@ describe('Header', () => {
|
|||||||
|
|
||||||
it('should show license nav when billing disabled, plan badge when enabled', () => {
|
it('should show license nav when billing disabled, plan badge when enabled', () => {
|
||||||
mockEnableBilling = false
|
mockEnableBilling = false
|
||||||
const { rerender } = render(<Header />)
|
const { rerender } = renderHeader()
|
||||||
expect(screen.getByTestId('license-nav')).toBeInTheDocument()
|
expect(screen.getByTestId('license-nav')).toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('plan-badge')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('plan-badge')).not.toBeInTheDocument()
|
||||||
|
|
||||||
@@ -145,7 +142,7 @@ describe('Header', () => {
|
|||||||
|
|
||||||
it('should hide explore nav when user is dataset operator', () => {
|
it('should hide explore nav when user is dataset operator', () => {
|
||||||
mockIsDatasetOperator = true
|
mockIsDatasetOperator = true
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.queryByTestId('explore-nav')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('explore-nav')).not.toBeInTheDocument()
|
||||||
expect(screen.getByTestId('dataset-nav')).toBeInTheDocument()
|
expect(screen.getByTestId('dataset-nav')).toBeInTheDocument()
|
||||||
@@ -154,7 +151,7 @@ describe('Header', () => {
|
|||||||
it('should call pricing modal for free plan, settings modal for paid plan', () => {
|
it('should call pricing modal for free plan, settings modal for paid plan', () => {
|
||||||
mockEnableBilling = true
|
mockEnableBilling = true
|
||||||
mockPlanType = 'sandbox'
|
mockPlanType = 'sandbox'
|
||||||
const { rerender } = render(<Header />)
|
const { rerender } = renderHeader()
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('plan-badge'))
|
fireEvent.click(screen.getByTestId('plan-badge'))
|
||||||
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
|
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
|
||||||
@@ -167,7 +164,7 @@ describe('Header', () => {
|
|||||||
|
|
||||||
it('should render mobile layout without env nav', () => {
|
it('should render mobile layout without env nav', () => {
|
||||||
mockMedia = 'mobile'
|
mockMedia = 'mobile'
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
|
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('env-nav')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('env-nav')).not.toBeInTheDocument()
|
||||||
@@ -178,7 +175,7 @@ describe('Header', () => {
|
|||||||
mockBrandingTitle = 'Acme Workspace'
|
mockBrandingTitle = 'Acme Workspace'
|
||||||
mockBrandingLogo = '/logo.png'
|
mockBrandingLogo = '/logo.png'
|
||||||
|
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.getByText('Acme Workspace')).toBeInTheDocument()
|
expect(screen.getByText('Acme Workspace')).toBeInTheDocument()
|
||||||
expect(screen.getByRole('img', { name: /logo/i })).toBeInTheDocument()
|
expect(screen.getByRole('img', { name: /logo/i })).toBeInTheDocument()
|
||||||
@@ -190,7 +187,7 @@ describe('Header', () => {
|
|||||||
mockBrandingTitle = 'Custom Title'
|
mockBrandingTitle = 'Custom Title'
|
||||||
mockBrandingLogo = null
|
mockBrandingLogo = null
|
||||||
|
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.getByText('Custom Title')).toBeInTheDocument()
|
expect(screen.getByText('Custom Title')).toBeInTheDocument()
|
||||||
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
|
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
|
||||||
@@ -201,7 +198,7 @@ describe('Header', () => {
|
|||||||
mockBrandingTitle = null
|
mockBrandingTitle = null
|
||||||
mockBrandingLogo = null
|
mockBrandingLogo = null
|
||||||
|
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.getByText('Dify')).toBeInTheDocument()
|
expect(screen.getByText('Dify')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@@ -210,7 +207,7 @@ describe('Header', () => {
|
|||||||
mockIsWorkspaceEditor = true
|
mockIsWorkspaceEditor = true
|
||||||
mockIsDatasetOperator = false
|
mockIsDatasetOperator = false
|
||||||
|
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.getByTestId('dataset-nav')).toBeInTheDocument()
|
expect(screen.getByTestId('dataset-nav')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('explore-nav')).toBeInTheDocument()
|
expect(screen.getByTestId('explore-nav')).toBeInTheDocument()
|
||||||
@@ -221,7 +218,7 @@ describe('Header', () => {
|
|||||||
mockIsWorkspaceEditor = false
|
mockIsWorkspaceEditor = false
|
||||||
mockIsDatasetOperator = false
|
mockIsDatasetOperator = false
|
||||||
|
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.queryByTestId('dataset-nav')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('dataset-nav')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@@ -230,7 +227,7 @@ describe('Header', () => {
|
|||||||
mockMedia = 'mobile'
|
mockMedia = 'mobile'
|
||||||
mockIsDatasetOperator = true
|
mockIsDatasetOperator = true
|
||||||
|
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.queryByTestId('explore-nav')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('explore-nav')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('app-nav')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('app-nav')).not.toBeInTheDocument()
|
||||||
@@ -243,7 +240,7 @@ describe('Header', () => {
|
|||||||
mockEnableBilling = true
|
mockEnableBilling = true
|
||||||
mockPlanType = 'sandbox'
|
mockPlanType = 'sandbox'
|
||||||
|
|
||||||
render(<Header />)
|
renderHeader()
|
||||||
|
|
||||||
expect(screen.getByTestId('plan-badge')).toBeInTheDocument()
|
expect(screen.getByTestId('plan-badge')).toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('license-nav')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('license-nav')).not.toBeInTheDocument()
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import type { LangGeniusVersionResponse } from '@/models/common'
|
import type { LangGeniusVersionResponse } from '@/models/common'
|
||||||
import type { SystemFeatures } from '@/types/feature'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import AccountAbout from '../index'
|
import AccountAbout from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
let mockIsCEEdition = false
|
let mockIsCEEdition = false
|
||||||
vi.mock('@/config', () => ({
|
vi.mock('@/config', async (importOriginal) => {
|
||||||
get IS_CE_EDITION() { return mockIsCEEdition },
|
const actual = await importOriginal<typeof import('@/config')>()
|
||||||
}))
|
return {
|
||||||
|
...actual,
|
||||||
type GlobalPublicStore = {
|
get IS_CE_EDITION() { return mockIsCEEdition },
|
||||||
systemFeatures: SystemFeatures
|
}
|
||||||
setSystemFeatures: (systemFeatures: SystemFeatures) => void
|
})
|
||||||
}
|
|
||||||
|
|
||||||
describe('AccountAbout', () => {
|
describe('AccountAbout', () => {
|
||||||
const mockVersionInfo: LangGeniusVersionResponse = {
|
const mockVersionInfo: LangGeniusVersionResponse = {
|
||||||
@@ -34,31 +28,23 @@ describe('AccountAbout', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockIsCEEdition = false
|
mockIsCEEdition = false
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
|
||||||
systemFeatures: { branding: { enabled: false } },
|
|
||||||
} as unknown as GlobalPublicStore))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('should render correctly with version information', () => {
|
it('should render correctly with version information', () => {
|
||||||
// Act
|
renderWithSystemFeatures(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />, {
|
||||||
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
systemFeatures: { branding: { enabled: false } },
|
||||||
|
})
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByText(/^Version/)).toBeInTheDocument()
|
expect(screen.getByText(/^Version/)).toBeInTheDocument()
|
||||||
expect(screen.getAllByText(/0.6.0/).length).toBeGreaterThan(0)
|
expect(screen.getAllByText(/0.6.0/).length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render branding logo if enabled', () => {
|
it('should render branding logo if enabled', () => {
|
||||||
// Arrange
|
renderWithSystemFeatures(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />, {
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
|
||||||
systemFeatures: { branding: { enabled: true, workspace_logo: 'custom-logo.png' } },
|
systemFeatures: { branding: { enabled: true, workspace_logo: 'custom-logo.png' } },
|
||||||
} as unknown as GlobalPublicStore))
|
})
|
||||||
|
|
||||||
// Act
|
|
||||||
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const img = screen.getByAltText('logo')
|
const img = screen.getByAltText('logo')
|
||||||
expect(img).toBeInTheDocument()
|
expect(img).toBeInTheDocument()
|
||||||
expect(img).toHaveAttribute('src', 'custom-logo.png')
|
expect(img).toHaveAttribute('src', 'custom-logo.png')
|
||||||
@@ -67,21 +53,16 @@ describe('AccountAbout', () => {
|
|||||||
|
|
||||||
describe('Version Logic', () => {
|
describe('Version Logic', () => {
|
||||||
it('should show "Latest Available" when current version equals latest', () => {
|
it('should show "Latest Available" when current version equals latest', () => {
|
||||||
// Act
|
renderWithSystemFeatures(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
||||||
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByText(/about.latestAvailable/)).toBeInTheDocument()
|
expect(screen.getByText(/about.latestAvailable/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show "Now Available" when current version is behind', () => {
|
it('should show "Now Available" when current version is behind', () => {
|
||||||
// Arrange
|
|
||||||
const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
|
const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
|
||||||
|
|
||||||
// Act
|
renderWithSystemFeatures(<AccountAbout langGeniusVersionInfo={behindVersionInfo} onCancel={mockOnCancel} />)
|
||||||
render(<AccountAbout langGeniusVersionInfo={behindVersionInfo} onCancel={mockOnCancel} />)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByText(/about.nowAvailable/)).toBeInTheDocument()
|
expect(screen.getByText(/about.nowAvailable/)).toBeInTheDocument()
|
||||||
expect(screen.getByText(/about.updateNow/)).toBeInTheDocument()
|
expect(screen.getByText(/about.updateNow/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@@ -89,33 +70,26 @@ describe('AccountAbout', () => {
|
|||||||
|
|
||||||
describe('Community Edition', () => {
|
describe('Community Edition', () => {
|
||||||
it('should render correctly in Community Edition', () => {
|
it('should render correctly in Community Edition', () => {
|
||||||
// Arrange
|
|
||||||
mockIsCEEdition = true
|
mockIsCEEdition = true
|
||||||
|
|
||||||
// Act
|
renderWithSystemFeatures(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
||||||
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.getByText(/Open Source License/)).toBeInTheDocument()
|
expect(screen.getByText(/Open Source License/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should hide update button in Community Edition when behind version', () => {
|
it('should hide update button in Community Edition when behind version', () => {
|
||||||
// Arrange
|
|
||||||
mockIsCEEdition = true
|
mockIsCEEdition = true
|
||||||
const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
|
const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
|
||||||
|
|
||||||
// Act
|
renderWithSystemFeatures(<AccountAbout langGeniusVersionInfo={behindVersionInfo} onCancel={mockOnCancel} />)
|
||||||
render(<AccountAbout langGeniusVersionInfo={behindVersionInfo} onCancel={mockOnCancel} />)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(screen.queryByText(/about.updateNow/)).not.toBeInTheDocument()
|
expect(screen.queryByText(/about.updateNow/)).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('User Interactions', () => {
|
describe('User Interactions', () => {
|
||||||
it('should call onCancel when close button is clicked', () => {
|
it('should call onCancel when close button is clicked', () => {
|
||||||
// Act
|
renderWithSystemFeatures(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
||||||
render(<AccountAbout langGeniusVersionInfo={mockVersionInfo} onCancel={mockOnCancel} />)
|
|
||||||
// Modal uses Headless UI Dialog which renders into a portal, so we need to use document
|
// Modal uses Headless UI Dialog which renders into a portal, so we need to use document
|
||||||
const closeButton = document.querySelector('div.absolute.cursor-pointer')
|
const closeButton = document.querySelector('div.absolute.cursor-pointer')
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
import type { LangGeniusVersionResponse } from '@/models/common'
|
import type { LangGeniusVersionResponse } from '@/models/common'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import Modal from '@/app/components/base/modal'
|
import Modal from '@/app/components/base/modal'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { IS_CE_EDITION } from '@/config'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
|
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
|
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
|
|
||||||
type IAccountSettingProps = {
|
type IAccountSettingProps = {
|
||||||
langGeniusVersionInfo: LangGeniusVersionResponse
|
langGeniusVersionInfo: LangGeniusVersionResponse
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
@@ -22,7 +23,7 @@ export default function AccountAbout({
|
|||||||
}: IAccountSettingProps) {
|
}: IAccountSettingProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isLatest = langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version
|
const isLatest = langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import type { AppContextValue } from '@/context/app-context'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import type { ModalContextState } from '@/context/modal-context'
|
import type { ModalContextState } from '@/context/modal-context'
|
||||||
import type { ProviderContextState } from '@/context/provider-context'
|
import type { ProviderContextState } from '@/context/provider-context'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useRouter } from '@/next/navigation'
|
import { useRouter } from '@/next/navigation'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import AppSelector from '../index'
|
import AppSelector from '../index'
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Array<infer U>
|
||||||
|
? Array<U>
|
||||||
|
: T extends object
|
||||||
|
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: T
|
||||||
|
|
||||||
vi.mock('../../account-setting', () => ({
|
vi.mock('../../account-setting', () => ({
|
||||||
default: () => <div data-testid="account-setting">AccountSetting</div>,
|
default: () => <div data-testid="account-setting">AccountSetting</div>,
|
||||||
}))
|
}))
|
||||||
@@ -37,10 +43,6 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
useAppContext: vi.fn(),
|
useAppContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: vi.fn(),
|
useProviderContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -79,15 +81,19 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
vi.mock('@/config', () => ({
|
vi.mock('@/config', async (importOriginal) => {
|
||||||
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
|
const actual = await importOriginal<typeof import('@/config')>()
|
||||||
get AMPLITUDE_API_KEY() { return mockConfig.AMPLITUDE_API_KEY },
|
return {
|
||||||
get isAmplitudeEnabled() { return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY },
|
...actual,
|
||||||
get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
|
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
|
||||||
get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS },
|
get AMPLITUDE_API_KEY() { return mockConfig.AMPLITUDE_API_KEY },
|
||||||
IS_DEV: false,
|
get isAmplitudeEnabled() { return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY },
|
||||||
IS_CE_EDITION: false,
|
get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
|
||||||
}))
|
get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS },
|
||||||
|
IS_DEV: false,
|
||||||
|
IS_CE_EDITION: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
vi.mock('@/env', () => mockEnv)
|
vi.mock('@/env', () => mockEnv)
|
||||||
|
|
||||||
const baseAppContextValue: AppContextValue = {
|
const baseAppContextValue: AppContextValue = {
|
||||||
@@ -136,20 +142,13 @@ describe('AccountDropdown', () => {
|
|||||||
const mockLogout = vi.fn()
|
const mockLogout = vi.fn()
|
||||||
const mockSetShowAccountSettingModal = vi.fn()
|
const mockSetShowAccountSettingModal = vi.fn()
|
||||||
|
|
||||||
const renderWithRouter = (ui: React.ReactElement) => {
|
const renderWithRouter = (
|
||||||
const queryClient = new QueryClient({
|
ui: React.ReactElement,
|
||||||
defaultOptions: {
|
options: { systemFeatures?: DeepPartial<SystemFeatures> } = {},
|
||||||
queries: {
|
) => {
|
||||||
retry: false,
|
return renderWithSystemFeatures(ui, {
|
||||||
},
|
systemFeatures: options.systemFeatures ?? { branding: { enabled: false } },
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
{ui}
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -159,10 +158,6 @@ describe('AccountDropdown', () => {
|
|||||||
mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'show'
|
mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'show'
|
||||||
|
|
||||||
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
|
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector?: unknown) => {
|
|
||||||
const fullState = { systemFeatures: { branding: { enabled: false } }, setSystemFeatures: vi.fn() }
|
|
||||||
return typeof selector === 'function' ? (selector as (state: typeof fullState) => unknown)(fullState) : fullState
|
|
||||||
})
|
|
||||||
vi.mocked(useProviderContext).mockReturnValue({
|
vi.mocked(useProviderContext).mockReturnValue({
|
||||||
isEducationAccount: false,
|
isEducationAccount: false,
|
||||||
plan: { type: Plan.sandbox },
|
plan: { type: Plan.sandbox },
|
||||||
@@ -316,14 +311,10 @@ describe('AccountDropdown', () => {
|
|||||||
|
|
||||||
describe('Branding and Environment', () => {
|
describe('Branding and Environment', () => {
|
||||||
it('should hide sections when branding is enabled', () => {
|
it('should hide sections when branding is enabled', () => {
|
||||||
// Arrange
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector?: unknown) => {
|
|
||||||
const fullState = { systemFeatures: { branding: { enabled: true } }, setSystemFeatures: vi.fn() }
|
|
||||||
return typeof selector === 'function' ? (selector as (state: typeof fullState) => unknown)(fullState) : fullState
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
renderWithRouter(<AppSelector />)
|
renderWithRouter(<AppSelector />, {
|
||||||
|
systemFeatures: { branding: { enabled: true } },
|
||||||
|
})
|
||||||
fireEvent.click(screen.getByRole('button'))
|
fireEvent.click(screen.getByRole('button'))
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { MouseEventHandler, ReactNode } from 'react'
|
|||||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||||
@@ -12,13 +13,13 @@ import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
|||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { env } from '@/env'
|
import { env } from '@/env'
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
import { useRouter } from '@/next/navigation'
|
import { useRouter } from '@/next/navigation'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import AccountAbout from '../account-about'
|
import AccountAbout from '../account-about'
|
||||||
import GithubStar from '../github-star'
|
import GithubStar from '../github-star'
|
||||||
@@ -110,7 +111,7 @@ export default function AppSelector() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [aboutVisible, setAboutVisible] = useState(false)
|
const [aboutVisible, setAboutVisible] = useState(false)
|
||||||
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
|
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const docLink = useDocLink()
|
const docLink = useDocLink()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { AccountSettingTab } from '../constants'
|
import type { AccountSettingTab } from '../constants'
|
||||||
import type { AppContextValue } from '@/context/app-context'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
@@ -47,36 +47,6 @@ vi.mock('@/hooks/use-breakpoints', () => ({
|
|||||||
default: vi.fn(),
|
default: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import('@/context/global-public-context')>()
|
|
||||||
const systemFeatures = {
|
|
||||||
...actual.useGlobalPublicStore.getState().systemFeatures,
|
|
||||||
webapp_auth: {
|
|
||||||
...actual.useGlobalPublicStore.getState().systemFeatures.webapp_auth,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
branding: {
|
|
||||||
...actual.useGlobalPublicStore.getState().systemFeatures.branding,
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
enable_marketplace: true,
|
|
||||||
enable_collaboration_mode: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useGlobalPublicStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
|
|
||||||
systemFeatures,
|
|
||||||
}),
|
|
||||||
useSystemFeaturesQuery: () => ({
|
|
||||||
data: systemFeatures,
|
|
||||||
isPending: false,
|
|
||||||
isLoading: false,
|
|
||||||
isFetching: false,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||||
useDefaultModel: vi.fn(() => ({ data: null, isLoading: false })),
|
useDefaultModel: vi.fn(() => ({ data: null, isLoading: false })),
|
||||||
useUpdateDefaultModel: vi.fn(() => ({ trigger: vi.fn() })),
|
useUpdateDefaultModel: vi.fn(() => ({ trigger: vi.fn() })),
|
||||||
@@ -176,11 +146,14 @@ describe('AccountSetting', () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(
|
return renderWithSystemFeatures(<StatefulAccountSetting />, {
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
systemFeatures: {
|
||||||
<StatefulAccountSetting />
|
webapp_auth: { enabled: true },
|
||||||
</QueryClientProvider>,
|
branding: { enabled: false },
|
||||||
)
|
enable_marketplace: true,
|
||||||
|
enable_collaboration_mode: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { UseQueryResult } from '@tanstack/react-query'
|
import type { UseQueryResult } from '@tanstack/react-query'
|
||||||
import type { DataSourceAuth } from '../types'
|
import type { DataSourceAuth } from '../types'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { usePluginAuthAction } from '@/app/components/plugins/plugin-auth'
|
import { usePluginAuthAction } from '@/app/components/plugins/plugin-auth'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
import { useGetDataSourceListAuth, useGetDataSourceOAuthUrl } from '@/service/use-datasource'
|
import { useGetDataSourceListAuth, useGetDataSourceOAuthUrl } from '@/service/use-datasource'
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
|
||||||
import { useDataSourceAuthUpdate, useMarketplaceAllPlugins } from '../hooks'
|
import { useDataSourceAuthUpdate, useMarketplaceAllPlugins } from '../hooks'
|
||||||
import DataSourcePage from '../index'
|
import DataSourcePage from '../index'
|
||||||
|
|
||||||
@@ -24,10 +23,6 @@ vi.mock('@/hooks/use-i18n', () => ({
|
|||||||
useRenderI18nObject: vi.fn(),
|
useRenderI18nObject: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/service/use-datasource', () => ({
|
vi.mock('@/service/use-datasource', () => ({
|
||||||
useGetDataSourceListAuth: vi.fn(),
|
useGetDataSourceListAuth: vi.fn(),
|
||||||
useGetDataSourceOAuthUrl: vi.fn(),
|
useGetDataSourceOAuthUrl: vi.fn(),
|
||||||
@@ -96,18 +91,14 @@ describe('DataSourcePage Component', () => {
|
|||||||
describe('Initial View Rendering', () => {
|
describe('Initial View Rendering', () => {
|
||||||
it('should render an empty view when no data is available and marketplace is disabled', () => {
|
it('should render an empty view when no data is available and marketplace is disabled', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
/* eslint-disable-next-line ts/no-explicit-any */
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: any) =>
|
|
||||||
selector({
|
|
||||||
systemFeatures: { ...defaultSystemFeatures, enable_marketplace: false },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
render(<DataSourcePage />)
|
renderWithSystemFeatures(<DataSourcePage />, {
|
||||||
|
systemFeatures: { enable_marketplace: false },
|
||||||
|
})
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.queryByText('Dify Source')).not.toBeInTheDocument()
|
expect(screen.queryByText('Dify Source')).not.toBeInTheDocument()
|
||||||
@@ -118,18 +109,14 @@ describe('DataSourcePage Component', () => {
|
|||||||
describe('Data Source List Rendering', () => {
|
describe('Data Source List Rendering', () => {
|
||||||
it('should render Card components for each data source returned from the API', () => {
|
it('should render Card components for each data source returned from the API', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
/* eslint-disable-next-line ts/no-explicit-any */
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: any) =>
|
|
||||||
selector({
|
|
||||||
systemFeatures: { ...defaultSystemFeatures, enable_marketplace: false },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
||||||
data: { result: mockProviders },
|
data: { result: mockProviders },
|
||||||
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
render(<DataSourcePage />)
|
renderWithSystemFeatures(<DataSourcePage />, {
|
||||||
|
systemFeatures: { enable_marketplace: false },
|
||||||
|
})
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.getByText('Dify Source')).toBeInTheDocument()
|
expect(screen.getByText('Dify Source')).toBeInTheDocument()
|
||||||
@@ -140,18 +127,14 @@ describe('DataSourcePage Component', () => {
|
|||||||
describe('Marketplace Integration', () => {
|
describe('Marketplace Integration', () => {
|
||||||
it('should render the InstallFromMarketplace component when enable_marketplace feature is enabled', () => {
|
it('should render the InstallFromMarketplace component when enable_marketplace feature is enabled', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
/* eslint-disable-next-line ts/no-explicit-any */
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: any) =>
|
|
||||||
selector({
|
|
||||||
systemFeatures: { ...defaultSystemFeatures, enable_marketplace: true },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
||||||
data: { result: mockProviders },
|
data: { result: mockProviders },
|
||||||
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
render(<DataSourcePage />)
|
renderWithSystemFeatures(<DataSourcePage />, {
|
||||||
|
systemFeatures: { enable_marketplace: true },
|
||||||
|
})
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.getByText('common.modelProvider.installDataSourceProvider')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.installDataSourceProvider')).toBeInTheDocument()
|
||||||
@@ -160,18 +143,14 @@ describe('DataSourcePage Component', () => {
|
|||||||
|
|
||||||
it('should pass an empty array to InstallFromMarketplace if data result is missing but marketplace is enabled', () => {
|
it('should pass an empty array to InstallFromMarketplace if data result is missing but marketplace is enabled', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
/* eslint-disable-next-line ts/no-explicit-any */
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: any) =>
|
|
||||||
selector({
|
|
||||||
systemFeatures: { ...defaultSystemFeatures, enable_marketplace: true },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
render(<DataSourcePage />)
|
renderWithSystemFeatures(<DataSourcePage />, {
|
||||||
|
systemFeatures: { enable_marketplace: true },
|
||||||
|
})
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.getByText('common.modelProvider.installDataSourceProvider')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.installDataSourceProvider')).toBeInTheDocument()
|
||||||
@@ -179,38 +158,30 @@ describe('DataSourcePage Component', () => {
|
|||||||
|
|
||||||
it('should handle the case where data exists but result is an empty array', () => {
|
it('should handle the case where data exists but result is an empty array', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
/* eslint-disable-next-line ts/no-explicit-any */
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: any) =>
|
|
||||||
selector({
|
|
||||||
systemFeatures: { ...defaultSystemFeatures, enable_marketplace: true },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
||||||
data: { result: [] },
|
data: { result: [] },
|
||||||
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
render(<DataSourcePage />)
|
renderWithSystemFeatures(<DataSourcePage />, {
|
||||||
|
systemFeatures: { enable_marketplace: true },
|
||||||
|
})
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.queryByText('Dify Source')).not.toBeInTheDocument()
|
expect(screen.queryByText('Dify Source')).not.toBeInTheDocument()
|
||||||
expect(screen.getByText('common.modelProvider.installDataSourceProvider')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.installDataSourceProvider')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle the case where systemFeatures is missing (edge case for coverage)', () => {
|
it('should handle the case where enable_marketplace is false (edge case for coverage)', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
/* eslint-disable-next-line ts/no-explicit-any */
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation((selector: any) =>
|
|
||||||
selector({
|
|
||||||
systemFeatures: {},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
vi.mocked(useGetDataSourceListAuth).mockReturnValue({
|
||||||
data: { result: [] },
|
data: { result: [] },
|
||||||
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
} as unknown as UseQueryResult<{ result: DataSourceAuth[] }, Error>)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
render(<DataSourcePage />)
|
renderWithSystemFeatures(<DataSourcePage />, {
|
||||||
|
systemFeatures: { enable_marketplace: false },
|
||||||
|
})
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(screen.queryByText('common.modelProvider.installDataSourceProvider')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.installDataSourceProvider')).not.toBeInTheDocument()
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useGetDataSourceListAuth } from '@/service/use-datasource'
|
import { useGetDataSourceListAuth } from '@/service/use-datasource'
|
||||||
import Card from './card'
|
import Card from './card'
|
||||||
import InstallFromMarketplace from './install-from-marketplace'
|
import InstallFromMarketplace from './install-from-marketplace'
|
||||||
|
|
||||||
const DataSourcePage = () => {
|
const DataSourcePage = () => {
|
||||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: enable_marketplace } = useSuspenseQuery({
|
||||||
|
...systemFeaturesQueryOptions(),
|
||||||
|
select: s => s.enable_marketplace,
|
||||||
|
})
|
||||||
const { data } = useGetDataSourceListAuth()
|
const { data } = useGetDataSourceListAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import type { AppContextValue } from '@/context/app-context'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import type { ICurrentWorkspace, Member } from '@/models/common'
|
import type { ICurrentWorkspace, Member } from '@/models/common'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import MembersPage from '../index'
|
import MembersPage from '../index'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/context/global-public-context')
|
|
||||||
vi.mock('@/context/provider-context')
|
vi.mock('@/context/provider-context')
|
||||||
vi.mock('@/hooks/use-format-time-from-now')
|
vi.mock('@/hooks/use-format-time-from-now')
|
||||||
vi.mock('@/service/use-common')
|
vi.mock('@/service/use-common')
|
||||||
|
|
||||||
|
const renderMembersPage = () => renderWithSystemFeatures(<MembersPage />, {
|
||||||
|
systemFeatures: { is_email_setup: true },
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../edit-workspace-modal', () => ({
|
vi.mock('../edit-workspace-modal', () => ({
|
||||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||||
<div>
|
<div>
|
||||||
@@ -112,10 +115,6 @@ describe('MembersPage', () => {
|
|||||||
refetch: mockRefetch,
|
refetch: mockRefetch,
|
||||||
} as unknown as ReturnType<typeof useMembers>)
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
|
||||||
systemFeatures: { is_email_setup: true },
|
|
||||||
} as unknown as Parameters<typeof selector>[0]))
|
|
||||||
|
|
||||||
vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({
|
vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({
|
||||||
enableBilling: false,
|
enableBilling: false,
|
||||||
isAllowTransferWorkspace: true,
|
isAllowTransferWorkspace: true,
|
||||||
@@ -127,7 +126,7 @@ describe('MembersPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render workspace and member information', () => {
|
it('should render workspace and member information', () => {
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText('Test Workspace'))!.toBeInTheDocument()
|
expect(screen.getByText('Test Workspace'))!.toBeInTheDocument()
|
||||||
expect(screen.getByText('Owner User'))!.toBeInTheDocument()
|
expect(screen.getByText('Owner User'))!.toBeInTheDocument()
|
||||||
@@ -137,7 +136,7 @@ describe('MembersPage', () => {
|
|||||||
it('should open and close invite modal', async () => {
|
it('should open and close invite modal', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /invite/i }))
|
await user.click(screen.getByRole('button', { name: /invite/i }))
|
||||||
expect(screen.getByText('Invite Modal'))!.toBeInTheDocument()
|
expect(screen.getByText('Invite Modal'))!.toBeInTheDocument()
|
||||||
@@ -149,7 +148,7 @@ describe('MembersPage', () => {
|
|||||||
it('should open invited modal after invite results are sent', async () => {
|
it('should open invited modal after invite results are sent', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /invite/i }))
|
await user.click(screen.getByRole('button', { name: /invite/i }))
|
||||||
await user.click(screen.getByRole('button', { name: 'Send Invite Results' }))
|
await user.click(screen.getByRole('button', { name: 'Send Invite Results' }))
|
||||||
@@ -164,7 +163,7 @@ describe('MembersPage', () => {
|
|||||||
it('should open transfer ownership modal when transfer action is used', async () => {
|
it('should open transfer ownership modal when transfer action is used', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /transfer ownership/i }))
|
await user.click(screen.getByRole('button', { name: /transfer ownership/i }))
|
||||||
expect(screen.getByText('Transfer Ownership Modal'))!.toBeInTheDocument()
|
expect(screen.getByText('Transfer Ownership Modal'))!.toBeInTheDocument()
|
||||||
@@ -176,7 +175,7 @@ describe('MembersPage', () => {
|
|||||||
isAllowTransferWorkspace: false,
|
isAllowTransferWorkspace: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText('common.members.owner'))!.toBeInTheDocument()
|
expect(screen.getByText('common.members.owner'))!.toBeInTheDocument()
|
||||||
expect(screen.queryByRole('button', { name: /transfer ownership/i })).not.toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: /transfer ownership/i })).not.toBeInTheDocument()
|
||||||
@@ -190,7 +189,7 @@ describe('MembersPage', () => {
|
|||||||
isCurrentWorkspaceManager: false,
|
isCurrentWorkspaceManager: false,
|
||||||
} as unknown as AppContextValue)
|
} as unknown as AppContextValue)
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.queryByRole('button', { name: /invite/i })).not.toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: /invite/i })).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('Transfer ownership')).not.toBeInTheDocument()
|
expect(screen.queryByText('Transfer ownership')).not.toBeInTheDocument()
|
||||||
@@ -199,7 +198,7 @@ describe('MembersPage', () => {
|
|||||||
it('should open and close edit workspace modal', async () => {
|
it('should open and close edit workspace modal', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
await user.click(screen.getByTestId('edit-workspace-pencil'))
|
await user.click(screen.getByTestId('edit-workspace-pencil'))
|
||||||
expect(screen.getByText('Edit Workspace Modal'))!.toBeInTheDocument()
|
expect(screen.getByText('Edit Workspace Modal'))!.toBeInTheDocument()
|
||||||
@@ -211,7 +210,7 @@ describe('MembersPage', () => {
|
|||||||
it('should close transfer ownership modal when close is clicked', async () => {
|
it('should close transfer ownership modal when close is clicked', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /transfer ownership/i }))
|
await user.click(screen.getByRole('button', { name: /transfer ownership/i }))
|
||||||
expect(screen.getByText('Transfer Ownership Modal'))!.toBeInTheDocument()
|
expect(screen.getByText('Transfer Ownership Modal'))!.toBeInTheDocument()
|
||||||
@@ -230,7 +229,7 @@ describe('MembersPage', () => {
|
|||||||
refetch: mockRefetch,
|
refetch: mockRefetch,
|
||||||
} as unknown as ReturnType<typeof useMembers>)
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText(/members\.pending/i))!.toBeInTheDocument()
|
expect(screen.getByText(/members\.pending/i))!.toBeInTheDocument()
|
||||||
expect(screen.getByText(/members\.you/i))!.toBeInTheDocument() // Current user is owner@example.com
|
expect(screen.getByText(/members\.you/i))!.toBeInTheDocument() // Current user is owner@example.com
|
||||||
@@ -245,7 +244,7 @@ describe('MembersPage', () => {
|
|||||||
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText(/plansCommon\.member/i))!.toBeInTheDocument()
|
expect(screen.getByText(/plansCommon\.member/i))!.toBeInTheDocument()
|
||||||
expect(screen.getByText('2'))!.toBeInTheDocument() // accounts.length
|
expect(screen.getByText('2'))!.toBeInTheDocument() // accounts.length
|
||||||
@@ -262,7 +261,7 @@ describe('MembersPage', () => {
|
|||||||
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText(/plansCommon\.unlimited/i))!.toBeInTheDocument()
|
expect(screen.getByText(/plansCommon\.unlimited/i))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@@ -276,7 +275,7 @@ describe('MembersPage', () => {
|
|||||||
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
// Plan.team is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout
|
// Plan.team is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout
|
||||||
// Plan.team is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout
|
// Plan.team is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout
|
||||||
@@ -291,7 +290,7 @@ describe('MembersPage', () => {
|
|||||||
isCurrentWorkspaceManager: true,
|
isCurrentWorkspaceManager: true,
|
||||||
} as unknown as AppContextValue)
|
} as unknown as AppContextValue)
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /invite/i }))!.toBeInTheDocument()
|
expect(screen.getByRole('button', { name: /invite/i }))!.toBeInTheDocument()
|
||||||
expect(screen.queryByRole('button', { name: /transfer ownership/i })).not.toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: /transfer ownership/i })).not.toBeInTheDocument()
|
||||||
@@ -308,7 +307,7 @@ describe('MembersPage', () => {
|
|||||||
refetch: mockRefetch,
|
refetch: mockRefetch,
|
||||||
} as unknown as ReturnType<typeof useMembers>)
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(mockFormatTimeFromNow).toHaveBeenCalledWith(1700000000000)
|
expect(mockFormatTimeFromNow).toHaveBeenCalledWith(1700000000000)
|
||||||
})
|
})
|
||||||
@@ -326,7 +325,7 @@ describe('MembersPage', () => {
|
|||||||
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText(/plansCommon\.member/i))!.toBeInTheDocument()
|
expect(screen.getByText(/plansCommon\.member/i))!.toBeInTheDocument()
|
||||||
expect(screen.getByText('1'))!.toBeInTheDocument()
|
expect(screen.getByText('1'))!.toBeInTheDocument()
|
||||||
@@ -338,7 +337,7 @@ describe('MembersPage', () => {
|
|||||||
refetch: mockRefetch,
|
refetch: mockRefetch,
|
||||||
} as unknown as ReturnType<typeof useMembers>)
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText(/plansCommon\.memberAfter/i))!.toBeInTheDocument()
|
expect(screen.getByText(/plansCommon\.memberAfter/i))!.toBeInTheDocument()
|
||||||
expect(screen.getByText('1'))!.toBeInTheDocument()
|
expect(screen.getByText('1'))!.toBeInTheDocument()
|
||||||
@@ -356,7 +355,7 @@ describe('MembersPage', () => {
|
|||||||
refetch: mockRefetch,
|
refetch: mockRefetch,
|
||||||
} as unknown as ReturnType<typeof useMembers>)
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText('common.members.normal'))!.toBeInTheDocument()
|
expect(screen.getByText('common.members.normal'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@@ -370,7 +369,7 @@ describe('MembersPage', () => {
|
|||||||
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
} as unknown as ReturnType<typeof useProviderContext>['plan'],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
render(<MembersPage />)
|
renderMembersPage()
|
||||||
|
|
||||||
expect(screen.getByText('Upgrade Button'))!.toBeInTheDocument()
|
expect(screen.getByText('Upgrade Button'))!.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import type { AppContextValue } from '@/context/app-context'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import type { ICurrentWorkspace } from '@/models/common'
|
import type { ICurrentWorkspace } from '@/models/common'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
import InviteButton from '../invite-button'
|
import InviteButton from '../invite-button'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/context/global-public-context')
|
|
||||||
vi.mock('@/service/use-workspace')
|
vi.mock('@/service/use-workspace')
|
||||||
|
|
||||||
describe('InviteButton', () => {
|
describe('InviteButton', () => {
|
||||||
const setupMocks = ({
|
const setupPermissions = ({
|
||||||
brandingEnabled,
|
|
||||||
isFetching,
|
isFetching,
|
||||||
allowInvite,
|
allowInvite,
|
||||||
}: {
|
}: {
|
||||||
brandingEnabled: boolean
|
|
||||||
isFetching: boolean
|
isFetching: boolean
|
||||||
allowInvite?: boolean
|
allowInvite?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
|
||||||
systemFeatures: { branding: { enabled: brandingEnabled } },
|
|
||||||
} as unknown as Parameters<typeof selector>[0]))
|
|
||||||
vi.mocked(useWorkspacePermissions).mockReturnValue({
|
vi.mocked(useWorkspacePermissions).mockReturnValue({
|
||||||
data: allowInvite === undefined ? null : { allow_member_invite: allowInvite },
|
data: allowInvite === undefined ? null : { allow_member_invite: allowInvite },
|
||||||
isFetching,
|
isFetching,
|
||||||
} as unknown as ReturnType<typeof useWorkspacePermissions>)
|
} as unknown as ReturnType<typeof useWorkspacePermissions>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderInviteButton = (brandingEnabled: boolean) =>
|
||||||
|
renderWithSystemFeatures(<InviteButton />, {
|
||||||
|
systemFeatures: { branding: { enabled: brandingEnabled } },
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
@@ -38,33 +37,33 @@ describe('InviteButton', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show invite button when branding is disabled', () => {
|
it('should show invite button when branding is disabled', () => {
|
||||||
setupMocks({ brandingEnabled: false, isFetching: false })
|
setupPermissions({ isFetching: false })
|
||||||
|
|
||||||
render(<InviteButton />)
|
renderInviteButton(false)
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /members\.invite/i })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: /members\.invite/i })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show loading status while permissions are loading', () => {
|
it('should show loading status while permissions are loading', () => {
|
||||||
setupMocks({ brandingEnabled: true, isFetching: true })
|
setupPermissions({ isFetching: true })
|
||||||
|
|
||||||
render(<InviteButton />)
|
renderInviteButton(true)
|
||||||
|
|
||||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should hide invite button when permission is denied', () => {
|
it('should hide invite button when permission is denied', () => {
|
||||||
setupMocks({ brandingEnabled: true, isFetching: false, allowInvite: false })
|
setupPermissions({ isFetching: false, allowInvite: false })
|
||||||
|
|
||||||
render(<InviteButton />)
|
renderInviteButton(true)
|
||||||
|
|
||||||
expect(screen.queryByRole('button', { name: /members\.invite/i })).not.toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: /members\.invite/i })).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show invite button when permission is granted', () => {
|
it('should show invite button when permission is granted', () => {
|
||||||
setupMocks({ brandingEnabled: true, isFetching: false, allowInvite: true })
|
setupPermissions({ isFetching: false, allowInvite: true })
|
||||||
|
|
||||||
render(<InviteButton />)
|
renderInviteButton(true)
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /members\.invite/i })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: /members\.invite/i })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
import type { InvitationResult } from '@/models/common'
|
import type { InvitationResult } from '@/models/common'
|
||||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
import { LanguagesSupported } from '@/i18n-config/language'
|
import { LanguagesSupported } from '@/i18n-config/language'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import EditWorkspaceModal from './edit-workspace-modal'
|
import EditWorkspaceModal from './edit-workspace-modal'
|
||||||
import InviteButton from './invite-button'
|
import InviteButton from './invite-button'
|
||||||
@@ -35,7 +36,7 @@ const MembersPage = () => {
|
|||||||
|
|
||||||
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
|
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
|
||||||
const { data, refetch } = useMembers()
|
const { data, refetch } = useMembers()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||||
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
||||||
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { RiUserAddLine } from '@remixicon/react'
|
import { RiUserAddLine } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
|
|
||||||
type InviteButtonProps = {
|
type InviteButtonProps = {
|
||||||
@@ -14,7 +15,7 @@ type InviteButtonProps = {
|
|||||||
const InviteButton = (props: InviteButtonProps) => {
|
const InviteButton = (props: InviteButtonProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentWorkspace } = useAppContext()
|
const { currentWorkspace } = useAppContext()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
|
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
|
||||||
if (systemFeatures.branding.enabled) {
|
if (systemFeatures.branding.enabled) {
|
||||||
if (isFetchingWorkspacePermissions) {
|
if (isFetchingWorkspacePermissions) {
|
||||||
|
|||||||
@@ -1,36 +1,38 @@
|
|||||||
import type { AppContextValue } from '@/context/app-context'
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import type { ICurrentWorkspace } from '@/models/common'
|
import type { ICurrentWorkspace } from '@/models/common'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
import TransferOwnership from '../transfer-ownership'
|
import TransferOwnership from '../transfer-ownership'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
vi.mock('@/context/global-public-context')
|
|
||||||
vi.mock('@/service/use-workspace')
|
vi.mock('@/service/use-workspace')
|
||||||
|
|
||||||
describe('TransferOwnership', () => {
|
describe('TransferOwnership', () => {
|
||||||
const setupMocks = ({
|
const setupPermissions = ({
|
||||||
brandingEnabled,
|
|
||||||
isFetching,
|
isFetching,
|
||||||
allowOwnerTransfer,
|
allowOwnerTransfer,
|
||||||
}: {
|
}: {
|
||||||
brandingEnabled: boolean
|
|
||||||
isFetching: boolean
|
isFetching: boolean
|
||||||
allowOwnerTransfer?: boolean
|
allowOwnerTransfer?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
|
||||||
systemFeatures: { branding: { enabled: brandingEnabled } },
|
|
||||||
} as unknown as Parameters<typeof selector>[0]))
|
|
||||||
vi.mocked(useWorkspacePermissions).mockReturnValue({
|
vi.mocked(useWorkspacePermissions).mockReturnValue({
|
||||||
data: allowOwnerTransfer === undefined ? null : { allow_owner_transfer: allowOwnerTransfer },
|
data: allowOwnerTransfer === undefined ? null : { allow_owner_transfer: allowOwnerTransfer },
|
||||||
isFetching,
|
isFetching,
|
||||||
} as unknown as ReturnType<typeof useWorkspacePermissions>)
|
} as unknown as ReturnType<typeof useWorkspacePermissions>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderTransferOwnership = (
|
||||||
|
brandingEnabled: boolean,
|
||||||
|
onOperate: () => void = vi.fn(),
|
||||||
|
) =>
|
||||||
|
renderWithSystemFeatures(<TransferOwnership onOperate={onOperate} />, {
|
||||||
|
systemFeatures: { branding: { enabled: brandingEnabled } },
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
@@ -39,17 +41,17 @@ describe('TransferOwnership', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show loading status while permissions are loading', () => {
|
it('should show loading status while permissions are loading', () => {
|
||||||
setupMocks({ brandingEnabled: true, isFetching: true })
|
setupPermissions({ isFetching: true })
|
||||||
|
|
||||||
render(<TransferOwnership onOperate={vi.fn()} />)
|
renderTransferOwnership(true)
|
||||||
|
|
||||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show owner text without transfer menu when transfer is forbidden', () => {
|
it('should show owner text without transfer menu when transfer is forbidden', () => {
|
||||||
setupMocks({ brandingEnabled: true, isFetching: false, allowOwnerTransfer: false })
|
setupPermissions({ isFetching: false, allowOwnerTransfer: false })
|
||||||
|
|
||||||
render(<TransferOwnership onOperate={vi.fn()} />)
|
renderTransferOwnership(true)
|
||||||
|
|
||||||
expect(screen.getByText(/members\.owner/i)).toBeInTheDocument()
|
expect(screen.getByText(/members\.owner/i)).toBeInTheDocument()
|
||||||
expect(screen.queryByText(/members\.transferOwnership/i)).toBeNull()
|
expect(screen.queryByText(/members\.transferOwnership/i)).toBeNull()
|
||||||
@@ -59,9 +61,9 @@ describe('TransferOwnership', () => {
|
|||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
const onOperate = vi.fn()
|
const onOperate = vi.fn()
|
||||||
|
|
||||||
setupMocks({ brandingEnabled: true, isFetching: false, allowOwnerTransfer: true })
|
setupPermissions({ isFetching: false, allowOwnerTransfer: true })
|
||||||
|
|
||||||
render(<TransferOwnership onOperate={onOperate} />)
|
renderTransferOwnership(true, onOperate)
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /members\.owner/i }))
|
await user.click(screen.getByRole('button', { name: /members\.owner/i }))
|
||||||
const transferOptionText = await screen.findByText(/members\.transferOwnership/i)
|
const transferOptionText = await screen.findByText(/members\.transferOwnership/i)
|
||||||
@@ -78,9 +80,9 @@ describe('TransferOwnership', () => {
|
|||||||
it('should allow transfer menu when branding is disabled', async () => {
|
it('should allow transfer menu when branding is disabled', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
setupMocks({ brandingEnabled: false, isFetching: false })
|
setupPermissions({ isFetching: false })
|
||||||
|
|
||||||
render(<TransferOwnership onOperate={vi.fn()} />)
|
renderTransferOwnership(false)
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /members\.owner/i }))
|
await user.click(screen.getByRole('button', { name: /members\.owner/i }))
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { cn } from '@langgenius/dify-ui/cn'
|
|||||||
import {
|
import {
|
||||||
RiArrowDownSLine,
|
RiArrowDownSLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -18,7 +19,7 @@ type Props = {
|
|||||||
const TransferOwnership = ({ onOperate }: Props) => {
|
const TransferOwnership = ({ onOperate }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentWorkspace } = useAppContext()
|
const { currentWorkspace } = useAppContext()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
|
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
|
||||||
if (systemFeatures.branding.enabled) {
|
if (systemFeatures.branding.enabled) {
|
||||||
if (isFetchingWorkspacePermissions) {
|
if (isFetchingWorkspacePermissions) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import {
|
import {
|
||||||
CurrentSystemQuotaTypeEnum,
|
CurrentSystemQuotaTypeEnum,
|
||||||
CustomConfigurationStatusEnum,
|
CustomConfigurationStatusEnum,
|
||||||
@@ -15,17 +16,13 @@ const mockQuotaConfig = {
|
|||||||
is_valid: true,
|
is_valid: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
vi.mock('@/config', () => ({
|
vi.mock('@/config', async (importOriginal) => {
|
||||||
IS_CLOUD_EDITION: false,
|
const actual = await importOriginal<typeof import('@/config')>()
|
||||||
}))
|
return {
|
||||||
|
...actual,
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
IS_CLOUD_EDITION: false,
|
||||||
useSystemFeaturesQuery: () => ({
|
}
|
||||||
data: {
|
})
|
||||||
enable_marketplace: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => ({
|
useProviderContext: () => ({
|
||||||
@@ -62,26 +59,41 @@ vi.mock('../install-from-marketplace', () => ({
|
|||||||
default: () => <div data-testid="install-from-marketplace" />,
|
default: () => <div data-testid="install-from-marketplace" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
vi.mock('@/service/client', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||||
|
const originalPlugins = actual.consoleQuery.plugins as unknown as Record<string, unknown>
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useQuery: () => ({ data: undefined }),
|
consoleQuery: new Proxy(actual.consoleQuery, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (prop === 'plugins') {
|
||||||
|
return {
|
||||||
|
...originalPlugins,
|
||||||
|
checkInstalled: {
|
||||||
|
queryOptions: () => ({
|
||||||
|
queryKey: ['plugins', 'checkInstalled'],
|
||||||
|
queryFn: () => new Promise(() => {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
latestVersions: {
|
||||||
|
queryOptions: () => ({
|
||||||
|
queryKey: ['plugins', 'latestVersions'],
|
||||||
|
queryFn: () => new Promise(() => {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Reflect.get(target, prop)
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('@/service/client', () => ({
|
|
||||||
consoleQuery: {
|
|
||||||
plugins: {
|
|
||||||
checkInstalled: { queryOptions: () => ({}) },
|
|
||||||
latestVersions: { queryOptions: () => ({}) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('ModelProviderPage non-cloud branch', () => {
|
describe('ModelProviderPage non-cloud branch', () => {
|
||||||
it('should skip the quota panel when cloud edition is disabled', () => {
|
it('should skip the quota panel when cloud edition is disabled', () => {
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderWithSystemFeatures(<ModelProviderPage searchText="" />, {
|
||||||
|
systemFeatures: { enable_marketplace: false },
|
||||||
|
})
|
||||||
|
|
||||||
expect(screen.getByTestId('system-model-selector')).toBeInTheDocument()
|
expect(screen.getByTestId('system-model-selector')).toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('quota-panel')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('quota-panel')).not.toBeInTheDocument()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { act, render, screen } from '@testing-library/react'
|
import { act, screen } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import {
|
import {
|
||||||
CurrentSystemQuotaTypeEnum,
|
CurrentSystemQuotaTypeEnum,
|
||||||
CustomConfigurationStatusEnum,
|
CustomConfigurationStatusEnum,
|
||||||
@@ -7,8 +8,6 @@ import {
|
|||||||
} from '../declarations'
|
} from '../declarations'
|
||||||
import ModelProviderPage from '../index'
|
import ModelProviderPage from '../index'
|
||||||
|
|
||||||
let mockEnableMarketplace = true
|
|
||||||
|
|
||||||
const mockQuotaConfig = {
|
const mockQuotaConfig = {
|
||||||
quota_type: CurrentSystemQuotaTypeEnum.free,
|
quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||||
quota_unit: QuotaUnitEnum.times,
|
quota_unit: QuotaUnitEnum.times,
|
||||||
@@ -18,13 +17,14 @@ const mockQuotaConfig = {
|
|||||||
is_valid: true,
|
is_valid: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
const renderModelProviderPage = (
|
||||||
useSystemFeaturesQuery: () => ({
|
props: { searchText?: string, enableMarketplace?: boolean } = {},
|
||||||
data: {
|
) => {
|
||||||
enable_marketplace: mockEnableMarketplace,
|
const { searchText = '', enableMarketplace = true } = props
|
||||||
},
|
return renderWithSystemFeatures(<ModelProviderPage searchText={searchText} />, {
|
||||||
}),
|
systemFeatures: { enable_marketplace: enableMarketplace },
|
||||||
}))
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const mockProviders = [
|
const mockProviders = [
|
||||||
{
|
{
|
||||||
@@ -83,28 +83,40 @@ vi.mock('../system-model-selector', () => ({
|
|||||||
default: () => <div data-testid="system-model-selector" />,
|
default: () => <div data-testid="system-model-selector" />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
vi.mock('@/service/client', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||||
|
const originalPlugins = actual.consoleQuery.plugins as unknown as Record<string, unknown>
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useQuery: () => ({ data: undefined }),
|
consoleQuery: new Proxy(actual.consoleQuery, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (prop === 'plugins') {
|
||||||
|
return {
|
||||||
|
...originalPlugins,
|
||||||
|
checkInstalled: {
|
||||||
|
queryOptions: () => ({
|
||||||
|
queryKey: ['plugins', 'checkInstalled'],
|
||||||
|
queryFn: () => new Promise(() => {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
latestVersions: {
|
||||||
|
queryOptions: () => ({
|
||||||
|
queryKey: ['plugins', 'latestVersions'],
|
||||||
|
queryFn: () => new Promise(() => {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Reflect.get(target, prop)
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('@/service/client', () => ({
|
|
||||||
consoleQuery: {
|
|
||||||
plugins: {
|
|
||||||
checkInstalled: { queryOptions: () => ({}) },
|
|
||||||
latestVersions: { queryOptions: () => ({}) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('ModelProviderPage', () => {
|
describe('ModelProviderPage', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockEnableMarketplace = true
|
|
||||||
Object.keys(mockDefaultModels).forEach((key) => {
|
Object.keys(mockDefaultModels).forEach((key) => {
|
||||||
mockDefaultModels[key] = { data: null, isLoading: false }
|
mockDefaultModels[key] = { data: null, isLoading: false }
|
||||||
})
|
})
|
||||||
@@ -134,21 +146,21 @@ describe('ModelProviderPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render main elements', () => {
|
it('should render main elements', () => {
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('system-model-selector')).toBeInTheDocument()
|
expect(screen.getByTestId('system-model-selector')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('install-from-marketplace')).toBeInTheDocument()
|
expect(screen.getByTestId('install-from-marketplace')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render configured and not configured providers sections', () => {
|
it('should render configured and not configured providers sections', () => {
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||||
expect(screen.getByText('common.modelProvider.toBeConfigured')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.toBeConfigured')).toBeInTheDocument()
|
||||||
expect(screen.getByText('anthropic')).toBeInTheDocument()
|
expect(screen.getByText('anthropic')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should filter providers based on search text', () => {
|
it('should filter providers based on search text', () => {
|
||||||
render(<ModelProviderPage searchText="open" />)
|
renderModelProviderPage({ searchText: 'open' })
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(600)
|
vi.advanceTimersByTime(600)
|
||||||
})
|
})
|
||||||
@@ -157,7 +169,7 @@ describe('ModelProviderPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show empty state if no configured providers match', () => {
|
it('should show empty state if no configured providers match', () => {
|
||||||
render(<ModelProviderPage searchText="non-existent" />)
|
renderModelProviderPage({ searchText: 'non-existent' })
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(600)
|
vi.advanceTimersByTime(600)
|
||||||
})
|
})
|
||||||
@@ -165,9 +177,7 @@ describe('ModelProviderPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should hide marketplace section when marketplace feature is disabled', () => {
|
it('should hide marketplace section when marketplace feature is disabled', () => {
|
||||||
mockEnableMarketplace = false
|
renderModelProviderPage({ enableMarketplace: false })
|
||||||
|
|
||||||
render(<ModelProviderPage searchText="" />)
|
|
||||||
|
|
||||||
expect(screen.queryByTestId('install-from-marketplace')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('install-from-marketplace')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@@ -185,14 +195,14 @@ describe('ModelProviderPage', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
|
||||||
expect(screen.getByText('common.modelProvider.emptyProviderTitle')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.emptyProviderTitle')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show none-configured warning when providers exist but no default models set', () => {
|
it('should show none-configured warning when providers exist but no default models set', () => {
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
expect(screen.getByText('common.modelProvider.noneConfigured')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.noneConfigured')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -202,7 +212,7 @@ describe('ModelProviderPage', () => {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
expect(screen.getByText('common.modelProvider.notConfigured')).toBeInTheDocument()
|
expect(screen.getByText('common.modelProvider.notConfigured')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -217,7 +227,7 @@ describe('ModelProviderPage', () => {
|
|||||||
mockDefaultModels.speech2text = makeModel('whisper-1', 'speech2text')
|
mockDefaultModels.speech2text = makeModel('whisper-1', 'speech2text')
|
||||||
mockDefaultModels.tts = makeModel('tts-1', 'tts')
|
mockDefaultModels.tts = makeModel('tts-1', 'tts')
|
||||||
|
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
expect(screen.queryByText('common.modelProvider.noProviderInstalled')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.noProviderInstalled')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
|
||||||
@@ -228,7 +238,7 @@ describe('ModelProviderPage', () => {
|
|||||||
mockDefaultModels[key] = { data: null, isLoading: true }
|
mockDefaultModels[key] = { data: null, isLoading: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
expect(screen.queryByText('common.modelProvider.noProviderInstalled')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.noProviderInstalled')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.noneConfigured')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
|
expect(screen.queryByText('common.modelProvider.notConfigured')).not.toBeInTheDocument()
|
||||||
@@ -265,7 +275,7 @@ describe('ModelProviderPage', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<ModelProviderPage searchText="" />)
|
renderModelProviderPage()
|
||||||
|
|
||||||
const renderedProviders = screen.getAllByTestId('provider-card').map(item => item.textContent)
|
const renderedProviders = screen.getAllByTestId('provider-card').map(item => item.textContent)
|
||||||
expect(renderedProviders).toEqual([
|
expect(renderedProviders).toEqual([
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type {
|
|||||||
} from './declarations'
|
} from './declarations'
|
||||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useDebounce } from 'ahooks'
|
import { useDebounce } from 'ahooks'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePluginsWithLatestVersion } from '@/app/components/plugins/hooks'
|
import { usePluginsWithLatestVersion } from '@/app/components/plugins/hooks'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
import { useSystemFeaturesQuery } from '@/context/global-public-context'
|
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { consoleQuery } from '@/service/client'
|
import { consoleQuery } from '@/service/client'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import {
|
import {
|
||||||
CustomConfigurationStatusEnum,
|
CustomConfigurationStatusEnum,
|
||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
@@ -42,7 +42,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
|||||||
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } = useDefaultModel(ModelTypeEnum.speech2text)
|
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } = useDefaultModel(ModelTypeEnum.speech2text)
|
||||||
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts)
|
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts)
|
||||||
const { modelProviders: providers } = useProviderContext()
|
const { modelProviders: providers } = useProviderContext()
|
||||||
const { data: systemFeatures } = useSystemFeaturesQuery()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
const allPluginIds = useMemo(() => {
|
const allPluginIds = useMemo(() => {
|
||||||
return [...new Set(providers.map(p => providerToPluginId(p.provider)).filter(Boolean))]
|
return [...new Set(providers.map(p => providerToPluginId(p.provider)).filter(Boolean))]
|
||||||
@@ -59,7 +59,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
|||||||
map.set(plugin.plugin_id, plugin)
|
map.set(plugin.plugin_id, plugin)
|
||||||
return map
|
return map
|
||||||
}, [enrichedPlugins])
|
}, [enrichedPlugins])
|
||||||
const enableMarketplace = systemFeatures?.enable_marketplace ?? false
|
const enableMarketplace = systemFeatures.enable_marketplace
|
||||||
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|
||||||
|| isEmbeddingsDefaultModelLoading
|
|| isEmbeddingsDefaultModelLoading
|
||||||
|| isRerankDefaultModelLoading
|
|| isRerankDefaultModelLoading
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { Model, ModelItem, ModelProvider } from '../../declarations'
|
import type { Model, ModelItem, ModelProvider } from '../../declarations'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import {
|
import {
|
||||||
ConfigurationMethodEnum,
|
ConfigurationMethodEnum,
|
||||||
ModelFeatureEnum,
|
ModelFeatureEnum,
|
||||||
@@ -59,11 +62,12 @@ vi.mock('@/context/provider-context', () => ({
|
|||||||
useProviderContext: () => ({ modelProviders: mockContextModelProviders.current }),
|
useProviderContext: () => ({ modelProviders: mockContextModelProviders.current }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
const renderPopup = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
useSystemFeaturesQuery: () => ({
|
// The Popup component never inspects trial_models beyond passing them
|
||||||
data: { trial_models: mockTrialModels.current },
|
// through, so an opaque string[] is enough; cast to satisfy the
|
||||||
}),
|
// ModelProviderQuotaGetPaid[] declared on SystemFeatures.
|
||||||
}))
|
systemFeatures: { trial_models: mockTrialModels.current as unknown as SystemFeatures['trial_models'] },
|
||||||
|
})
|
||||||
|
|
||||||
const mockTrialCredits = vi.hoisted(() => ({
|
const mockTrialCredits = vi.hoisted(() => ({
|
||||||
credits: 200,
|
credits: 200,
|
||||||
@@ -183,7 +187,7 @@ describe('Popup', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should filter models by search and allow clearing search', () => {
|
it('should filter models by search and allow clearing search', () => {
|
||||||
const { container } = render(
|
const { container } = renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel()]}
|
modelList={[makeModel()]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -204,7 +208,7 @@ describe('Popup', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not show compatible-only helper text when no scope features are applied', () => {
|
it('should not show compatible-only helper text when no scope features are applied', () => {
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel()]}
|
modelList={[makeModel()]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -216,7 +220,7 @@ describe('Popup', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show compatible-only helper banner when scope features are applied', () => {
|
it('should show compatible-only helper banner when scope features are applied', () => {
|
||||||
const { container } = render(
|
const { container } = renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel()]}
|
modelList={[makeModel()]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -236,7 +240,7 @@ describe('Popup', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
mockSupportFunctionCall.mockReturnValue(false)
|
mockSupportFunctionCall.mockReturnValue(false)
|
||||||
const { unmount } = render(
|
const { unmount } = renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={modelList}
|
modelList={modelList}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -248,7 +252,7 @@ describe('Popup', () => {
|
|||||||
|
|
||||||
unmount()
|
unmount()
|
||||||
mockSupportFunctionCall.mockReturnValue(true)
|
mockSupportFunctionCall.mockReturnValue(true)
|
||||||
const { unmount: unmount2 } = render(
|
const { unmount: unmount2 } = renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={modelList}
|
modelList={modelList}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -259,7 +263,7 @@ describe('Popup', () => {
|
|||||||
expect(screen.getByText('openai'))!.toBeInTheDocument()
|
expect(screen.getByText('openai'))!.toBeInTheDocument()
|
||||||
|
|
||||||
unmount2()
|
unmount2()
|
||||||
const { unmount: unmount3 } = render(
|
const { unmount: unmount3 } = renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={modelList}
|
modelList={modelList}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -270,7 +274,7 @@ describe('Popup', () => {
|
|||||||
expect(screen.getByText('openai'))!.toBeInTheDocument()
|
expect(screen.getByText('openai'))!.toBeInTheDocument()
|
||||||
|
|
||||||
unmount3()
|
unmount3()
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel({ models: [makeModelItem({ features: undefined })] })]}
|
modelList={[makeModel({ models: [makeModelItem({ features: undefined })] })]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -284,7 +288,7 @@ describe('Popup', () => {
|
|||||||
it('should match model labels from fallback languages when current language key is missing', () => {
|
it('should match model labels from fallback languages when current language key is missing', () => {
|
||||||
mockLanguage = 'fr_FR'
|
mockLanguage = 'fr_FR'
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[
|
modelList={[
|
||||||
makeModel({
|
makeModel({
|
||||||
@@ -323,7 +327,7 @@ describe('Popup', () => {
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel()]}
|
modelList={[makeModel()]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -350,7 +354,7 @@ describe('Popup', () => {
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel()]}
|
modelList={[makeModel()]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -380,7 +384,7 @@ describe('Popup', () => {
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel()]}
|
modelList={[makeModel()]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -393,7 +397,7 @@ describe('Popup', () => {
|
|||||||
|
|
||||||
it('should open provider settings when clicking footer link', () => {
|
it('should open provider settings when clicking footer link', () => {
|
||||||
const onHide = vi.fn()
|
const onHide = vi.fn()
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[makeModel()]}
|
modelList={[makeModel()]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -411,7 +415,7 @@ describe('Popup', () => {
|
|||||||
|
|
||||||
it('should show empty state when no providers are configured', () => {
|
it('should show empty state when no providers are configured', () => {
|
||||||
const onHide = vi.fn()
|
const onHide = vi.fn()
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -432,7 +436,7 @@ describe('Popup', () => {
|
|||||||
it('should render marketplace providers that are not installed', () => {
|
it('should render marketplace providers that are not installed', () => {
|
||||||
mockContextModelProviders.current = [makeContextProvider({ provider: 'test-openai' })]
|
mockContextModelProviders.current = [makeContextProvider({ provider: 'test-openai' })]
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -454,7 +458,7 @@ describe('Popup', () => {
|
|||||||
} as MockContextProvider['system_configuration'],
|
} as MockContextProvider['system_configuration'],
|
||||||
})]
|
})]
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -479,7 +483,7 @@ describe('Popup', () => {
|
|||||||
} as MockContextProvider['system_configuration'],
|
} as MockContextProvider['system_configuration'],
|
||||||
})]
|
})]
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -493,7 +497,7 @@ describe('Popup', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should toggle marketplace section collapse', () => {
|
it('should toggle marketplace section collapse', () => {
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -518,7 +522,7 @@ describe('Popup', () => {
|
|||||||
]
|
]
|
||||||
mockInstallMutateAsync.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
mockInstallMutateAsync.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -541,7 +545,7 @@ describe('Popup', () => {
|
|||||||
]
|
]
|
||||||
mockInstallMutateAsync.mockRejectedValue(new Error('Install failed'))
|
mockInstallMutateAsync.mockRejectedValue(new Error('Install failed'))
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -567,7 +571,7 @@ describe('Popup', () => {
|
|||||||
mockInstallMutateAsync.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
mockInstallMutateAsync.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
||||||
mockCheck.mockResolvedValue(undefined)
|
mockCheck.mockResolvedValue(undefined)
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -593,7 +597,7 @@ describe('Popup', () => {
|
|||||||
]
|
]
|
||||||
mockMarketplacePlugins.isLoading = true
|
mockMarketplacePlugins.isLoading = true
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -611,7 +615,7 @@ describe('Popup', () => {
|
|||||||
it('should skip install requests when the marketplace plugin cannot be found', async () => {
|
it('should skip install requests when the marketplace plugin cannot be found', async () => {
|
||||||
mockMarketplacePlugins.current = []
|
mockMarketplacePlugins.current = []
|
||||||
|
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
modelList={[]}
|
modelList={[]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
@@ -627,7 +631,7 @@ describe('Popup', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should sort the selected provider to the top when a default model is provided', () => {
|
it('should sort the selected provider to the top when a default model is provided', () => {
|
||||||
render(
|
renderPopup(
|
||||||
<Popup
|
<Popup
|
||||||
defaultModel={{ provider: 'anthropic', model: 'claude-3' }}
|
defaultModel={{ provider: 'anthropic', model: 'claude-3' }}
|
||||||
modelList={[
|
modelList={[
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ import type {
|
|||||||
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||||
import { useSystemFeaturesQuery } from '@/context/global-public-context'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||||
import { supportFunctionCall } from '@/utils/tool-call'
|
import { supportFunctionCall } from '@/utils/tool-call'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
@@ -60,8 +61,8 @@ const Popup: FC<PopupProps> = ({
|
|||||||
const { refreshPluginList } = useRefreshPluginList()
|
const { refreshPluginList } = useRefreshPluginList()
|
||||||
const [installingProvider, setInstallingProvider] = useState<ModelProviderQuotaGetPaid | null>(null)
|
const [installingProvider, setInstallingProvider] = useState<ModelProviderQuotaGetPaid | null>(null)
|
||||||
const { isExhausted: isCreditsExhausted } = useTrialCredits()
|
const { isExhausted: isCreditsExhausted } = useTrialCredits()
|
||||||
const { data: systemFeatures } = useSystemFeaturesQuery()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const trialModels = systemFeatures?.trial_models
|
const trialModels = systemFeatures.trial_models
|
||||||
const installedProviderMap = useMemo(() => new Map(
|
const installedProviderMap = useMemo(() => new Map(
|
||||||
modelProviders.map(provider => [provider.provider, provider]),
|
modelProviders.map(provider => [provider.provider, provider]),
|
||||||
), [modelProviders])
|
), [modelProviders])
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ModelProvider } from '../../declarations'
|
import type { ModelProvider } from '../../declarations'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import {
|
import {
|
||||||
ConfigurationMethodEnum,
|
ConfigurationMethodEnum,
|
||||||
CurrentSystemQuotaTypeEnum,
|
CurrentSystemQuotaTypeEnum,
|
||||||
@@ -28,10 +28,6 @@ vi.mock('@/config', async (importOriginal) => {
|
|||||||
return { ...actual, IS_CLOUD_EDITION: true }
|
return { ...actual, IS_CLOUD_EDITION: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useSystemFeaturesQuery: () => ({ data: { trial_models: ['langgenius/openai/openai'] } }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||||
default: { notify: mockToastNotify },
|
default: { notify: mockToastNotify },
|
||||||
toast: {
|
toast: {
|
||||||
@@ -42,24 +38,33 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/service/client', () => ({
|
vi.mock('@/service/client', async (importOriginal) => {
|
||||||
consoleQuery: {
|
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||||
modelProviders: {
|
const mockedModelProviders = {
|
||||||
models: {
|
models: {
|
||||||
queryKey: ({ input }: { input: { params: { provider: string } } }) => ['console', 'modelProviders', 'models', input.params.provider],
|
queryKey: ({ input }: { input: { params: { provider: string } } }) => ['console', 'modelProviders', 'models', input.params.provider],
|
||||||
},
|
|
||||||
changePreferredProviderType: {
|
|
||||||
mutationOptions: (opts: Record<string, unknown>) => ({
|
|
||||||
mutationFn: (...args: unknown[]) => {
|
|
||||||
mockChangePriorityFn(...args)
|
|
||||||
return Promise.resolve({ result: 'success' })
|
|
||||||
},
|
|
||||||
...opts,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
changePreferredProviderType: {
|
||||||
}))
|
mutationOptions: (opts: Record<string, unknown>) => ({
|
||||||
|
mutationFn: (...args: unknown[]) => {
|
||||||
|
mockChangePriorityFn(...args)
|
||||||
|
return Promise.resolve({ result: 'success' })
|
||||||
|
},
|
||||||
|
...opts,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
consoleQuery: new Proxy(actual.consoleQuery, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (prop === 'modelProviders')
|
||||||
|
return mockedModelProviders
|
||||||
|
return Reflect.get(target, prop)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useUpdateModelList: () => mockUpdateModelList,
|
useUpdateModelList: () => mockUpdateModelList,
|
||||||
@@ -88,13 +93,6 @@ vi.mock('@/app/components/base/icons/src/vender/line/alertsAndFeedback/Warning',
|
|||||||
default: (props: Record<string, unknown>) => <div data-testid="warning-icon" className={props.className as string} />,
|
default: (props: Record<string, unknown>) => <div data-testid="warning-icon" className={props.className as string} />,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const createTestQueryClient = () => new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: { retry: false, gcTime: 0 },
|
|
||||||
mutations: { retry: false },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
||||||
provider: 'langgenius/openai/openai',
|
provider: 'langgenius/openai/openai',
|
||||||
provider_credential_schema: { credential_form_schemas: [] },
|
provider_credential_schema: { credential_form_schemas: [] },
|
||||||
@@ -112,12 +110,9 @@ const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider =
|
|||||||
} as unknown as ModelProvider)
|
} as unknown as ModelProvider)
|
||||||
|
|
||||||
const renderWithQueryClient = (provider: ModelProvider) => {
|
const renderWithQueryClient = (provider: ModelProvider) => {
|
||||||
const queryClient = createTestQueryClient()
|
return renderWithSystemFeatures(<CredentialPanel provider={provider} />, {
|
||||||
return render(
|
systemFeatures: { trial_models: ['langgenius/openai/openai'] as never },
|
||||||
<QueryClientProvider client={queryClient}>
|
})
|
||||||
<CredentialPanel provider={provider} />
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('CredentialPanel', () => {
|
describe('CredentialPanel', () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
import type { ModelProvider } from '../../declarations'
|
import type { ModelProvider } from '../../declarations'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import QuotaPanel from '../quota-panel'
|
import QuotaPanel from '../quota-panel'
|
||||||
|
|
||||||
let mockWorkspaceData: {
|
let mockWorkspaceData: {
|
||||||
@@ -37,11 +39,9 @@ vi.mock('@/service/use-common', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
const renderQuotaPanel = (ui: ReactElement) => renderWithSystemFeatures(ui, {
|
||||||
useSystemFeaturesQuery: () => ({
|
systemFeatures: mockTrialModels === undefined ? null : { trial_models: mockTrialModels as never },
|
||||||
data: mockTrialModels ? { trial_models: mockTrialModels } : undefined,
|
})
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('../../hooks', () => ({
|
vi.mock('../../hooks', () => ({
|
||||||
useMarketplaceAllPlugins: () => ({
|
useMarketplaceAllPlugins: () => ({
|
||||||
@@ -89,12 +89,12 @@ describe('QuotaPanel', () => {
|
|||||||
mockWorkspaceData = undefined
|
mockWorkspaceData = undefined
|
||||||
mockWorkspaceIsPending = true
|
mockWorkspaceIsPending = true
|
||||||
|
|
||||||
render(<QuotaPanel providers={mockProviders} />)
|
renderQuotaPanel(<QuotaPanel providers={mockProviders} />)
|
||||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show remaining credits and reset date', () => {
|
it('should show remaining credits and reset date', () => {
|
||||||
render(
|
renderQuotaPanel(
|
||||||
<QuotaPanel
|
<QuotaPanel
|
||||||
providers={mockProviders}
|
providers={mockProviders}
|
||||||
/>,
|
/>,
|
||||||
@@ -108,7 +108,7 @@ describe('QuotaPanel', () => {
|
|||||||
it('should keep quota content during background refetch when cached workspace exists', () => {
|
it('should keep quota content during background refetch when cached workspace exists', () => {
|
||||||
mockWorkspaceIsPending = true
|
mockWorkspaceIsPending = true
|
||||||
|
|
||||||
render(<QuotaPanel providers={mockProviders} />)
|
renderQuotaPanel(<QuotaPanel providers={mockProviders} />)
|
||||||
|
|
||||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||||
expect(screen.getByText('70')).toBeInTheDocument()
|
expect(screen.getByText('70')).toBeInTheDocument()
|
||||||
@@ -121,14 +121,14 @@ describe('QuotaPanel', () => {
|
|||||||
next_credit_reset_date: '',
|
next_credit_reset_date: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<QuotaPanel providers={mockProviders} />)
|
renderQuotaPanel(<QuotaPanel providers={mockProviders} />)
|
||||||
|
|
||||||
expect(screen.getByText(/modelProvider\.card\.quotaExhausted/)).toBeInTheDocument()
|
expect(screen.getByText(/modelProvider\.card\.quotaExhausted/)).toBeInTheDocument()
|
||||||
expect(screen.queryByText(/modelProvider\.resetDate/)).not.toBeInTheDocument()
|
expect(screen.queryByText(/modelProvider\.resetDate/)).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should open install modal when clicking an unsupported trial provider', () => {
|
it('should open install modal when clicking an unsupported trial provider', () => {
|
||||||
render(<QuotaPanel providers={[]} />)
|
renderQuotaPanel(<QuotaPanel providers={[]} />)
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('openai'))
|
fireEvent.click(screen.getByText('openai'))
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ describe('QuotaPanel', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should close install modal when provider becomes installed', async () => {
|
it('should close install modal when provider becomes installed', async () => {
|
||||||
const { rerender } = render(<QuotaPanel providers={[]} />)
|
const { rerender } = renderQuotaPanel(<QuotaPanel providers={[]} />)
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('openai'))
|
fireEvent.click(screen.getByText('openai'))
|
||||||
expect(screen.getByText('install modal')).toBeInTheDocument()
|
expect(screen.getByText('install modal')).toBeInTheDocument()
|
||||||
@@ -151,13 +151,13 @@ describe('QuotaPanel', () => {
|
|||||||
it('should tolerate missing trial model configuration', () => {
|
it('should tolerate missing trial model configuration', () => {
|
||||||
mockTrialModels = undefined
|
mockTrialModels = undefined
|
||||||
|
|
||||||
render(<QuotaPanel providers={mockProviders} />)
|
renderQuotaPanel(<QuotaPanel providers={mockProviders} />)
|
||||||
|
|
||||||
expect(screen.queryByText('openai')).not.toBeInTheDocument()
|
expect(screen.queryByText('openai')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render installed custom providers without opening the install modal', () => {
|
it('should render installed custom providers without opening the install modal', () => {
|
||||||
render(<QuotaPanel providers={mockProviders} />)
|
renderQuotaPanel(<QuotaPanel providers={mockProviders} />)
|
||||||
|
|
||||||
expect(screen.getByLabelText(/modelAPI/)).toBeInTheDocument()
|
expect(screen.getByLabelText(/modelAPI/)).toBeInTheDocument()
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ describe('QuotaPanel', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show the supported-model tooltip for installed non-custom providers', () => {
|
it('should show the supported-model tooltip for installed non-custom providers', () => {
|
||||||
render(
|
renderQuotaPanel(
|
||||||
<QuotaPanel providers={[
|
<QuotaPanel providers={[
|
||||||
{
|
{
|
||||||
provider: 'langgenius/openai/openai',
|
provider: 'langgenius/openai/openai',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ModelProvider } from '../../declarations'
|
import type { ModelProvider } from '../../declarations'
|
||||||
import { renderHook } from '@testing-library/react'
|
import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import {
|
import {
|
||||||
ConfigurationMethodEnum,
|
ConfigurationMethodEnum,
|
||||||
CurrentSystemQuotaTypeEnum,
|
CurrentSystemQuotaTypeEnum,
|
||||||
@@ -15,15 +15,16 @@ vi.mock('../use-trial-credits', () => ({
|
|||||||
useTrialCredits: () => mockTrialCredits,
|
useTrialCredits: () => mockTrialCredits,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useSystemFeaturesQuery: () => ({ data: { trial_models: mockTrialModels } }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/config', async (importOriginal) => {
|
vi.mock('@/config', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@/config')>()
|
const actual = await importOriginal<typeof import('@/config')>()
|
||||||
return { ...actual, IS_CLOUD_EDITION: true }
|
return { ...actual, IS_CLOUD_EDITION: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const renderPanelHook = (provider: ModelProvider | undefined) =>
|
||||||
|
renderHookWithSystemFeatures(() => useCredentialPanelState(provider), {
|
||||||
|
systemFeatures: { trial_models: mockTrialModels as never },
|
||||||
|
})
|
||||||
|
|
||||||
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
||||||
provider: 'langgenius/openai/openai',
|
provider: 'langgenius/openai/openai',
|
||||||
provider_credential_schema: { credential_form_schemas: [] },
|
provider_credential_schema: { credential_form_schemas: [] },
|
||||||
@@ -49,7 +50,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
// Credits priority variants
|
// Credits priority variants
|
||||||
describe('Credits priority variants', () => {
|
describe('Credits priority variants', () => {
|
||||||
it('should return credits-active when credits available', () => {
|
it('should return credits-active when credits available', () => {
|
||||||
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
|
const { result } = renderPanelHook(createProvider())
|
||||||
|
|
||||||
expect(result.current.variant).toBe('credits-active')
|
expect(result.current.variant).toBe('credits-active')
|
||||||
expect(result.current.priority).toBe('credits')
|
expect(result.current.priority).toBe('credits')
|
||||||
@@ -60,7 +61,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
mockTrialCredits.isExhausted = true
|
mockTrialCredits.isExhausted = true
|
||||||
mockTrialCredits.credits = 0
|
mockTrialCredits.credits = 0
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
|
const { result } = renderPanelHook(createProvider())
|
||||||
|
|
||||||
expect(result.current.variant).toBe('api-fallback')
|
expect(result.current.variant).toBe('api-fallback')
|
||||||
})
|
})
|
||||||
@@ -76,7 +77,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('no-usage')
|
expect(result.current.variant).toBe('no-usage')
|
||||||
})
|
})
|
||||||
@@ -90,7 +91,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('credits-exhausted')
|
expect(result.current.variant).toBe('credits-exhausted')
|
||||||
})
|
})
|
||||||
@@ -103,7 +104,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('api-active')
|
expect(result.current.variant).toBe('api-active')
|
||||||
expect(result.current.priority).toBe('apiKey')
|
expect(result.current.priority).toBe('apiKey')
|
||||||
@@ -120,7 +121,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('credits-fallback')
|
expect(result.current.variant).toBe('credits-fallback')
|
||||||
})
|
})
|
||||||
@@ -134,7 +135,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('credits-fallback')
|
expect(result.current.variant).toBe('credits-fallback')
|
||||||
})
|
})
|
||||||
@@ -150,7 +151,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('no-usage')
|
expect(result.current.variant).toBe('no-usage')
|
||||||
})
|
})
|
||||||
@@ -168,7 +169,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('api-unavailable')
|
expect(result.current.variant).toBe('api-unavailable')
|
||||||
})
|
})
|
||||||
@@ -186,7 +187,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.variant).toBe('api-required-configure')
|
expect(result.current.variant).toBe('api-required-configure')
|
||||||
})
|
})
|
||||||
@@ -199,7 +200,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
system_configuration: { enabled: false, current_quota_type: CurrentSystemQuotaTypeEnum.trial, quota_configurations: [] },
|
system_configuration: { enabled: false, current_quota_type: CurrentSystemQuotaTypeEnum.trial, quota_configurations: [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.priority).toBe('apiKeyOnly')
|
expect(result.current.priority).toBe('apiKeyOnly')
|
||||||
expect(result.current.supportsCredits).toBe(false)
|
expect(result.current.supportsCredits).toBe(false)
|
||||||
@@ -212,7 +213,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
preferred_provider_type: PreferredProviderTypeEnum.system,
|
preferred_provider_type: PreferredProviderTypeEnum.system,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.priority).toBe('apiKeyOnly')
|
expect(result.current.priority).toBe('apiKeyOnly')
|
||||||
expect(result.current.supportsCredits).toBe(false)
|
expect(result.current.supportsCredits).toBe(false)
|
||||||
@@ -223,7 +224,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
// Undefined provider
|
// Undefined provider
|
||||||
describe('Undefined provider', () => {
|
describe('Undefined provider', () => {
|
||||||
it('should return safe defaults when provider is undefined', () => {
|
it('should return safe defaults when provider is undefined', () => {
|
||||||
const { result } = renderHook(() => useCredentialPanelState(undefined))
|
const { result } = renderPanelHook(undefined)
|
||||||
|
|
||||||
expect(result.current.priority).toBe('apiKeyOnly')
|
expect(result.current.priority).toBe('apiKeyOnly')
|
||||||
expect(result.current.supportsCredits).toBe(false)
|
expect(result.current.supportsCredits).toBe(false)
|
||||||
@@ -237,7 +238,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
it('should show priority switcher when credits supported and custom config active', () => {
|
it('should show priority switcher when credits supported and custom config active', () => {
|
||||||
const provider = createProvider()
|
const provider = createProvider()
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.showPrioritySwitcher).toBe(true)
|
expect(result.current.showPrioritySwitcher).toBe(true)
|
||||||
})
|
})
|
||||||
@@ -247,7 +248,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
system_configuration: { enabled: false, current_quota_type: CurrentSystemQuotaTypeEnum.trial, quota_configurations: [] },
|
system_configuration: { enabled: false, current_quota_type: CurrentSystemQuotaTypeEnum.trial, quota_configurations: [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.showPrioritySwitcher).toBe(false)
|
expect(result.current.showPrioritySwitcher).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -258,13 +259,13 @@ describe('useCredentialPanelState', () => {
|
|||||||
system_configuration: { enabled: true, current_quota_type: CurrentSystemQuotaTypeEnum.trial, quota_configurations: [] },
|
system_configuration: { enabled: true, current_quota_type: CurrentSystemQuotaTypeEnum.trial, quota_configurations: [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(provider))
|
const { result } = renderPanelHook(provider)
|
||||||
|
|
||||||
expect(result.current.showPrioritySwitcher).toBe(false)
|
expect(result.current.showPrioritySwitcher).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should expose credential name from provider', () => {
|
it('should expose credential name from provider', () => {
|
||||||
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
|
const { result } = renderPanelHook(createProvider())
|
||||||
|
|
||||||
expect(result.current.credentialName).toBe('My Key')
|
expect(result.current.credentialName).toBe('My Key')
|
||||||
})
|
})
|
||||||
@@ -272,7 +273,7 @@ describe('useCredentialPanelState', () => {
|
|||||||
it('should expose credits amount', () => {
|
it('should expose credits amount', () => {
|
||||||
mockTrialCredits.credits = 500
|
mockTrialCredits.credits = 500
|
||||||
|
|
||||||
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
|
const { result } = renderPanelHook(createProvider())
|
||||||
|
|
||||||
expect(result.current.credits).toBe(500)
|
expect(result.current.credits).toBe(500)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import type { Plugin } from '@/app/components/plugins/types'
|
|||||||
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useBoolean } from 'ahooks'
|
import { useBoolean } from 'ahooks'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||||
import { useSystemFeaturesQuery } from '@/context/global-public-context'
|
|
||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { formatNumber } from '@/utils/format'
|
import { formatNumber } from '@/utils/format'
|
||||||
import { PreferredProviderTypeEnum } from '../declarations'
|
import { PreferredProviderTypeEnum } from '../declarations'
|
||||||
import { useMarketplaceAllPlugins } from '../hooks'
|
import { useMarketplaceAllPlugins } from '../hooks'
|
||||||
@@ -32,8 +33,8 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { credits, isExhausted, isLoading, nextCreditResetDate } = useTrialCredits()
|
const { credits, isExhausted, isLoading, nextCreditResetDate } = useTrialCredits()
|
||||||
const { data: systemFeatures } = useSystemFeaturesQuery()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const trialModels = systemFeatures?.trial_models ?? []
|
const trialModels = systemFeatures.trial_models
|
||||||
const providerMap = useMemo(() => new Map(
|
const providerMap = useMemo(() => new Map(
|
||||||
providers.map(p => [p.provider, p.preferred_provider_type]),
|
providers.map(p => [p.provider, p.preferred_provider_type]),
|
||||||
), [providers])
|
), [providers])
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ModelProvider } from '../declarations'
|
import type { ModelProvider } from '../declarations'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||||
import { useSystemFeaturesQuery } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import {
|
import {
|
||||||
PreferredProviderTypeEnum,
|
PreferredProviderTypeEnum,
|
||||||
} from '../declarations'
|
} from '../declarations'
|
||||||
@@ -79,8 +80,8 @@ export function useCredentialPanelState(provider: ModelProvider | undefined): Cr
|
|||||||
current_credential_name,
|
current_credential_name,
|
||||||
} = useCredentialStatus(provider)
|
} = useCredentialStatus(provider)
|
||||||
|
|
||||||
const { data: systemFeatures } = useSystemFeaturesQuery()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const trialModels = systemFeatures?.trial_models
|
const trialModels = systemFeatures.trial_models
|
||||||
|
|
||||||
const preferredType = provider?.preferred_provider_type
|
const preferredType = provider?.preferred_provider_type
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import WorkplaceSelector from '@/app/components/header/account-dropdown/workplace-selector'
|
import WorkplaceSelector from '@/app/components/header/account-dropdown/workplace-selector'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { WorkspaceProvider } from '@/context/workspace-context-provider'
|
import { WorkspaceProvider } from '@/context/workspace-context-provider'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { Plan } from '../billing/type'
|
import { Plan } from '../billing/type'
|
||||||
import AccountDropdown from './account-dropdown'
|
import AccountDropdown from './account-dropdown'
|
||||||
import AppNav from './app-nav'
|
import AppNav from './app-nav'
|
||||||
@@ -33,7 +34,7 @@ const Header = () => {
|
|||||||
const isMobile = media === MediaType.mobile
|
const isMobile = media === MediaType.mobile
|
||||||
const { enableBilling, plan } = useProviderContext()
|
const { enableBilling, plan } = useProviderContext()
|
||||||
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const isFreePlan = plan.type === Plan.sandbox
|
const isFreePlan = plan.type === Plan.sandbox
|
||||||
const isBrandingEnabled = systemFeatures.branding.enabled
|
const isBrandingEnabled = systemFeatures.branding.enabled
|
||||||
const handlePlanClick = useCallback(() => {
|
const handlePlanClick = useCallback(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { screen } from '@testing-library/react'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||||
import { defaultSystemFeatures, LicenseStatus } from '@/types/feature'
|
import { LicenseStatus } from '@/types/feature'
|
||||||
import LicenseNav from '../index'
|
import LicenseNav from '../index'
|
||||||
|
|
||||||
describe('LicenseNav', () => {
|
describe('LicenseNav', () => {
|
||||||
@@ -10,9 +10,6 @@ describe('LicenseNav', () => {
|
|||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
const now = new Date('2024-01-01T12:00:00Z')
|
const now = new Date('2024-01-01T12:00:00Z')
|
||||||
vi.setSystemTime(now)
|
vi.setSystemTime(now)
|
||||||
useGlobalPublicStore.setState({
|
|
||||||
systemFeatures: defaultSystemFeatures,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -20,72 +17,60 @@ describe('LicenseNav', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render null when license status is NONE', () => {
|
it('should render null when license status is NONE', () => {
|
||||||
const { container } = render(<LicenseNav />)
|
const { container } = renderWithSystemFeatures(<LicenseNav />)
|
||||||
expect(container).toBeEmptyDOMElement()
|
expect(container).toBeEmptyDOMElement()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render Enterprise badge when license status is ACTIVE', () => {
|
it('should render Enterprise badge when license status is ACTIVE', () => {
|
||||||
useGlobalPublicStore.setState({
|
renderWithSystemFeatures(<LicenseNav />, {
|
||||||
systemFeatures: {
|
systemFeatures: {
|
||||||
...defaultSystemFeatures,
|
|
||||||
license: {
|
license: {
|
||||||
status: LicenseStatus.ACTIVE,
|
status: LicenseStatus.ACTIVE,
|
||||||
expired_at: null,
|
expired_at: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<LicenseNav />)
|
|
||||||
expect(screen.getByText('Enterprise')).toBeInTheDocument()
|
expect(screen.getByText('Enterprise')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render singular expiring message when license expires in 0 days', () => {
|
it('should render singular expiring message when license expires in 0 days', () => {
|
||||||
const expiredAt = dayjs().add(2, 'hours').toISOString()
|
const expiredAt = dayjs().add(2, 'hours').toISOString()
|
||||||
useGlobalPublicStore.setState({
|
renderWithSystemFeatures(<LicenseNav />, {
|
||||||
systemFeatures: {
|
systemFeatures: {
|
||||||
...defaultSystemFeatures,
|
|
||||||
license: {
|
license: {
|
||||||
status: LicenseStatus.EXPIRING,
|
status: LicenseStatus.EXPIRING,
|
||||||
expired_at: expiredAt,
|
expired_at: expiredAt,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<LicenseNav />)
|
|
||||||
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
|
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
|
||||||
expect(screen.getByText(/count":0/)).toBeInTheDocument()
|
expect(screen.getByText(/count":0/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render singular expiring message when license expires in 1 day', () => {
|
it('should render singular expiring message when license expires in 1 day', () => {
|
||||||
const tomorrow = dayjs().add(1, 'day').add(1, 'hour').toISOString()
|
const tomorrow = dayjs().add(1, 'day').add(1, 'hour').toISOString()
|
||||||
useGlobalPublicStore.setState({
|
renderWithSystemFeatures(<LicenseNav />, {
|
||||||
systemFeatures: {
|
systemFeatures: {
|
||||||
...defaultSystemFeatures,
|
|
||||||
license: {
|
license: {
|
||||||
status: LicenseStatus.EXPIRING,
|
status: LicenseStatus.EXPIRING,
|
||||||
expired_at: tomorrow,
|
expired_at: tomorrow,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<LicenseNav />)
|
|
||||||
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
|
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
|
||||||
expect(screen.getByText(/count":1/)).toBeInTheDocument()
|
expect(screen.getByText(/count":1/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render plural expiring message when license expires in 5 days', () => {
|
it('should render plural expiring message when license expires in 5 days', () => {
|
||||||
const fiveDaysLater = dayjs().add(5, 'day').add(1, 'hour').toISOString()
|
const fiveDaysLater = dayjs().add(5, 'day').add(1, 'hour').toISOString()
|
||||||
useGlobalPublicStore.setState({
|
renderWithSystemFeatures(<LicenseNav />, {
|
||||||
systemFeatures: {
|
systemFeatures: {
|
||||||
...defaultSystemFeatures,
|
|
||||||
license: {
|
license: {
|
||||||
status: LicenseStatus.EXPIRING,
|
status: LicenseStatus.EXPIRING,
|
||||||
expired_at: fiveDaysLater,
|
expired_at: fiveDaysLater,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<LicenseNav />)
|
|
||||||
expect(screen.getByText(/license\.expiring_plural/)).toBeInTheDocument()
|
expect(screen.getByText(/license\.expiring_plural/)).toBeInTheDocument()
|
||||||
expect(screen.getByText(/count":5/)).toBeInTheDocument()
|
expect(screen.getByText(/count":5/)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { RiHourglass2Fill } from '@remixicon/react'
|
import { RiHourglass2Fill } from '@remixicon/react'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { LicenseStatus } from '@/types/feature'
|
import { LicenseStatus } from '@/types/feature'
|
||||||
import PremiumBadge from '../../base/premium-badge'
|
import PremiumBadge from '../../base/premium-badge'
|
||||||
|
|
||||||
const LicenseNav = () => {
|
const LicenseNav = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
|
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
|
||||||
const expiredAt = systemFeatures.license?.expired_at
|
const expiredAt = systemFeatures.license?.expired_at
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { renderHook } from '@testing-library/react'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { renderHookWithSystemFeatures as renderHook } from '@/__tests__/utils/mock-system-features'
|
||||||
import { InstallationScope } from '@/types/feature'
|
import { InstallationScope } from '@/types/feature'
|
||||||
import { pluginInstallLimit } from '../use-install-plugin-limit'
|
import { pluginInstallLimit } from '../use-install-plugin-limit'
|
||||||
|
|
||||||
const mockSystemFeatures = {
|
|
||||||
plugin_installation_permission: {
|
|
||||||
restrict_to_marketplace_only: false,
|
|
||||||
plugin_installation_scope: InstallationScope.ALL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: (selector: (state: { systemFeatures: typeof mockSystemFeatures }) => unknown) =>
|
|
||||||
selector({ systemFeatures: mockSystemFeatures }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const basePlugin = {
|
const basePlugin = {
|
||||||
from: 'marketplace' as const,
|
from: 'marketplace' as const,
|
||||||
verification: { authorized_category: 'langgenius' },
|
verification: { authorized_category: 'langgenius' },
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Plugin, PluginManifestInMarket } from '../../types'
|
import type { Plugin, PluginManifestInMarket } from '../../types'
|
||||||
import type { SystemFeatures } from '@/types/feature'
|
import type { SystemFeatures } from '@/types/feature'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { InstallationScope } from '@/types/feature'
|
import { InstallationScope } from '@/types/feature'
|
||||||
|
|
||||||
type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' }
|
type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' }
|
||||||
@@ -41,6 +42,6 @@ export function pluginInstallLimit(plugin: PluginProps, systemFeatures: SystemFe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function usePluginInstallLimit(plugin: PluginProps) {
|
export default function usePluginInstallLimit(plugin: PluginProps) {
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
return pluginInstallLimit(plugin, systemFeatures)
|
return pluginInstallLimit(plugin, systemFeatures)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, InstallStatus, PackageDependency, Plugin, PluginDeclaration, VersionProps } from '../../../types'
|
import type { Dependency, GitHubItemAndMarketPlaceDependency, InstallStatus, PackageDependency, Plugin, PluginDeclaration, VersionProps } from '../../../types'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import { InstallStep, PluginCategoryEnum } from '../../../types'
|
import { InstallStep, PluginCategoryEnum } from '../../../types'
|
||||||
import InstallBundle, { InstallType } from '../index'
|
import InstallBundle, { InstallType } from '../index'
|
||||||
import GithubItem from '../item/github-item'
|
import GithubItem from '../item/github-item'
|
||||||
@@ -183,11 +184,6 @@ vi.mock('@/context/mitt-context', () => ({
|
|||||||
useMittContextSelector: () => vi.fn(),
|
useMittContextSelector: () => vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock global public context
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: () => ({}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock useCanInstallPluginFromMarketplace
|
// Mock useCanInstallPluginFromMarketplace
|
||||||
vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
|
vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
|
||||||
useCanInstallPluginFromMarketplace: () => ({ canInstallPluginFromMarketplace: true }),
|
useCanInstallPluginFromMarketplace: () => ({ canInstallPluginFromMarketplace: true }),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../../types'
|
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../../types'
|
||||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||||
import { PluginCategoryEnum } from '../../../../types'
|
import { PluginCategoryEnum } from '../../../../types'
|
||||||
import InstallMulti from '../install-multi'
|
import InstallMulti from '../install-multi'
|
||||||
|
|
||||||
@@ -56,11 +57,6 @@ vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', ()
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock useGlobalPublicStore
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: () => ({}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock pluginInstallLimit (imported by the useInstallMultiState hook via @/ path)
|
// Mock pluginInstallLimit (imported by the useInstallMultiState hook via @/ path)
|
||||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit', () => ({
|
vi.mock('@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit', () => ({
|
||||||
pluginInstallLimit: () => ({ canInstall: true }),
|
pluginInstallLimit: () => ({ canInstall: true }),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '@/app/components/plugins/types'
|
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '@/app/components/plugins/types'
|
||||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
import { act, waitFor } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { renderHookWithSystemFeatures as renderHook } from '@/__tests__/utils/mock-system-features'
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
import { getPluginKey, useInstallMultiState } from '../use-install-multi-state'
|
import { getPluginKey, useInstallMultiState } from '../use-install-multi-state'
|
||||||
|
|
||||||
@@ -23,10 +24,6 @@ vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', ()
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/global-public-context', () => ({
|
|
||||||
useGlobalPublicStore: () => ({}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit', () => ({
|
vi.mock('@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit', () => ({
|
||||||
pluginInstallLimit: () => ({ canInstall: mockCanInstall }),
|
pluginInstallLimit: () => ({ canInstall: mockCanInstall }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '@/app/components/plugins/types'
|
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '@/app/components/plugins/types'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||||
import { pluginInstallLimit } from '@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit'
|
import { pluginInstallLimit } from '@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
|
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
|
||||||
|
|
||||||
type UseInstallMultiStateParams = {
|
type UseInstallMultiStateParams = {
|
||||||
@@ -86,7 +87,7 @@ export function useInstallMultiState({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onLoadedAllPlugin,
|
onLoadedAllPlugin,
|
||||||
}: UseInstallMultiStateParams) {
|
}: UseInstallMultiStateParams) {
|
||||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
|
||||||
// Marketplace plugins filtering and index mapping
|
// Marketplace plugins filtering and index mapping
|
||||||
const marketplacePlugins = useMemo(
|
const marketplacePlugins = useMemo(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user