refactor: migrate common service toward TanStack Query (#29009)

This commit is contained in:
yyh
2025-12-19 17:34:14 +08:00
committed by GitHub
parent 89e4261883
commit 079620714e
33 changed files with 885 additions and 633 deletions

View File

@ -1,13 +1,13 @@
'use client'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useRouter, useSearchParams } from 'next/navigation'
import { cn } from '@/utils/classnames'
import Button from '@/app/components/base/button'
import { invitationCheck } from '@/service/common'
import Loading from '@/app/components/base/loading'
import useDocumentTitle from '@/hooks/use-document-title'
import { useInvitationCheck } from '@/service/use-common'
const ActivateForm = () => {
useDocumentTitle('')
@ -26,19 +26,21 @@ const ActivateForm = () => {
token,
},
}
const { data: checkRes } = useSWR(checkParams, invitationCheck, {
revalidateOnFocus: false,
onSuccess(data) {
if (data.is_valid) {
const params = new URLSearchParams(searchParams)
const { email, workspace_id } = data.data
params.set('email', encodeURIComponent(email))
params.set('workspace_id', encodeURIComponent(workspace_id))
params.set('invite_token', encodeURIComponent(token as string))
router.replace(`/signin?${params.toString()}`)
}
},
})
const { data: checkRes } = useInvitationCheck({
...checkParams.params,
token: token || undefined,
}, true)
useEffect(() => {
if (checkRes?.is_valid) {
const params = new URLSearchParams(searchParams)
const { email, workspace_id } = checkRes.data
params.set('email', encodeURIComponent(email))
params.set('workspace_id', encodeURIComponent(workspace_id))
params.set('invite_token', encodeURIComponent(token as string))
router.replace(`/signin?${params.toString()}`)
}
}, [checkRes, router, searchParams, token])
return (
<div className={

View File

@ -7,8 +7,9 @@ import { ChunkingMode, DataSourceType, DatasetPermission, RerankingModeEnum } fr
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { updateDatasetSetting } from '@/service/datasets'
import { fetchMembers } from '@/service/common'
import { useMembers } from '@/service/use-common'
import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
const mockNotify = jest.fn()
const mockOnCancel = jest.fn()
@ -41,8 +42,10 @@ jest.mock('@/service/datasets', () => ({
updateDatasetSetting: jest.fn(),
}))
jest.mock('@/service/common', () => ({
fetchMembers: jest.fn(),
jest.mock('@/service/use-common', () => ({
__esModule: true,
...jest.requireActual('@/service/use-common'),
useMembers: jest.fn(),
}))
jest.mock('@/context/app-context', () => ({
@ -103,7 +106,7 @@ jest.mock('@/app/components/datasets/settings/utils', () => ({
}))
const mockUpdateDatasetSetting = updateDatasetSetting as jest.MockedFunction<typeof updateDatasetSetting>
const mockFetchMembers = fetchMembers as jest.MockedFunction<typeof fetchMembers>
const mockUseMembers = useMembers as jest.MockedFunction<typeof useMembers>
const createRetrievalConfig = (overrides: Partial<RetrievalConfig> = {}): RetrievalConfig => ({
search_method: RETRIEVE_METHOD.semantic,
@ -192,10 +195,43 @@ const renderWithProviders = (dataset: DataSet) => {
)
}
const createMemberList = (): DataSet['partial_member_list'] => ([
'member-2',
])
const renderSettingsModal = async (dataset: DataSet) => {
renderWithProviders(dataset)
await waitFor(() => expect(mockUseMembers).toHaveBeenCalled())
}
describe('SettingsModal', () => {
beforeEach(() => {
jest.clearAllMocks()
mockIsWorkspaceDatasetOperator = false
mockUseMembers.mockReturnValue({
data: {
accounts: [
{
id: 'user-1',
name: 'User One',
email: 'user@example.com',
avatar: 'avatar.png',
avatar_url: 'avatar.png',
status: 'active',
role: 'owner',
},
{
id: 'member-2',
name: 'Member Two',
email: 'member@example.com',
avatar: 'avatar.png',
avatar_url: 'avatar.png',
status: 'active',
role: 'editor',
},
],
},
} as ReturnType<typeof useMembers>)
mockUseModelList.mockImplementation((type: ModelTypeEnum) => {
if (type === ModelTypeEnum.rerank) {
return {
@ -213,261 +249,289 @@ describe('SettingsModal', () => {
mockUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({ defaultModel: null, currentModel: null })
mockUseCurrentProviderAndModel.mockReturnValue({ currentProvider: null, currentModel: null })
mockCheckShowMultiModalTip.mockReturnValue(false)
mockFetchMembers.mockResolvedValue({
accounts: [
{
id: 'user-1',
name: 'User One',
email: 'user@example.com',
avatar: 'avatar.png',
avatar_url: 'avatar.png',
status: 'active',
role: 'owner',
},
{
id: 'member-2',
name: 'Member Two',
email: 'member@example.com',
avatar: 'avatar.png',
avatar_url: 'avatar.png',
status: 'active',
role: 'editor',
},
],
})
mockUpdateDatasetSetting.mockResolvedValue(createDataset())
})
it('renders dataset details', async () => {
renderWithProviders(createDataset())
// Rendering and basic field bindings.
describe('Rendering', () => {
it('should render dataset details when dataset is provided', async () => {
// Arrange
const dataset = createDataset()
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
// Act
await renderSettingsModal(dataset)
expect(screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')).toHaveValue('Test Dataset')
expect(screen.getByPlaceholderText('datasetSettings.form.descPlaceholder')).toHaveValue('Description')
})
it('calls onCancel when cancel is clicked', async () => {
renderWithProviders(createDataset())
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
await userEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
expect(mockOnCancel).toHaveBeenCalledTimes(1)
})
it('shows external knowledge info for external datasets', async () => {
const dataset = createDataset({
provider: 'external',
external_knowledge_info: {
external_knowledge_id: 'ext-id-123',
external_knowledge_api_id: 'ext-api-id-123',
external_knowledge_api_name: 'External Knowledge API',
external_knowledge_api_endpoint: 'https://api.external.com',
},
// Assert
expect(screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')).toHaveValue('Test Dataset')
expect(screen.getByPlaceholderText('datasetSettings.form.descPlaceholder')).toHaveValue('Description')
})
renderWithProviders(dataset)
it('should show external knowledge info when dataset is external', async () => {
// Arrange
const dataset = createDataset({
provider: 'external',
external_knowledge_info: {
external_knowledge_id: 'ext-id-123',
external_knowledge_api_id: 'ext-api-id-123',
external_knowledge_api_name: 'External Knowledge API',
external_knowledge_api_endpoint: 'https://api.external.com',
},
})
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
// Act
await renderSettingsModal(dataset)
expect(screen.getByText('External Knowledge API')).toBeInTheDocument()
expect(screen.getByText('https://api.external.com')).toBeInTheDocument()
expect(screen.getByText('ext-id-123')).toBeInTheDocument()
})
it('updates name when user types', async () => {
renderWithProviders(createDataset())
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')
await userEvent.clear(nameInput)
await userEvent.type(nameInput, 'New Dataset Name')
expect(nameInput).toHaveValue('New Dataset Name')
})
it('updates description when user types', async () => {
renderWithProviders(createDataset())
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
const descriptionInput = screen.getByPlaceholderText('datasetSettings.form.descPlaceholder')
await userEvent.clear(descriptionInput)
await userEvent.type(descriptionInput, 'New description')
expect(descriptionInput).toHaveValue('New description')
})
it('shows and dismisses retrieval change tip when index method changes', async () => {
const dataset = createDataset({ indexing_technique: IndexingType.ECONOMICAL })
renderWithProviders(dataset)
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
await userEvent.click(screen.getByText('datasetCreation.stepTwo.qualified'))
expect(await screen.findByText('appDebug.datasetConfig.retrieveChangeTip')).toBeInTheDocument()
await userEvent.click(screen.getByLabelText('close-retrieval-change-tip'))
await waitFor(() => {
expect(screen.queryByText('appDebug.datasetConfig.retrieveChangeTip')).not.toBeInTheDocument()
// Assert
expect(screen.getByText('External Knowledge API')).toBeInTheDocument()
expect(screen.getByText('https://api.external.com')).toBeInTheDocument()
expect(screen.getByText('ext-id-123')).toBeInTheDocument()
})
})
it('requires dataset name before saving', async () => {
renderWithProviders(createDataset())
// User interactions that update visible state.
describe('Interactions', () => {
it('should call onCancel when cancel button is clicked', async () => {
// Arrange
const user = userEvent.setup()
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
// Act
await renderSettingsModal(createDataset())
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')
await userEvent.clear(nameInput)
await userEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
message: 'datasetSettings.form.nameError',
}))
expect(mockUpdateDatasetSetting).not.toHaveBeenCalled()
})
it('requires rerank model when reranking is enabled', async () => {
mockUseModelList.mockReturnValue({ data: [] })
const dataset = createDataset({}, createRetrievalConfig({
reranking_enable: true,
reranking_model: {
reranking_provider_name: '',
reranking_model_name: '',
},
}))
renderWithProviders(dataset)
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
await userEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
message: 'appDebug.datasetConfig.rerankModelRequired',
}))
expect(mockUpdateDatasetSetting).not.toHaveBeenCalled()
})
it('saves internal dataset changes', async () => {
const rerankRetrieval = createRetrievalConfig({
reranking_enable: true,
reranking_model: {
reranking_provider_name: 'rerank-provider',
reranking_model_name: 'rerank-model',
},
})
const dataset = createDataset({
retrieval_model: rerankRetrieval,
retrieval_model_dict: rerankRetrieval,
// Assert
expect(mockOnCancel).toHaveBeenCalledTimes(1)
})
renderWithProviders(dataset)
it('should update name input when user types', async () => {
// Arrange
const user = userEvent.setup()
await renderSettingsModal(createDataset())
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')
const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')
await userEvent.clear(nameInput)
await userEvent.type(nameInput, 'Updated Internal Dataset')
await userEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
// Act
await user.clear(nameInput)
await user.type(nameInput, 'New Dataset Name')
await waitFor(() => expect(mockUpdateDatasetSetting).toHaveBeenCalled())
// Assert
expect(nameInput).toHaveValue('New Dataset Name')
})
expect(mockUpdateDatasetSetting).toHaveBeenCalledWith(expect.objectContaining({
body: expect.objectContaining({
name: 'Updated Internal Dataset',
permission: DatasetPermission.allTeamMembers,
}),
}))
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'success',
message: 'common.actionMsg.modifiedSuccessfully',
}))
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
name: 'Updated Internal Dataset',
retrieval_model_dict: expect.objectContaining({
it('should update description input when user types', async () => {
// Arrange
const user = userEvent.setup()
await renderSettingsModal(createDataset())
const descriptionInput = screen.getByPlaceholderText('datasetSettings.form.descPlaceholder')
// Act
await user.clear(descriptionInput)
await user.type(descriptionInput, 'New description')
// Assert
expect(descriptionInput).toHaveValue('New description')
})
it('should show and dismiss retrieval change tip when indexing method changes', async () => {
// Arrange
const user = userEvent.setup()
const dataset = createDataset({ indexing_technique: IndexingType.ECONOMICAL })
// Act
await renderSettingsModal(dataset)
await user.click(screen.getByText('datasetCreation.stepTwo.qualified'))
// Assert
expect(await screen.findByText('appDebug.datasetConfig.retrieveChangeTip')).toBeInTheDocument()
// Act
await user.click(screen.getByLabelText('close-retrieval-change-tip'))
// Assert
await waitFor(() => {
expect(screen.queryByText('appDebug.datasetConfig.retrieveChangeTip')).not.toBeInTheDocument()
})
})
it('should open account setting modal when embedding model tip is clicked', async () => {
// Arrange
const user = userEvent.setup()
// Act
await renderSettingsModal(createDataset())
await user.click(screen.getByText('datasetSettings.form.embeddingModelTipLink'))
// Assert
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
})
})
// Validation guardrails before saving.
describe('Validation', () => {
it('should block save when dataset name is empty', async () => {
// Arrange
const user = userEvent.setup()
await renderSettingsModal(createDataset())
const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')
// Act
await user.clear(nameInput)
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
// Assert
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
message: 'datasetSettings.form.nameError',
}))
expect(mockUpdateDatasetSetting).not.toHaveBeenCalled()
})
it('should block save when reranking is enabled without model', async () => {
// Arrange
const user = userEvent.setup()
mockUseModelList.mockReturnValue({ data: [] })
const dataset = createDataset({}, createRetrievalConfig({
reranking_enable: true,
}),
}))
reranking_model: {
reranking_provider_name: '',
reranking_model_name: '',
},
}))
// Act
await renderSettingsModal(dataset)
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
// Assert
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
message: 'appDebug.datasetConfig.rerankModelRequired',
}))
expect(mockUpdateDatasetSetting).not.toHaveBeenCalled()
})
})
it('saves external dataset with partial members and updated retrieval params', async () => {
const dataset = createDataset({
provider: 'external',
permission: DatasetPermission.partialMembers,
partial_member_list: ['member-2'],
external_retrieval_model: {
top_k: 5,
score_threshold: 0.3,
score_threshold_enabled: true,
},
}, {
score_threshold_enabled: true,
score_threshold: 0.8,
// Save flows and side effects.
describe('Save', () => {
it('should save internal dataset changes when form is valid', async () => {
// Arrange
const user = userEvent.setup()
const rerankRetrieval = createRetrievalConfig({
reranking_enable: true,
reranking_model: {
reranking_provider_name: 'rerank-provider',
reranking_model_name: 'rerank-model',
},
})
const dataset = createDataset({
retrieval_model: rerankRetrieval,
retrieval_model_dict: rerankRetrieval,
})
// Act
await renderSettingsModal(dataset)
const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')
await user.clear(nameInput)
await user.type(nameInput, 'Updated Internal Dataset')
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
// Assert
await waitFor(() => expect(mockUpdateDatasetSetting).toHaveBeenCalled())
expect(mockUpdateDatasetSetting).toHaveBeenCalledWith(expect.objectContaining({
body: expect.objectContaining({
name: 'Updated Internal Dataset',
permission: DatasetPermission.allTeamMembers,
}),
}))
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'success',
message: 'common.actionMsg.modifiedSuccessfully',
}))
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
name: 'Updated Internal Dataset',
retrieval_model_dict: expect.objectContaining({
reranking_enable: true,
}),
}))
})
renderWithProviders(dataset)
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
await userEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
await waitFor(() => expect(mockUpdateDatasetSetting).toHaveBeenCalled())
expect(mockUpdateDatasetSetting).toHaveBeenCalledWith(expect.objectContaining({
body: expect.objectContaining({
it('should save external dataset changes when partial members configured', async () => {
// Arrange
const user = userEvent.setup()
const dataset = createDataset({
provider: 'external',
permission: DatasetPermission.partialMembers,
external_retrieval_model: expect.objectContaining({
partial_member_list: createMemberList(),
external_retrieval_model: {
top_k: 5,
}),
partial_member_list: [
{
user_id: 'member-2',
role: 'editor',
},
],
}),
}))
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
retrieval_model_dict: expect.objectContaining({
score_threshold: 0.3,
score_threshold_enabled: true,
},
}, {
score_threshold_enabled: true,
score_threshold: 0.8,
}),
}))
})
})
it('disables save button while saving', async () => {
mockUpdateDatasetSetting.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)))
// Act
await renderSettingsModal(dataset)
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
renderWithProviders(createDataset())
// Assert
await waitFor(() => expect(mockUpdateDatasetSetting).toHaveBeenCalled())
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
expect(mockUpdateDatasetSetting).toHaveBeenCalledWith(expect.objectContaining({
body: expect.objectContaining({
permission: DatasetPermission.partialMembers,
external_retrieval_model: expect.objectContaining({
top_k: 5,
}),
partial_member_list: [
{
user_id: 'member-2',
role: 'editor',
},
],
}),
}))
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
retrieval_model_dict: expect.objectContaining({
score_threshold_enabled: true,
score_threshold: 0.8,
}),
}))
})
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
await userEvent.click(saveButton)
it('should disable save button while saving', async () => {
// Arrange
const user = userEvent.setup()
mockUpdateDatasetSetting.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)))
expect(saveButton).toBeDisabled()
})
// Act
await renderSettingsModal(createDataset())
it('shows error toast when save fails', async () => {
mockUpdateDatasetSetting.mockRejectedValue(new Error('API Error'))
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
await user.click(saveButton)
renderWithProviders(createDataset())
// Assert
expect(saveButton).toBeDisabled()
})
await waitFor(() => expect(mockFetchMembers).toHaveBeenCalled())
it('should show error toast when save fails', async () => {
// Arrange
const user = userEvent.setup()
mockUpdateDatasetSetting.mockRejectedValue(new Error('API Error'))
await userEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
// Act
await renderSettingsModal(createDataset())
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
})
})
})
})

View File

@ -1,6 +1,5 @@
import type { FC } from 'react'
import { useMemo, useRef, useState } from 'react'
import { useMount } from 'ahooks'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { isEqual } from 'lodash-es'
import { RiCloseLine } from '@remixicon/react'
@ -21,10 +20,10 @@ import PermissionSelector from '@/app/components/datasets/settings/permission-se
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fetchMembers } from '@/service/common'
import type { Member } from '@/models/common'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { useDocLink } from '@/context/i18n'
import { useMembers } from '@/service/use-common'
import { checkShowMultiModalTip } from '@/app/components/datasets/settings/utils'
import { RetrievalChangeTip, RetrievalSection } from './retrieval-section'
@ -63,6 +62,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(localeCurrentDataset?.external_retrieval_model.score_threshold_enabled ?? false)
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(currentDataset.partial_member_list || [])
const [memberList, setMemberList] = useState<Member[]>([])
const { data: membersData } = useMembers()
const [indexMethod, setIndexMethod] = useState(currentDataset.indexing_technique)
const [retrievalConfig, setRetrievalConfig] = useState(localeCurrentDataset?.retrieval_model_dict as RetrievalConfig)
@ -160,17 +160,12 @@ const SettingsModal: FC<SettingsModalProps> = ({
}
}
const getMembers = async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts)
useEffect(() => {
if (!membersData?.accounts)
setMemberList([])
else
setMemberList(accounts)
}
useMount(() => {
getMembers()
})
setMemberList(membersData.accounts)
}, [membersData])
const showMultiModalTip = useMemo(() => {
return checkShowMultiModalTip({

View File

@ -1,7 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr'
import { basePath } from '@/utils/var'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
@ -72,7 +71,7 @@ import type { Features as FeaturesData, FileUpload } from '@/app/components/base
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
import { fetchFileUploadConfig } from '@/service/common'
import { useFileUploadConfig } from '@/service/use-common'
import {
correctModelProvider,
correctToolProvider,
@ -101,7 +100,7 @@ const Configuration: FC = () => {
showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal,
setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal,
})))
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
const { data: fileUploadConfigResponse } = useFileUploadConfig()
const latestPublishedAt = useMemo(() => appDetail?.model_config?.updated_at, [appDetail])
const [formattingChanged, setFormattingChanged] = useState(false)

View File

@ -1,6 +1,5 @@
import type { FC } from 'react'
import { useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation'
@ -9,7 +8,6 @@ import Button from '@/app/components/base/button'
import EmojiPicker from '@/app/components/base/emoji-picker'
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import { fetchCodeBasedExtensionList } from '@/service/common'
import { SimpleSelect } from '@/app/components/base/select'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
@ -21,6 +19,7 @@ import { useToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { noop } from 'lodash-es'
import { useDocLink } from '@/context/i18n'
import { useCodeBasedExtensions } from '@/service/use-common'
const systemTypes = ['api']
type ExternalDataToolModalProps = {
@ -46,10 +45,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
const { locale } = useContext(I18n)
const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' })
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=external_data_tool',
fetchCodeBasedExtensionList,
)
const { data: codeBasedExtensionList } = useCodeBasedExtensions('external_data_tool')
const providers: Provider[] = [
{

View File

@ -1,6 +1,5 @@
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { produce } from 'immer'
import { useContext } from 'use-context-selector'
import { RiEqualizer2Line } from '@remixicon/react'
@ -10,9 +9,9 @@ import Button from '@/app/components/base/button'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
import { fetchCodeBasedExtensionList } from '@/service/common'
import { useModalContext } from '@/context/modal-context'
import I18n from '@/context/i18n'
import { useCodeBasedExtensions } from '@/service/use-common'
type Props = {
disabled?: boolean
@ -28,10 +27,7 @@ const Moderation = ({
const { locale } = useContext(I18n)
const featuresStore = useFeaturesStore()
const moderation = useFeatures(s => s.features.moderation)
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=moderation',
fetchCodeBasedExtensionList,
)
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
const [isHovering, setIsHovering] = useState(false)
const handleOpenModerationSettingModal = () => {

View File

@ -1,6 +1,5 @@
import type { ChangeEvent, FC } from 'react'
import { useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
@ -13,10 +12,6 @@ import Divider from '@/app/components/base/divider'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
import { useToastContext } from '@/app/components/base/toast'
import {
fetchCodeBasedExtensionList,
fetchModelProviders,
} from '@/service/common'
import type { CodeBasedExtensionItem } from '@/models/common'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
@ -27,6 +22,7 @@ import { cn } from '@/utils/classnames'
import { noop } from 'lodash-es'
import { useDocLink } from '@/context/i18n'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useCodeBasedExtensions, useModelProviders } from '@/service/use-common'
const systemTypes = ['openai_moderation', 'keywords', 'api']
@ -51,21 +47,18 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
const docLink = useDocLink()
const { notify } = useToastContext()
const { locale } = useContext(I18n)
const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
const { setShowAccountSettingModal } = useModalContext()
const handleOpenSettingsModal = () => {
setShowAccountSettingModal({
payload: ACCOUNT_SETTING_TAB.PROVIDER,
onCancelCallback: () => {
mutate()
refetchModelProviders()
},
})
}
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=moderation',
fetchCodeBasedExtensionList,
)
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
const openaiProvider = modelProviders?.data.find(item => item.provider === 'langgenius/openai/openai')
const systemOpenaiProviderEnabled = openaiProvider?.system_configuration.enabled
const systemOpenaiProviderQuota = systemOpenaiProviderEnabled ? openaiProvider?.system_configuration.quota_configurations.find(item => item.quota_type === openaiProvider.system_configuration.current_quota_type) : undefined

View File

@ -1,6 +1,5 @@
'use client'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useMount } from 'ahooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PermissionSelector from '../permission-selector'
import IndexMethod from '../index-method'
@ -23,7 +22,6 @@ import ModelSelector from '@/app/components/header/account-setting/model-provide
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fetchMembers } from '@/service/common'
import type { Member } from '@/models/common'
import AppIcon from '@/app/components/base/app-icon'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
@ -34,6 +32,7 @@ import Toast from '@/app/components/base/toast'
import { RiAlertFill } from '@remixicon/react'
import { useDocLink } from '@/context/i18n'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import { useMembers } from '@/service/use-common'
import { checkShowMultiModalTip } from '../utils'
const rowClass = 'flex gap-x-1'
@ -79,16 +78,9 @@ const Form = () => {
)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: membersData } = useMembers()
const previousAppIcon = useRef(DEFAULT_APP_ICON)
const getMembers = async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts)
setMemberList([])
else
setMemberList(accounts)
}
const handleOpenAppIconPicker = useCallback(() => {
setShowAppIconPicker(true)
previousAppIcon.current = iconInfo
@ -119,9 +111,12 @@ const Form = () => {
setScoreThresholdEnabled(data.score_threshold_enabled)
}, [])
useMount(() => {
getMembers()
})
useEffect(() => {
if (!membersData?.accounts)
setMemberList([])
else
setMemberList(membersData.accounts)
}, [membersData])
const invalidDatasetList = useInvalidDatasetList()
const handleSave = async () => {

View File

@ -5,10 +5,10 @@ import { useRouter } from 'next/navigation'
import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
import { fetchMembers } from '@/service/common'
import type { InstalledApp } from '@/models/explore'
import { useTranslation } from 'react-i18next'
import useDocumentTitle from '@/hooks/use-document-title'
import { useMembers } from '@/service/use-common'
export type IExploreProps = {
children: React.ReactNode
@ -24,18 +24,16 @@ const Explore: FC<IExploreProps> = ({
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
const [isFetchingInstalledApps, setIsFetchingInstalledApps] = useState(false)
const { t } = useTranslation()
const { data: membersData } = useMembers()
useDocumentTitle(t('common.menus.explore'))
useEffect(() => {
(async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts)
return
const currUser = accounts.find(account => account.id === userProfile.id)
setHasEditPermission(currUser?.role !== 'normal')
})()
}, [])
if (!membersData?.accounts)
return
const currUser = membersData.accounts.find(account => account.id === userProfile.id)
setHasEditPermission(currUser?.role !== 'normal')
}, [membersData, userProfile.id])
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)

View File

@ -1,11 +1,10 @@
'use client'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import Link from 'next/link'
import s from './index.module.css'
import { cn } from '@/utils/classnames'
import { fetchAccountIntegrates } from '@/service/common'
import { useAccountIntegrates } from '@/service/use-common'
const titleClassName = `
mb-2 text-sm font-medium text-gray-900
@ -25,33 +24,38 @@ export default function IntegrationsPage() {
},
}
const { data } = useSWR({ url: '/account/integrates' }, fetchAccountIntegrates)
const integrates = data?.data?.length ? data.data : []
const { data } = useAccountIntegrates()
const integrates = data?.data ?? []
return (
<>
<div className='mb-8'>
<div className={titleClassName}>{t('common.integrations.connected')}</div>
{
integrates.map(integrate => (
<div key={integrate.provider} className='mb-2 flex items-center rounded-lg border-[0.5px] border-gray-200 bg-gray-50 px-3 py-2'>
<div className={cn('mr-3 h-8 w-8 rounded-lg border border-gray-100 bg-white', s[`${integrate.provider}-icon`])} />
<div className='grow'>
<div className='text-sm font-medium leading-[21px] text-gray-800'>{integrateMap[integrate.provider].name}</div>
<div className='text-xs font-normal leading-[18px] text-gray-500'>{integrateMap[integrate.provider].description}</div>
integrates.map((integrate) => {
const info = integrateMap[integrate.provider]
if (!info)
return null
return (
<div key={integrate.provider} className='mb-2 flex items-center rounded-lg border-[0.5px] border-gray-200 bg-gray-50 px-3 py-2'>
<div className={cn('mr-3 h-8 w-8 rounded-lg border border-gray-100 bg-white', s[`${integrate.provider}-icon`])} />
<div className='grow'>
<div className='text-sm font-medium leading-[21px] text-gray-800'>{info.name}</div>
<div className='text-xs font-normal leading-[18px] text-gray-500'>{info.description}</div>
</div>
{
!integrate.is_bound && (
<Link
className='flex h-8 cursor-pointer items-center rounded-lg border border-gray-200 bg-white px-[7px] text-xs font-medium text-gray-700'
href={integrate.link}
target='_blank' rel='noopener noreferrer'>
{t('common.integrations.connect')}
</Link>
)
}
</div>
{
!integrate.is_bound && (
<Link
className='flex h-8 cursor-pointer items-center rounded-lg border border-gray-200 bg-white px-[7px] text-xs font-medium text-gray-700'
href={integrate.link}
target='_blank' rel='noopener noreferrer'>
{t('common.integrations.connect')}
</Link>
)
}
</div>
))
)
})
}
</div>
{/* <div className='mb-8'>

View File

@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import {
RiAddLine,
} from '@remixicon/react'
@ -7,15 +6,12 @@ import Item from './item'
import Empty from './empty'
import Button from '@/app/components/base/button'
import { useModalContext } from '@/context/modal-context'
import { fetchApiBasedExtensionList } from '@/service/common'
import { useApiBasedExtensions } from '@/service/use-common'
const ApiBasedExtensionPage = () => {
const { t } = useTranslation()
const { setShowApiBasedExtensionModal } = useModalContext()
const { data, mutate, isLoading } = useSWR(
'/api-based-extension',
fetchApiBasedExtensionList,
)
const { data, refetch: mutate, isPending: isLoading } = useApiBasedExtensions()
const handleOpenApiBasedExtensionModal = () => {
setShowApiBasedExtensionModal({

View File

@ -1,6 +1,5 @@
import type { FC } from 'react'
import { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
@ -15,8 +14,8 @@ import {
ArrowUpRight,
} from '@/app/components/base/icons/src/vender/line/arrows'
import { useModalContext } from '@/context/modal-context'
import { fetchApiBasedExtensionList } from '@/service/common'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useApiBasedExtensions } from '@/service/use-common'
type ApiBasedExtensionSelectorProps = {
value: string
@ -33,10 +32,7 @@ const ApiBasedExtensionSelector: FC<ApiBasedExtensionSelectorProps> = ({
setShowAccountSettingModal,
setShowApiBasedExtensionModal,
} = useModalContext()
const { data, mutate } = useSWR(
'/api-based-extension',
fetchApiBasedExtensionList,
)
const { data, refetch: mutate } = useApiBasedExtensions()
const handleSelect = (id: string) => {
onChange(id)
setOpen(false)

View File

@ -1,16 +1,15 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import useSWR from 'swr'
import Panel from '../panel'
import { DataSourceType } from '../panel/types'
import type { DataSourceNotion as TDataSourceNotion } from '@/models/common'
import { useAppContext } from '@/context/app-context'
import { fetchNotionConnection } from '@/service/common'
import NotionIcon from '@/app/components/base/notion-icon'
import { noop } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { useDataSourceIntegrates, useNotionConnection } from '@/service/use-common'
const Icon: FC<{
src: string
@ -26,7 +25,7 @@ const Icon: FC<{
)
}
type Props = {
workspaces: TDataSourceNotion[]
workspaces?: TDataSourceNotion[]
}
const DataSourceNotion: FC<Props> = ({
@ -34,10 +33,14 @@ const DataSourceNotion: FC<Props> = ({
}) => {
const { isCurrentWorkspaceManager } = useAppContext()
const [canConnectNotion, setCanConnectNotion] = useState(false)
const { data } = useSWR(canConnectNotion ? '/oauth/data-source/notion' : null, fetchNotionConnection)
const { data: integrates } = useDataSourceIntegrates({
initialData: workspaces ? { data: workspaces } : undefined,
})
const { data } = useNotionConnection(canConnectNotion)
const { t } = useTranslation()
const connected = !!workspaces.length
const resolvedWorkspaces = integrates?.data ?? []
const connected = !!resolvedWorkspaces.length
const handleConnectNotion = () => {
if (!isCurrentWorkspaceManager)
@ -74,7 +77,7 @@ const DataSourceNotion: FC<Props> = ({
onConfigure={handleConnectNotion}
readOnly={!isCurrentWorkspaceManager}
isSupportList
configuredList={workspaces.map(workspace => ({
configuredList={resolvedWorkspaces.map(workspace => ({
id: workspace.id,
logo: ({ className }: { className: string }) => (
<Icon

View File

@ -1,7 +1,6 @@
'use client'
import { useTranslation } from 'react-i18next'
import { Fragment } from 'react'
import { useSWRConfig } from 'swr'
import {
RiDeleteBinLine,
RiLoopLeftLine,
@ -10,6 +9,7 @@ import {
} from '@remixicon/react'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common'
import { useInvalidDataSourceIntegrates } from '@/service/use-common'
import Toast from '@/app/components/base/toast'
import { cn } from '@/utils/classnames'
@ -25,14 +25,14 @@ export default function Operate({
onAuthAgain,
}: OperateProps) {
const { t } = useTranslation()
const { mutate } = useSWRConfig()
const invalidateDataSourceIntegrates = useInvalidDataSourceIntegrates()
const updateIntegrates = () => {
Toast.notify({
type: 'success',
message: t('common.api.success'),
})
mutate({ url: 'data-source/integrates' })
invalidateDataSourceIntegrates()
}
const handleSync = async () => {
await syncDataSourceNotion({ url: `/oauth/data-source/notion/${payload.id}/sync` })

View File

@ -1,6 +1,5 @@
'use client'
import { useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { RiUserAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
@ -10,7 +9,6 @@ import EditWorkspaceModal from './edit-workspace-modal'
import TransferOwnershipModal from './transfer-ownership-modal'
import Operation from './operation'
import TransferOwnership from './operation/transfer-ownership'
import { fetchMembers } from '@/service/common'
import I18n from '@/context/i18n'
import { useAppContext } from '@/context/app-context'
import Avatar from '@/app/components/base/avatar'
@ -26,6 +24,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { RiPencilLine } from '@remixicon/react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useMembers } from '@/service/use-common'
const MembersPage = () => {
const { t } = useTranslation()
@ -39,13 +38,7 @@ const MembersPage = () => {
const { locale } = useContext(I18n)
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
const { data, mutate } = useSWR(
{
url: '/workspaces/current/members',
params: {},
},
fetchMembers,
)
const { data, refetch } = useMembers()
const { systemFeatures } = useGlobalPublicStore()
const { formatTimeFromNow } = useFormatTimeFromNow()
const [inviteModalVisible, setInviteModalVisible] = useState(false)
@ -140,7 +133,7 @@ const MembersPage = () => {
<div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
)}
{isCurrentWorkspaceOwner && account.role !== 'owner' && (
<Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} />
<Operation member={account} operatorRole={currentWorkspace.role} onOperate={refetch} />
)}
{!isCurrentWorkspaceOwner && (
<div className='system-sm-regular px-3 text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
@ -160,7 +153,7 @@ const MembersPage = () => {
onSend={(invitationResults) => {
setInvitedModalVisible(true)
setInvitationResults(invitationResults)
mutate()
refetch()
}}
/>
)

View File

@ -2,15 +2,14 @@
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import Avatar from '@/app/components/base/avatar'
import Input from '@/app/components/base/input'
import { fetchMembers } from '@/service/common'
import { cn } from '@/utils/classnames'
import { useMembers } from '@/service/use-common'
type Props = {
value?: any
@ -27,13 +26,7 @@ const MemberSelector: FC<Props> = ({
const [open, setOpen] = useState(false)
const [searchValue, setSearchValue] = useState('')
const { data } = useSWR(
{
url: '/workspaces/current/members',
params: {},
},
fetchMembers,
)
const { data } = useMembers()
const currentValue = useMemo(() => {
if (!data?.accounts) return null

View File

@ -3,15 +3,21 @@ import { useLanguage } from './hooks'
import { useContext } from 'use-context-selector'
import { after } from 'node:test'
jest.mock('swr', () => ({
__esModule: true,
default: jest.fn(), // mock useSWR
useSWRConfig: jest.fn(),
jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(),
useQueryClient: jest.fn(() => ({
invalidateQueries: jest.fn(),
})),
}))
// mock use-context-selector
jest.mock('use-context-selector', () => ({
useContext: jest.fn(),
createContext: () => ({
Provider: ({ children }: any) => children,
Consumer: ({ children }: any) => children(null),
}),
useContextSelector: jest.fn(),
}))
// mock service/common functions
@ -19,10 +25,15 @@ jest.mock('@/service/common', () => ({
fetchDefaultModal: jest.fn(),
fetchModelList: jest.fn(),
fetchModelProviderCredentials: jest.fn(),
fetchModelProviders: jest.fn(),
getPayUrl: jest.fn(),
}))
jest.mock('@/service/use-common', () => ({
commonQueryKeys: {
modelProviders: ['common', 'model-providers'],
},
}))
// mock context hooks
jest.mock('@/context/i18n', () => ({
__esModule: true,

View File

@ -4,7 +4,7 @@ import {
useMemo,
useState,
} from 'react'
import useSWR, { useSWRConfig } from 'swr'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useContext } from 'use-context-selector'
import type {
Credential,
@ -27,9 +27,9 @@ import {
fetchDefaultModal,
fetchModelList,
fetchModelProviderCredentials,
fetchModelProviders,
getPayUrl,
} from '@/service/common'
import { commonQueryKeys } from '@/service/use-common'
import { useProviderContext } from '@/context/provider-context'
import {
useMarketplacePlugins,
@ -81,17 +81,23 @@ export const useProviderCredentialsAndLoadBalancing = (
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
credentialId?: string,
) => {
const { data: predefinedFormSchemasValue, mutate: mutatePredefined, isLoading: isPredefinedLoading } = useSWR(
(configurationMethod === ConfigurationMethodEnum.predefinedModel && configured && credentialId)
? `/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`
: null,
fetchModelProviderCredentials,
const queryClient = useQueryClient()
const predefinedEnabled = configurationMethod === ConfigurationMethodEnum.predefinedModel && configured && !!credentialId
const customEnabled = configurationMethod === ConfigurationMethodEnum.customizableModel && !!currentCustomConfigurationModelFixedFields && !!credentialId
const { data: predefinedFormSchemasValue, isPending: isPredefinedLoading } = useQuery(
{
queryKey: ['model-providers', 'credentials', provider, credentialId],
queryFn: () => fetchModelProviderCredentials(`/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`),
enabled: predefinedEnabled,
},
)
const { data: customFormSchemasValue, mutate: mutateCustomized, isLoading: isCustomizedLoading } = useSWR(
(configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields && credentialId)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}${credentialId ? `&credential_id=${credentialId}` : ''}`
: null,
fetchModelProviderCredentials,
const { data: customFormSchemasValue, isPending: isCustomizedLoading } = useQuery(
{
queryKey: ['model-providers', 'models', 'credentials', provider, currentCustomConfigurationModelFixedFields?.__model_type, currentCustomConfigurationModelFixedFields?.__model_name, credentialId],
queryFn: () => fetchModelProviderCredentials(`/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}${credentialId ? `&credential_id=${credentialId}` : ''}`),
enabled: customEnabled,
},
)
const credentials = useMemo(() => {
@ -112,9 +118,11 @@ export const useProviderCredentialsAndLoadBalancing = (
])
const mutate = useMemo(() => () => {
mutatePredefined()
mutateCustomized()
}, [mutateCustomized, mutatePredefined])
if (predefinedEnabled)
queryClient.invalidateQueries({ queryKey: ['model-providers', 'credentials', provider, credentialId] })
if (customEnabled)
queryClient.invalidateQueries({ queryKey: ['model-providers', 'models', 'credentials', provider, currentCustomConfigurationModelFixedFields?.__model_type, currentCustomConfigurationModelFixedFields?.__model_name, credentialId] })
}, [customEnabled, credentialId, currentCustomConfigurationModelFixedFields?.__model_name, currentCustomConfigurationModelFixedFields?.__model_type, predefinedEnabled, provider, queryClient])
return {
credentials,
@ -129,22 +137,28 @@ export const useProviderCredentialsAndLoadBalancing = (
}
export const useModelList = (type: ModelTypeEnum) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/models/model-types/${type}`, fetchModelList)
const { data, refetch, isPending } = useQuery({
queryKey: commonQueryKeys.modelList(type),
queryFn: () => fetchModelList(`/workspaces/current/models/model-types/${type}`),
})
return {
data: data?.data || [],
mutate,
isLoading,
mutate: refetch,
isLoading: isPending,
}
}
export const useDefaultModel = (type: ModelTypeEnum) => {
const { data, mutate, isLoading } = useSWR(`/workspaces/current/default-model?model_type=${type}`, fetchDefaultModal)
const { data, refetch, isPending } = useQuery({
queryKey: commonQueryKeys.defaultModel(type),
queryFn: () => fetchDefaultModal(`/workspaces/current/default-model?model_type=${type}`),
})
return {
data: data?.data,
mutate,
isLoading,
mutate: refetch,
isLoading: isPending,
}
}
@ -200,11 +214,11 @@ export const useModelListAndDefaultModelAndCurrentProviderAndModel = (type: Mode
}
export const useUpdateModelList = () => {
const { mutate } = useSWRConfig()
const queryClient = useQueryClient()
const updateModelList = useCallback((type: ModelTypeEnum) => {
mutate(`/workspaces/current/models/model-types/${type}`)
}, [mutate])
queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelList(type) })
}, [queryClient])
return updateModelList
}
@ -230,22 +244,12 @@ export const useAnthropicBuyQuota = () => {
return handleGetPayUrl
}
export const useModelProviders = () => {
const { data: providersData, mutate, isLoading } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
return {
data: providersData?.data || [],
mutate,
isLoading,
}
}
export const useUpdateModelProviders = () => {
const { mutate } = useSWRConfig()
const queryClient = useQueryClient()
const updateModelProviders = useCallback(() => {
mutate('/workspaces/current/model-providers')
}, [mutate])
queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviders })
}, [queryClient])
return updateModelProviders
}

View File

@ -3,7 +3,6 @@ import type {
ReactNode,
} from 'react'
import { useMemo, useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import type {
DefaultModel,
@ -26,11 +25,11 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { fetchModelParameterRules } from '@/service/common'
import Loading from '@/app/components/base/loading'
import { useProviderContext } from '@/context/provider-context'
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
import { useModelParameterRules } from '@/service/use-common'
export type ModelParameterModalProps = {
popupClassName?: string
@ -69,7 +68,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
const { t } = useTranslation()
const { isAPIKeySet } = useProviderContext()
const [open, setOpen] = useState(false)
const { data: parameterRulesData, isLoading } = useSWR((provider && modelId) ? `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` : null, fetchModelParameterRules)
const { data: parameterRulesData, isPending: isLoading } = useModelParameterRules(provider, modelId)
const {
currentProvider,
currentModel,

View File

@ -5,6 +5,7 @@ import type { ModelItem, ModelProvider } from '../declarations'
import { ModelStatusEnum } from '../declarations'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import { useUpdateModelList } from '../hooks'
import { cn } from '@/utils/classnames'
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import Switch from '@/app/components/base/switch'
@ -20,21 +21,25 @@ export type ModelListItemProps = {
model: ModelItem
provider: ModelProvider
isConfigurable: boolean
onChange?: (provider: string) => void
onModifyLoadBalancing?: (model: ModelItem) => void
}
const ModelListItem = ({ model, provider, isConfigurable, onModifyLoadBalancing }: ModelListItemProps) => {
const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoadBalancing }: ModelListItemProps) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const { isCurrentWorkspaceManager } = useAppContext()
const updateModelList = useUpdateModelList()
const toggleModelEnablingStatus = useCallback(async (enabled: boolean) => {
if (enabled)
await enableModel(`/workspaces/current/model-providers/${provider.provider}/models/enable`, { model: model.model, model_type: model.model_type })
else
await disableModel(`/workspaces/current/model-providers/${provider.provider}/models/disable`, { model: model.model, model_type: model.model_type })
}, [model.model, model.model_type, provider.provider])
updateModelList(model.model_type)
onChange?.(provider.provider)
}, [model.model, model.model_type, onChange, provider.provider, updateModelList])
const { run: debouncedToggleModelEnablingStatus } = useDebounceFn(toggleModelEnablingStatus, { wait: 500 })

View File

@ -91,6 +91,7 @@ const ModelList: FC<ModelListProps> = ({
model,
provider,
isConfigurable,
onChange,
onModifyLoadBalancing,
}}
/>

View File

@ -1,14 +1,13 @@
import useSWR from 'swr'
import { LockClosedIcon } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import SerpapiPlugin from './SerpapiPlugin'
import { fetchPluginProviders } from '@/service/common'
import type { PluginProvider } from '@/models/common'
import { usePluginProviders } from '@/service/use-common'
const PluginPage = () => {
const { t } = useTranslation()
const { data: plugins, mutate } = useSWR('/workspaces/current/tool-providers', fetchPluginProviders)
const { data: plugins, refetch: mutate } = usePluginProviders()
const Plugin_MAP: Record<string, (plugin: PluginProvider) => React.JSX.Element> = {
serpapi: (plugin: PluginProvider) => <SerpapiPlugin key='serpapi' plugin={plugin} onUpdate={() => mutate()} />,

View File

@ -1,5 +1,4 @@
import React, { useMemo } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import PresetsParameter from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter'
import ParameterItem from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item'
@ -9,9 +8,9 @@ import type {
ModelParameterRule,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ParameterValue } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item'
import { fetchModelParameterRules } from '@/service/common'
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config'
import { cn } from '@/utils/classnames'
import { useModelParameterRules } from '@/service/use-common'
type Props = {
isAdvancedMode: boolean
@ -29,11 +28,7 @@ const LLMParamsPanel = ({
onCompletionParamsChange,
}: Props) => {
const { t } = useTranslation()
const { data: parameterRulesData, isLoading } = useSWR(
(provider && modelId)
? `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}`
: null, fetchModelParameterRules,
)
const { data: parameterRulesData, isPending: isLoading } = useModelParameterRules(provider, modelId)
const parameterRules: ModelParameterRule[] = useMemo(() => {
return parameterRulesData?.data || []

View File

@ -1,7 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import useSWR from 'swr'
import { produce } from 'immer'
import { useTranslation } from 'react-i18next'
import type { UploadFileSetting } from '../../../types'
@ -11,9 +10,9 @@ import FileTypeItem from './file-type-item'
import InputNumberWithSlider from './input-number-with-slider'
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
import { TransferMethod } from '@/types/app'
import { fetchFileUploadConfig } from '@/service/common'
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
import { formatFileSize } from '@/utils/format'
import { useFileUploadConfig } from '@/service/use-common'
type Props = {
payload: UploadFileSetting
@ -38,7 +37,7 @@ const FileUploadSetting: FC<Props> = ({
allowed_file_types,
allowed_file_extensions,
} = payload
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
const { data: fileUploadConfigResponse } = useFileUploadConfig()
const {
imgSizeLimit,
docSizeLimit,

View File

@ -1,30 +1,28 @@
'use client'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useSearchParams } from 'next/navigation'
import { basePath } from '@/utils/var'
import { cn } from '@/utils/classnames'
import { CheckCircleIcon } from '@heroicons/react/24/solid'
import Input from '../components/base/input'
import Button from '@/app/components/base/button'
import { changePasswordWithToken, verifyForgotPasswordToken } from '@/service/common'
import { changePasswordWithToken } from '@/service/common'
import Toast from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import { validPassword } from '@/config'
import { useVerifyForgotPasswordToken } from '@/service/use-common'
const ChangePasswordForm = () => {
const { t } = useTranslation()
const searchParams = useSearchParams()
const token = searchParams.get('token')
const isTokenMissing = !token
const verifyTokenParams = {
url: '/forgot-password/validity',
body: { token },
}
const { data: verifyTokenRes, mutate: revalidateToken } = useSWR(verifyTokenParams, verifyForgotPasswordToken, {
revalidateOnFocus: false,
})
const {
data: verifyTokenRes,
refetch: revalidateToken,
} = useVerifyForgotPasswordToken(token)
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
@ -82,8 +80,8 @@ const ChangePasswordForm = () => {
'md:px-[108px]',
)
}>
{!verifyTokenRes && <Loading />}
{verifyTokenRes && !verifyTokenRes.is_valid && (
{!isTokenMissing && !verifyTokenRes && <Loading />}
{(isTokenMissing || (verifyTokenRes && !verifyTokenRes.is_valid)) && (
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-[20px] border border-divider-regular bg-components-option-card-option-bg p-5 text-[40px] font-bold shadow-lg">🤷</div>

View File

@ -5,7 +5,6 @@ import { useCallback, useState } from 'react'
import Link from 'next/link'
import { useContext } from 'use-context-selector'
import { useRouter, useSearchParams } from 'next/navigation'
import useSWR from 'swr'
import { RiAccountCircleLine } from '@remixicon/react'
import Input from '@/app/components/base/input'
import { SimpleSelect } from '@/app/components/base/select'
@ -13,12 +12,13 @@ import Button from '@/app/components/base/button'
import { timezones } from '@/utils/timezone'
import { LanguagesSupported, languages } from '@/i18n-config/language'
import I18n from '@/context/i18n'
import { activateMember, invitationCheck } from '@/service/common'
import { activateMember } from '@/service/common'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { noop } from 'lodash-es'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
import { useInvitationCheck } from '@/service/use-common'
export default function InviteSettingsPage() {
const { t } = useTranslation()
@ -38,9 +38,7 @@ export default function InviteSettingsPage() {
token,
},
}
const { data: checkRes, mutate: recheck } = useSWR(checkParams, invitationCheck, {
revalidateOnFocus: false,
})
const { data: checkRes, refetch: recheck } = useInvitationCheck(checkParams.params, !!token)
const handleActivate = useCallback(async () => {
try {

View File

@ -1,8 +1,7 @@
'use client'
import React, { type Reducer, useEffect, useReducer } from 'react'
import React, { type Reducer, useReducer } from 'react'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import useSWR from 'swr'
import { useRouter, useSearchParams } from 'next/navigation'
import Input from '../components/base/input'
import Button from '@/app/components/base/button'
@ -10,12 +9,11 @@ import Tooltip from '@/app/components/base/tooltip'
import { SimpleSelect } from '@/app/components/base/select'
import { timezones } from '@/utils/timezone'
import { LanguagesSupported, languages } from '@/i18n-config/language'
import { oneMoreStep } from '@/service/common'
import Toast from '@/app/components/base/toast'
import { useDocLink } from '@/context/i18n'
import { useOneMoreStep } from '@/service/use-common'
type IState = {
formState: 'processing' | 'error' | 'success' | 'initial'
invitation_code: string
interface_language: string
timezone: string
@ -26,7 +24,6 @@ type IAction
| { type: 'invitation_code', value: string }
| { type: 'interface_language', value: string }
| { type: 'timezone', value: string }
| { type: 'formState', value: 'processing' }
const reducer: Reducer<IState, IAction> = (state: IState, action: IAction) => {
switch (action.type) {
@ -36,11 +33,8 @@ const reducer: Reducer<IState, IAction> = (state: IState, action: IAction) => {
return { ...state, interface_language: action.value }
case 'timezone':
return { ...state, timezone: action.value }
case 'formState':
return { ...state, formState: action.value }
case 'failed':
return {
formState: 'initial',
invitation_code: '',
interface_language: 'en-US',
timezone: 'Asia/Shanghai',
@ -57,30 +51,29 @@ const OneMoreStep = () => {
const searchParams = useSearchParams()
const [state, dispatch] = useReducer(reducer, {
formState: 'initial',
invitation_code: searchParams.get('invitation_code') || '',
interface_language: 'en-US',
timezone: 'Asia/Shanghai',
})
const { data, error } = useSWR(state.formState === 'processing'
? {
url: '/account/init',
body: {
const { mutateAsync: submitOneMoreStep, isPending } = useOneMoreStep()
const handleSubmit = async () => {
if (isPending)
return
try {
await submitOneMoreStep({
invitation_code: state.invitation_code,
interface_language: state.interface_language,
timezone: state.timezone,
},
})
router.push('/apps')
}
: null, oneMoreStep)
useEffect(() => {
if (error && error.status === 400) {
Toast.notify({ type: 'error', message: t('login.invalidInvitationCode') })
catch (error: any) {
if (error && error.status === 400)
Toast.notify({ type: 'error', message: t('login.invalidInvitationCode') })
dispatch({ type: 'failed', payload: null })
}
if (data)
router.push('/apps')
}, [data, error])
}
return (
<>
@ -151,10 +144,8 @@ const OneMoreStep = () => {
<Button
variant='primary'
className='w-full'
disabled={state.formState === 'processing'}
onClick={() => {
dispatch({ type: 'formState', value: 'processing' })
}}
disabled={isPending}
onClick={handleSubmit}
>
{t('login.go')}
</Button>