From 079620714eca76101a97f87bd8330154d76f2349 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:34:14 +0800 Subject: [PATCH] refactor: migrate common service toward TanStack Query (#29009) --- web/app/activate/activateForm.tsx | 32 +- .../settings-modal/index.spec.tsx | 504 ++++++++++-------- .../dataset-config/settings-modal/index.tsx | 19 +- .../components/app/configuration/index.tsx | 5 +- .../tools/external-data-tool-modal.tsx | 8 +- .../new-feature-panel/moderation/index.tsx | 8 +- .../moderation/moderation-setting-modal.tsx | 15 +- .../datasets/settings/form/index.tsx | 23 +- web/app/components/explore/index.tsx | 16 +- .../Integrations-page/index.tsx | 48 +- .../api-based-extension-page/index.tsx | 8 +- .../api-based-extension-page/selector.tsx | 8 +- .../data-source-notion/index.tsx | 15 +- .../data-source-notion/operate/index.tsx | 6 +- .../account-setting/members-page/index.tsx | 15 +- .../member-selector.tsx | 11 +- .../model-provider-page/hooks.spec.ts | 21 +- .../model-provider-page/hooks.ts | 78 +-- .../model-parameter-modal/index.tsx | 5 +- .../provider-added-card/model-list-item.tsx | 9 +- .../provider-added-card/model-list.tsx | 1 + .../account-setting/plugin-page/index.tsx | 5 +- .../model-selector/llm-params-panel.tsx | 9 +- .../_base/components/file-upload-setting.tsx | 5 +- .../forgot-password/ChangePasswordForm.tsx | 20 +- web/app/signin/invite-settings/page.tsx | 8 +- web/app/signin/one-more-step.tsx | 43 +- web/context/app-context.tsx | 74 +-- web/context/provider-context.tsx | 24 +- web/context/workspace-context.tsx | 5 +- web/hooks/use-pay.tsx | 12 +- web/service/common.ts | 207 ++++--- web/service/use-common.ts | 251 ++++++++- 33 files changed, 885 insertions(+), 633 deletions(-) diff --git a/web/app/activate/activateForm.tsx b/web/app/activate/activateForm.tsx index 4789a579a7..11fc4866f3 100644 --- a/web/app/activate/activateForm.tsx +++ b/web/app/activate/activateForm.tsx @@ -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 (
({ 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 -const mockFetchMembers = fetchMembers as jest.MockedFunction +const mockUseMembers = useMembers as jest.MockedFunction const createRetrievalConfig = (overrides: Partial = {}): 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) 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' })) + }) }) }) }) diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 8c3e753b22..c191ff5d46 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -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 = ({ const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(localeCurrentDataset?.external_retrieval_model.score_threshold_enabled ?? false) const [selectedMemberIDs, setSelectedMemberIDs] = useState(currentDataset.partial_member_list || []) const [memberList, setMemberList] = useState([]) + 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 = ({ } } - 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({ diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 2537062e13..4da12319f2 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -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) diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index 990e679c79..6f177643ae 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -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 = ({ 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[] = [ { diff --git a/web/app/components/base/features/new-feature-panel/moderation/index.tsx b/web/app/components/base/features/new-feature-panel/moderation/index.tsx index b5bcbca474..abbde2bab9 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/index.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/index.tsx @@ -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 = () => { diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index 53f5362103..dd9c58c5ab 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -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 = ({ 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(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 diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index 5ca85925cc..24942e6249 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -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 () => { diff --git a/web/app/components/explore/index.tsx b/web/app/components/explore/index.tsx index e716de96f1..b9460f8135 100644 --- a/web/app/components/explore/index.tsx +++ b/web/app/components/explore/index.tsx @@ -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 = ({ const [installedApps, setInstalledApps] = useState([]) 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) diff --git a/web/app/components/header/account-setting/Integrations-page/index.tsx b/web/app/components/header/account-setting/Integrations-page/index.tsx index ae2efcf3d1..460cc2ed5a 100644 --- a/web/app/components/header/account-setting/Integrations-page/index.tsx +++ b/web/app/components/header/account-setting/Integrations-page/index.tsx @@ -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 ( <>
{t('common.integrations.connected')}
{ - integrates.map(integrate => ( -
-
-
-
{integrateMap[integrate.provider].name}
-
{integrateMap[integrate.provider].description}
+ integrates.map((integrate) => { + const info = integrateMap[integrate.provider] + if (!info) + return null + return ( +
+
+
+
{info.name}
+
{info.description}
+
+ { + !integrate.is_bound && ( + + {t('common.integrations.connect')} + + ) + }
- { - !integrate.is_bound && ( - - {t('common.integrations.connect')} - - ) - } -
- )) + ) + }) }
{/*
diff --git a/web/app/components/header/account-setting/api-based-extension-page/index.tsx b/web/app/components/header/account-setting/api-based-extension-page/index.tsx index d16c4f2ded..24dfce5b90 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/index.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/index.tsx @@ -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({ diff --git a/web/app/components/header/account-setting/api-based-extension-page/selector.tsx b/web/app/components/header/account-setting/api-based-extension-page/selector.tsx index 549b5e7910..9da3745f2f 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/selector.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/selector.tsx @@ -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 = ({ setShowAccountSettingModal, setShowApiBasedExtensionModal, } = useModalContext() - const { data, mutate } = useSWR( - '/api-based-extension', - fetchApiBasedExtensionList, - ) + const { data, refetch: mutate } = useApiBasedExtensions() const handleSelect = (id: string) => { onChange(id) setOpen(false) diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx index 065ef91eba..68fd52d0a4 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx @@ -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 = ({ @@ -34,10 +33,14 @@ const DataSourceNotion: FC = ({ }) => { 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 = ({ onConfigure={handleConnectNotion} readOnly={!isCurrentWorkspaceManager} isSupportList - configuredList={workspaces.map(workspace => ({ + configuredList={resolvedWorkspaces.map(workspace => ({ id: workspace.id, logo: ({ className }: { className: string }) => ( { 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` }) diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index 6f75372ed9..e951e5b85a 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -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 = () => {
{RoleMap[account.role] || RoleMap.normal}
)} {isCurrentWorkspaceOwner && account.role !== 'owner' && ( - + )} {!isCurrentWorkspaceOwner && (
{RoleMap[account.role] || RoleMap.normal}
@@ -160,7 +153,7 @@ const MembersPage = () => { onSend={(invitationResults) => { setInvitedModalVisible(true) setInvitationResults(invitationResults) - mutate() + refetch() }} /> ) diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx index 78988db071..70c8e300f0 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx @@ -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 = ({ 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 diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.spec.ts b/web/app/components/header/account-setting/model-provider-page/hooks.spec.ts index b7a56f7b60..e1f42aa56f 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.spec.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.spec.ts @@ -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, diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index 0ffd1df9de..ff5899f01c 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -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 } diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx index 016b8b0fd6..e7323c86e6 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx @@ -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 = ({ 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, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index 0282d36214..911485edf6 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -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 }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx index 2e008a0b35..1efa9628ac 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx @@ -91,6 +91,7 @@ const ModelList: FC = ({ model, provider, isConfigurable, + onChange, onModifyLoadBalancing, }} /> diff --git a/web/app/components/header/account-setting/plugin-page/index.tsx b/web/app/components/header/account-setting/plugin-page/index.tsx index bf404b05bb..5195ca9501 100644 --- a/web/app/components/header/account-setting/plugin-page/index.tsx +++ b/web/app/components/header/account-setting/plugin-page/index.tsx @@ -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 React.JSX.Element> = { serpapi: (plugin: PluginProvider) => mutate()} />, diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx index b05be5005a..e741ec1772 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx @@ -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 || [] diff --git a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx index 0dccf23e9e..ce41777471 100644 --- a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx @@ -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 = ({ allowed_file_types, allowed_file_extensions, } = payload - const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) + const { data: fileUploadConfigResponse } = useFileUploadConfig() const { imgSizeLimit, docSizeLimit, diff --git a/web/app/forgot-password/ChangePasswordForm.tsx b/web/app/forgot-password/ChangePasswordForm.tsx index b2424c85fc..66119ea691 100644 --- a/web/app/forgot-password/ChangePasswordForm.tsx +++ b/web/app/forgot-password/ChangePasswordForm.tsx @@ -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 && } - {verifyTokenRes && !verifyTokenRes.is_valid && ( + {!isTokenMissing && !verifyTokenRes && } + {(isTokenMissing || (verifyTokenRes && !verifyTokenRes.is_valid)) && (
🤷‍♂️
diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index cbd37f51f6..de8d6c60ea 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -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 { diff --git a/web/app/signin/one-more-step.tsx b/web/app/signin/one-more-step.tsx index 3293caa8f5..4b20f85681 100644 --- a/web/app/signin/one-more-step.tsx +++ b/web/app/signin/one-more-step.tsx @@ -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 = (state: IState, action: IAction) => { switch (action.type) { @@ -36,11 +33,8 @@ const reducer: Reducer = (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 = () => { diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index 426ef2217e..48d67c3611 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -1,10 +1,14 @@ 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' -import useSWR from 'swr' +import { useCallback, useEffect, useMemo } from 'react' import { createContext, useContext, useContextSelector } from 'use-context-selector' import type { FC, ReactNode } from 'react' -import { fetchCurrentWorkspace, fetchLangGeniusVersion, fetchUserProfile } from '@/service/common' +import { useQueryClient } from '@tanstack/react-query' +import { + useCurrentWorkspace, + useLangGeniusVersion, + useUserProfile, +} from '@/service/use-common' import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import MaintenanceNotice from '@/app/components/header/maintenance-notice' import { noop } from 'lodash-es' @@ -79,48 +83,44 @@ export type AppContextProviderProps = { } export const AppContextProvider: FC = ({ children }) => { + const queryClient = useQueryClient() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const { data: userProfileResponse, mutate: mutateUserProfile, error: userProfileError } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile) - const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace) + const { data: userProfileResp } = useUserProfile() + const { data: currentWorkspaceResp, isPending: isLoadingCurrentWorkspace } = useCurrentWorkspace() + const langGeniusVersionQuery = useLangGeniusVersion( + userProfileResp?.meta.currentVersion, + !systemFeatures.branding.enabled, + ) + + const userProfile = useMemo(() => userProfileResp?.profile || userProfilePlaceholder, [userProfileResp?.profile]) + const currentWorkspace = useMemo(() => currentWorkspaceResp || initialWorkspaceInfo, [currentWorkspaceResp]) + const langGeniusVersionInfo = useMemo(() => { + if (!userProfileResp?.meta?.currentVersion || !langGeniusVersionQuery.data) + return initialLangGeniusVersionInfo + + const current_version = userProfileResp.meta.currentVersion + const current_env = userProfileResp.meta.currentEnv || '' + const versionData = langGeniusVersionQuery.data + return { + ...versionData, + current_version, + latest_version: versionData.version, + current_env, + } + }, [langGeniusVersionQuery.data, userProfileResp?.meta]) - const [userProfile, setUserProfile] = useState(userProfilePlaceholder) - const [langGeniusVersionInfo, setLangGeniusVersionInfo] = useState(initialLangGeniusVersionInfo) - const [currentWorkspace, setCurrentWorkspace] = useState(initialWorkspaceInfo) const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role]) const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role]) const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role]) const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role]) - const updateUserProfileAndVersion = useCallback(async () => { - if (userProfileResponse && !userProfileResponse.bodyUsed) { - try { - const result = await userProfileResponse.json() - setUserProfile(result) - if (!systemFeatures.branding.enabled) { - const current_version = userProfileResponse.headers.get('x-version') - const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env') - const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } }) - setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env }) - } - } - catch (error) { - console.error('Failed to update user profile:', error) - if (userProfile.id === '') - setUserProfile(userProfilePlaceholder) - } - } - else if (userProfileError && userProfile.id === '') { - setUserProfile(userProfilePlaceholder) - } - }, [userProfileResponse, userProfileError, userProfile.id]) - useEffect(() => { - updateUserProfileAndVersion() - }, [updateUserProfileAndVersion, userProfileResponse]) + const mutateUserProfile = useCallback(() => { + queryClient.invalidateQueries({ queryKey: ['common', 'user-profile'] }) + }, [queryClient]) - useEffect(() => { - if (currentWorkspaceResponse) - setCurrentWorkspace(currentWorkspaceResponse) - }, [currentWorkspaceResponse]) + const mutateCurrentWorkspace = useCallback(() => { + queryClient.invalidateQueries({ queryKey: ['common', 'current-workspace'] }) + }, [queryClient]) // #region Zendesk conversation fields useEffect(() => { diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 70944d85f1..e1739853c6 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -1,15 +1,15 @@ 'use client' import { createContext, useContext, useContextSelector } from 'use-context-selector' -import useSWR from 'swr' import { useEffect, useState } from 'react' import dayjs from 'dayjs' import { useTranslation } from 'react-i18next' +import { useQueryClient } from '@tanstack/react-query' import { - fetchModelList, - fetchModelProviders, - fetchSupportRetrievalMethods, -} from '@/service/common' + useModelListByType, + useModelProviders, + useSupportRetrievalMethods, +} from '@/service/use-common' import { CurrentSystemQuotaTypeEnum, ModelStatusEnum, @@ -114,10 +114,10 @@ type ProviderContextProviderProps = { export const ProviderContextProvider = ({ children, }: ProviderContextProviderProps) => { - const { data: providersData, mutate: refreshModelProviders } = useSWR('/workspaces/current/model-providers', fetchModelProviders) - const fetchModelListUrlPrefix = '/workspaces/current/models/model-types/' - const { data: textGenerationModelList } = useSWR(`${fetchModelListUrlPrefix}${ModelTypeEnum.textGeneration}`, fetchModelList) - const { data: supportRetrievalMethods } = useSWR('/datasets/retrieval-setting', fetchSupportRetrievalMethods) + const queryClient = useQueryClient() + const { data: providersData } = useModelProviders() + const { data: textGenerationModelList } = useModelListByType(ModelTypeEnum.textGeneration) + const { data: supportRetrievalMethods } = useSupportRetrievalMethods() const [plan, setPlan] = useState(defaultPlan) const [isFetchedPlan, setIsFetchedPlan] = useState(false) @@ -139,6 +139,10 @@ export const ProviderContextProvider = ({ const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false) const [isAllowPublishAsCustomKnowledgePipelineTemplate, setIsAllowPublishAsCustomKnowledgePipelineTemplate] = useState(false) + const refreshModelProviders = () => { + queryClient.invalidateQueries({ queryKey: ['common', 'model-providers'] }) + } + const fetchPlan = async () => { try { const data = await fetchCurrentPlanInfo() @@ -226,7 +230,7 @@ export const ProviderContextProvider = ({ modelProviders: providersData?.data || [], refreshModelProviders, textGenerationModelList: textGenerationModelList?.data || [], - isAPIKeySet: !!textGenerationModelList?.data.some(model => model.status === ModelStatusEnum.active), + isAPIKeySet: !!textGenerationModelList?.data?.some(model => model.status === ModelStatusEnum.active), supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [], plan, isFetchedPlan, diff --git a/web/context/workspace-context.tsx b/web/context/workspace-context.tsx index 9350a959b4..da7dcf5a50 100644 --- a/web/context/workspace-context.tsx +++ b/web/context/workspace-context.tsx @@ -1,8 +1,7 @@ 'use client' import { createContext, useContext } from 'use-context-selector' -import useSWR from 'swr' -import { fetchWorkspaces } from '@/service/common' +import { useWorkspaces } from '@/service/use-common' import type { IWorkspace } from '@/models/common' export type WorkspacesContextValue = { @@ -20,7 +19,7 @@ type IWorkspaceProviderProps = { export const WorkspaceProvider = ({ children, }: IWorkspaceProviderProps) => { - const { data } = useSWR({ url: '/workspaces' }, fetchWorkspaces) + const { data } = useWorkspaces() return ( @@ -58,12 +55,7 @@ export const useCheckNotion = () => { const type = searchParams.get('type') const notionCode = searchParams.get('code') const notionError = searchParams.get('error') - const { data } = useSWR( - (canBinding && notionCode) - ? `/oauth/data-source/binding/notion?code=${notionCode}` - : null, - fetchDataSourceNotionBinding, - ) + const { data } = useNotionBinding(notionCode, canBinding) useEffect(() => { if (data) diff --git a/web/service/common.ts b/web/service/common.ts index 7a092a6a24..1793675bc5 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -1,4 +1,3 @@ -import type { Fetcher } from 'swr' import { del, get, patch, post, put } from './base' import type { AccountIntegrate, @@ -49,145 +48,145 @@ type LoginFail = { message: string } type LoginResponse = LoginSuccess | LoginFail -export const login: Fetcher }> = ({ url, body }) => { - return post(url, { body }) as Promise +export const login = ({ url, body }: { url: string; body: Record }): Promise => { + return post(url, { body }) } -export const webAppLogin: Fetcher }> = ({ url, body }) => { - return post(url, { body }, { isPublicAPI: true }) as Promise +export const webAppLogin = ({ url, body }: { url: string; body: Record }): Promise => { + return post(url, { body }, { isPublicAPI: true }) } -export const setup: Fetcher }> = ({ body }) => { +export const setup = ({ body }: { body: Record }): Promise => { return post('/setup', { body }) } -export const initValidate: Fetcher }> = ({ body }) => { +export const initValidate = ({ body }: { body: Record }): Promise => { return post('/init', { body }) } -export const fetchInitValidateStatus = () => { +export const fetchInitValidateStatus = (): Promise => { return get('/init') } -export const fetchSetupStatus = () => { +export const fetchSetupStatus = (): Promise => { return get('/setup') } -export const fetchUserProfile: Fetcher }> = ({ url, params }) => { +export const fetchUserProfile = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, params, { needAllResponseContent: true }) } -export const updateUserProfile: Fetcher }> = ({ url, body }) => { +export const updateUserProfile = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const fetchLangGeniusVersion: Fetcher }> = ({ url, params }) => { +export const fetchLangGeniusVersion = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const oauth: Fetcher }> = ({ url, params }) => { +export const oauth = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const oneMoreStep: Fetcher }> = ({ url, body }) => { +export const oneMoreStep = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const fetchMembers: Fetcher<{ accounts: Member[] | null }, { url: string; params: Record }> = ({ url, params }) => { +export const fetchMembers = ({ url, params }: { url: string; params: Record }): Promise<{ accounts: Member[] | null }> => { return get<{ accounts: Member[] | null }>(url, { params }) } -export const fetchProviders: Fetcher }> = ({ url, params }) => { +export const fetchProviders = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const validateProviderKey: Fetcher = ({ url, body }) => { +export const validateProviderKey = ({ url, body }: { url: string; body: { token: string } }): Promise => { return post(url, { body }) } -export const updateProviderAIKey: Fetcher = ({ url, body }) => { +export const updateProviderAIKey = ({ url, body }: { url: string; body: { token: string | ProviderAzureToken | ProviderAnthropicToken } }): Promise => { return post(url, { body }) } -export const fetchAccountIntegrates: Fetcher<{ data: AccountIntegrate[] | null }, { url: string; params: Record }> = ({ url, params }) => { +export const fetchAccountIntegrates = ({ url, params }: { url: string; params: Record }): Promise<{ data: AccountIntegrate[] | null }> => { return get<{ data: AccountIntegrate[] | null }>(url, { params }) } -export const inviteMember: Fetcher }> = ({ url, body }) => { +export const inviteMember = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const updateMemberRole: Fetcher }> = ({ url, body }) => { +export const updateMemberRole = ({ url, body }: { url: string; body: Record }): Promise => { return put(url, { body }) } -export const deleteMemberOrCancelInvitation: Fetcher = ({ url }) => { +export const deleteMemberOrCancelInvitation = ({ url }: { url: string }): Promise => { return del(url) } -export const sendOwnerEmail = (body: { language?: string }) => +export const sendOwnerEmail = (body: { language?: string }): Promise => post('/workspaces/current/members/send-owner-transfer-confirm-email', { body }) -export const verifyOwnerEmail = (body: { code: string; token: string }) => +export const verifyOwnerEmail = (body: { code: string; token: string }): Promise => post('/workspaces/current/members/owner-transfer-check', { body }) -export const ownershipTransfer = (memberID: string, body: { token: string }) => +export const ownershipTransfer = (memberID: string, body: { token: string }): Promise => post(`/workspaces/current/members/${memberID}/owner-transfer`, { body }) -export const fetchFilePreview: Fetcher<{ content: string }, { fileID: string }> = ({ fileID }) => { +export const fetchFilePreview = ({ fileID }: { fileID: string }): Promise<{ content: string }> => { return get<{ content: string }>(`/files/${fileID}/preview`) } -export const fetchCurrentWorkspace: Fetcher }> = ({ url, params }) => { +export const fetchCurrentWorkspace = ({ url, params }: { url: string; params: Record }): Promise => { return post(url, { body: params }) } -export const updateCurrentWorkspace: Fetcher }> = ({ url, body }) => { +export const updateCurrentWorkspace = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record }> = ({ url, params }) => { +export const fetchWorkspaces = ({ url, params }: { url: string; params: Record }): Promise<{ workspaces: IWorkspace[] }> => { return get<{ workspaces: IWorkspace[] }>(url, { params }) } -export const switchWorkspace: Fetcher }> = ({ url, body }) => { +export const switchWorkspace = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const updateWorkspaceInfo: Fetcher }> = ({ url, body }) => { +export const updateWorkspaceInfo = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const fetchDataSource: Fetcher<{ data: DataSourceNotion[] }, { url: string }> = ({ url }) => { +export const fetchDataSource = ({ url }: { url: string }): Promise<{ data: DataSourceNotion[] }> => { return get<{ data: DataSourceNotion[] }>(url) } -export const syncDataSourceNotion: Fetcher = ({ url }) => { +export const syncDataSourceNotion = ({ url }: { url: string }): Promise => { return get(url) } -export const updateDataSourceNotionAction: Fetcher = ({ url }) => { +export const updateDataSourceNotionAction = ({ url }: { url: string }): Promise => { return patch(url) } -export const fetchPluginProviders: Fetcher = (url) => { +export const fetchPluginProviders = (url: string): Promise => { return get(url) } -export const validatePluginProviderKey: Fetcher = ({ url, body }) => { +export const validatePluginProviderKey = ({ url, body }: { url: string; body: { credentials: any } }): Promise => { return post(url, { body }) } -export const updatePluginProviderAIKey: Fetcher = ({ url, body }) => { +export const updatePluginProviderAIKey = ({ url, body }: { url: string; body: { credentials: any } }): Promise => { return post(url, { body }) } -export const invitationCheck: Fetcher = ({ url, params }) => { +export const invitationCheck = ({ url, params }: { url: string; params: { workspace_id?: string; email?: string; token: string } }): Promise => { return get(url, { params }) } -export const activateMember: Fetcher = ({ url, body }) => { +export const activateMember = ({ url, body }: { url: string; body: any }): Promise => { return post(url, { body }) } -export const fetchModelProviders: Fetcher<{ data: ModelProvider[] }, string> = (url) => { +export const fetchModelProviders = (url: string): Promise<{ data: ModelProvider[] }> => { return get<{ data: ModelProvider[] }>(url) } @@ -195,197 +194,197 @@ export type ModelProviderCredentials = { credentials?: Record load_balancing: ModelLoadBalancingConfig } -export const fetchModelProviderCredentials: Fetcher = (url) => { +export const fetchModelProviderCredentials = (url: string): Promise => { return get(url) } -export const fetchModelLoadBalancingConfig: Fetcher<{ +export const fetchModelLoadBalancingConfig = (url: string): Promise<{ credentials?: Record load_balancing: ModelLoadBalancingConfig -}, string> = (url) => { +}> => { return get<{ credentials?: Record load_balancing: ModelLoadBalancingConfig }>(url) } -export const fetchModelProviderModelList: Fetcher<{ data: ModelItem[] }, string> = (url) => { +export const fetchModelProviderModelList = (url: string): Promise<{ data: ModelItem[] }> => { return get<{ data: ModelItem[] }>(url) } -export const fetchModelList: Fetcher<{ data: Model[] }, string> = (url) => { +export const fetchModelList = (url: string): Promise<{ data: Model[] }> => { return get<{ data: Model[] }>(url) } -export const validateModelProvider: Fetcher = ({ url, body }) => { +export const validateModelProvider = ({ url, body }: { url: string; body: any }): Promise => { return post(url, { body }) } -export const validateModelLoadBalancingCredentials: Fetcher = ({ url, body }) => { +export const validateModelLoadBalancingCredentials = ({ url, body }: { url: string; body: any }): Promise => { return post(url, { body }) } -export const setModelProvider: Fetcher = ({ url, body }) => { +export const setModelProvider = ({ url, body }: { url: string; body: any }): Promise => { return post(url, { body }) } -export const deleteModelProvider: Fetcher = ({ url, body }) => { +export const deleteModelProvider = ({ url, body }: { url: string; body?: any }): Promise => { return del(url, { body }) } -export const changeModelProviderPriority: Fetcher = ({ url, body }) => { +export const changeModelProviderPriority = ({ url, body }: { url: string; body: any }): Promise => { return post(url, { body }) } -export const setModelProviderModel: Fetcher = ({ url, body }) => { +export const setModelProviderModel = ({ url, body }: { url: string; body: any }): Promise => { return post(url, { body }) } -export const deleteModelProviderModel: Fetcher = ({ url }) => { +export const deleteModelProviderModel = ({ url }: { url: string }): Promise => { return del(url) } -export const getPayUrl: Fetcher<{ url: string }, string> = (url) => { +export const getPayUrl = (url: string): Promise<{ url: string }> => { return get<{ url: string }>(url) } -export const fetchDefaultModal: Fetcher<{ data: DefaultModelResponse }, string> = (url) => { +export const fetchDefaultModal = (url: string): Promise<{ data: DefaultModelResponse }> => { return get<{ data: DefaultModelResponse }>(url) } -export const updateDefaultModel: Fetcher = ({ url, body }) => { +export const updateDefaultModel = ({ url, body }: { url: string; body: any }): Promise => { return post(url, { body }) } -export const fetchModelParameterRules: Fetcher<{ data: ModelParameterRule[] }, string> = (url) => { +export const fetchModelParameterRules = (url: string): Promise<{ data: ModelParameterRule[] }> => { return get<{ data: ModelParameterRule[] }>(url) } -export const fetchFileUploadConfig: Fetcher = ({ url }) => { +export const fetchFileUploadConfig = ({ url }: { url: string }): Promise => { return get(url) } -export const fetchNotionConnection: Fetcher<{ data: string }, string> = (url) => { - return get(url) as Promise<{ data: string }> +export const fetchNotionConnection = (url: string): Promise<{ data: string }> => { + return get<{ data: string }>(url) } -export const fetchDataSourceNotionBinding: Fetcher<{ result: string }, string> = (url) => { - return get(url) as Promise<{ result: string }> +export const fetchDataSourceNotionBinding = (url: string): Promise<{ result: string }> => { + return get<{ result: string }>(url) } -export const fetchApiBasedExtensionList: Fetcher = (url) => { - return get(url) as Promise +export const fetchApiBasedExtensionList = (url: string): Promise => { + return get(url) } -export const fetchApiBasedExtensionDetail: Fetcher = (url) => { - return get(url) as Promise +export const fetchApiBasedExtensionDetail = (url: string): Promise => { + return get(url) } -export const addApiBasedExtension: Fetcher = ({ url, body }) => { - return post(url, { body }) as Promise +export const addApiBasedExtension = ({ url, body }: { url: string; body: ApiBasedExtension }): Promise => { + return post(url, { body }) } -export const updateApiBasedExtension: Fetcher = ({ url, body }) => { - return post(url, { body }) as Promise +export const updateApiBasedExtension = ({ url, body }: { url: string; body: ApiBasedExtension }): Promise => { + return post(url, { body }) } -export const deleteApiBasedExtension: Fetcher<{ result: string }, string> = (url) => { - return del(url) as Promise<{ result: string }> +export const deleteApiBasedExtension = (url: string): Promise<{ result: string }> => { + return del<{ result: string }>(url) } -export const fetchCodeBasedExtensionList: Fetcher = (url) => { - return get(url) as Promise +export const fetchCodeBasedExtensionList = (url: string): Promise => { + return get(url) } -export const moderate = (url: string, body: { app_id: string; text: string }) => { - return post(url, { body }) as Promise +export const moderate = (url: string, body: { app_id: string; text: string }): Promise => { + return post(url, { body }) } type RetrievalMethodsRes = { retrieval_method: RETRIEVE_METHOD[] } -export const fetchSupportRetrievalMethods: Fetcher = (url) => { +export const fetchSupportRetrievalMethods = (url: string): Promise => { return get(url) } -export const getSystemFeatures = () => { +export const getSystemFeatures = (): Promise => { return get('/system-features') } -export const enableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) => +export const enableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }): Promise => patch(url, { body }) -export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) => +export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }): Promise => patch(url, { body }) -export const sendForgotPasswordEmail: Fetcher = ({ url, body }) => +export const sendForgotPasswordEmail = ({ url, body }: { url: string; body: { email: string } }): Promise => post(url, { body }) -export const verifyForgotPasswordToken: Fetcher = ({ url, body }) => { - return post(url, { body }) as Promise +export const verifyForgotPasswordToken = ({ url, body }: { url: string; body: { token: string } }): Promise => { + return post(url, { body }) } -export const changePasswordWithToken: Fetcher = ({ url, body }) => +export const changePasswordWithToken = ({ url, body }: { url: string; body: { token: string; new_password: string; password_confirm: string } }): Promise => post(url, { body }) -export const sendWebAppForgotPasswordEmail: Fetcher = ({ url, body }) => +export const sendWebAppForgotPasswordEmail = ({ url, body }: { url: string; body: { email: string } }): Promise => post(url, { body }, { isPublicAPI: true }) -export const verifyWebAppForgotPasswordToken: Fetcher = ({ url, body }) => { - return post(url, { body }, { isPublicAPI: true }) as Promise +export const verifyWebAppForgotPasswordToken = ({ url, body }: { url: string; body: { token: string } }): Promise => { + return post(url, { body }, { isPublicAPI: true }) } -export const changeWebAppPasswordWithToken: Fetcher = ({ url, body }) => +export const changeWebAppPasswordWithToken = ({ url, body }: { url: string; body: { token: string; new_password: string; password_confirm: string } }): Promise => post(url, { body }, { isPublicAPI: true }) -export const uploadRemoteFileInfo = (url: string, isPublic?: boolean, silent?: boolean) => { +export const uploadRemoteFileInfo = (url: string, isPublic?: boolean, silent?: boolean): Promise<{ id: string; name: string; size: number; mime_type: string; url: string }> => { return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic, silent }) } -export const sendEMailLoginCode = (email: string, language = 'en-US') => +export const sendEMailLoginCode = (email: string, language = 'en-US'): Promise => post('/email-code-login', { body: { email, language } }) -export const emailLoginWithCode = (data: { email: string; code: string; token: string; language: string }) => +export const emailLoginWithCode = (data: { email: string; code: string; token: string; language: string }): Promise => post('/email-code-login/validity', { body: data }) -export const sendResetPasswordCode = (email: string, language = 'en-US') => +export const sendResetPasswordCode = (email: string, language = 'en-US'): Promise => post('/forgot-password', { body: { email, language } }) -export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) => +export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }): Promise => post('/forgot-password/validity', { body }) -export const sendWebAppEMailLoginCode = (email: string, language = 'en-US') => +export const sendWebAppEMailLoginCode = (email: string, language = 'en-US'): Promise => post('/email-code-login', { body: { email, language } }, { isPublicAPI: true }) -export const webAppEmailLoginWithCode = (data: { email: string; code: string; token: string }) => +export const webAppEmailLoginWithCode = (data: { email: string; code: string; token: string }): Promise => post('/email-code-login/validity', { body: data }, { isPublicAPI: true }) -export const sendWebAppResetPasswordCode = (email: string, language = 'en-US') => +export const sendWebAppResetPasswordCode = (email: string, language = 'en-US'): Promise => post('/forgot-password', { body: { email, language } }, { isPublicAPI: true }) -export const verifyWebAppResetPasswordCode = (body: { email: string; code: string; token: string }) => +export const verifyWebAppResetPasswordCode = (body: { email: string; code: string; token: string }): Promise => post('/forgot-password/validity', { body }, { isPublicAPI: true }) -export const sendDeleteAccountCode = () => +export const sendDeleteAccountCode = (): Promise => get('/account/delete/verify') -export const verifyDeleteAccountCode = (body: { code: string; token: string }) => +export const verifyDeleteAccountCode = (body: { code: string; token: string }): Promise => post('/account/delete', { body }) -export const submitDeleteAccountFeedback = (body: { feedback: string; email: string }) => +export const submitDeleteAccountFeedback = (body: { feedback: string; email: string }): Promise => post('/account/delete/feedback', { body }) -export const getDocDownloadUrl = (doc_name: string) => +export const getDocDownloadUrl = (doc_name: string): Promise<{ url: string }> => get<{ url: string }>('/compliance/download', { params: { doc_name } }, { silent: true }) -export const sendVerifyCode = (body: { email: string; phase: string; token?: string }) => +export const sendVerifyCode = (body: { email: string; phase: string; token?: string }): Promise => post('/account/change-email', { body }) -export const verifyEmail = (body: { email: string; code: string; token: string }) => +export const verifyEmail = (body: { email: string; code: string; token: string }): Promise => post('/account/change-email/validity', { body }) -export const resetEmail = (body: { new_email: string; token: string }) => +export const resetEmail = (body: { new_email: string; token: string }): Promise => post('/account/change-email/reset', { body }) -export const checkEmailExisted = (body: { email: string }) => +export const checkEmailExisted = (body: { email: string }): Promise => post('/account/change-email/check-email-unique', { body }, { silent: true }) diff --git a/web/service/use-common.ts b/web/service/use-common.ts index 51b35c453b..5c71553781 100644 --- a/web/service/use-common.ts +++ b/web/service/use-common.ts @@ -1,5 +1,8 @@ import { get, post } from './base' import type { + AccountIntegrate, + CommonResponse, + DataSourceNotion, FileUploadConfigResponse, Member, StructuredOutputRulesRequestBody, @@ -7,16 +10,112 @@ import type { } from '@/models/common' import { useMutation, useQuery } from '@tanstack/react-query' import type { FileTypesRes } from './datasets' +import type { ICurrentWorkspace, IWorkspace, UserProfileResponse } from '@/models/common' +import type { + Model, + ModelProvider, + ModelTypeEnum, +} from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { RETRIEVE_METHOD } from '@/types/app' +import type { LangGeniusVersionResponse } from '@/models/common' +import type { PluginProvider } from '@/models/common' +import type { ApiBasedExtension } from '@/models/common' +import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { CodeBasedExtension } from '@/models/common' +import { useInvalid } from './use-base' const NAME_SPACE = 'common' +export const commonQueryKeys = { + fileUploadConfig: [NAME_SPACE, 'file-upload-config'] as const, + userProfile: [NAME_SPACE, 'user-profile'] as const, + currentWorkspace: [NAME_SPACE, 'current-workspace'] as const, + workspaces: [NAME_SPACE, 'workspaces'] as const, + members: [NAME_SPACE, 'members'] as const, + filePreview: (fileID: string) => [NAME_SPACE, 'file-preview', fileID] as const, + schemaDefinitions: [NAME_SPACE, 'schema-type-definitions'] as const, + isLogin: [NAME_SPACE, 'is-login'] as const, + modelProviders: [NAME_SPACE, 'model-providers'] as const, + modelList: (type: ModelTypeEnum) => [NAME_SPACE, 'model-list', type] as const, + defaultModel: (type: ModelTypeEnum) => [NAME_SPACE, 'default-model', type] as const, + retrievalMethods: [NAME_SPACE, 'support-retrieval-methods'] as const, + accountIntegrates: [NAME_SPACE, 'account-integrates'] as const, + pluginProviders: [NAME_SPACE, 'plugin-providers'] as const, + notionConnection: [NAME_SPACE, 'notion-connection'] as const, + apiBasedExtensions: [NAME_SPACE, 'api-based-extensions'] as const, + codeBasedExtensions: (module?: string) => [NAME_SPACE, 'code-based-extensions', module] as const, + invitationCheck: (params?: { workspace_id?: string; email?: string; token?: string }) => [ + NAME_SPACE, + 'invitation-check', + params?.workspace_id ?? '', + params?.email ?? '', + params?.token ?? '', + ] as const, + notionBinding: (code?: string | null) => [NAME_SPACE, 'notion-binding', code] as const, + modelParameterRules: (provider?: string, model?: string) => [NAME_SPACE, 'model-parameter-rules', provider, model] as const, + langGeniusVersion: (currentVersion?: string | null) => [NAME_SPACE, 'lang-genius-version', currentVersion] as const, + forgotPasswordValidity: (token?: string | null) => [NAME_SPACE, 'forgot-password-validity', token] as const, + dataSourceIntegrates: [NAME_SPACE, 'data-source-integrates'] as const, +} + export const useFileUploadConfig = () => { return useQuery({ - queryKey: [NAME_SPACE, 'file-upload-config'], + queryKey: commonQueryKeys.fileUploadConfig, queryFn: () => get('/files/upload'), }) } +type UserProfileWithMeta = { + profile: UserProfileResponse + meta: { + currentVersion: string | null + currentEnv: string | null + } +} + +export const useUserProfile = () => { + return useQuery({ + queryKey: commonQueryKeys.userProfile, + queryFn: async () => { + const response = await get('/account/profile', {}, { needAllResponseContent: true }) as Response + const profile = await response.clone().json() as UserProfileResponse + return { + profile, + meta: { + currentVersion: response.headers.get('x-version'), + currentEnv: process.env.NODE_ENV === 'development' + ? 'DEVELOPMENT' + : response.headers.get('x-env'), + }, + } + }, + staleTime: 0, + gcTime: 0, + }) +} + +export const useLangGeniusVersion = (currentVersion?: string | null, enabled?: boolean) => { + return useQuery({ + queryKey: commonQueryKeys.langGeniusVersion(currentVersion || undefined), + queryFn: () => get('/version', { params: { current_version: currentVersion } }), + enabled: !!currentVersion && (enabled ?? true), + }) +} + +export const useCurrentWorkspace = () => { + return useQuery({ + queryKey: commonQueryKeys.currentWorkspace, + queryFn: () => post('/workspaces/current', { body: {} }), + }) +} + +export const useWorkspaces = () => { + return useQuery<{ workspaces: IWorkspace[] }>({ + queryKey: commonQueryKeys.workspaces, + queryFn: () => get<{ workspaces: IWorkspace[] }>('/workspaces'), + }) +} + export const useGenerateStructuredOutputRules = () => { return useMutation({ mutationKey: [NAME_SPACE, 'generate-structured-output-rules'], @@ -74,10 +173,8 @@ type MemberResponse = { export const useMembers = () => { return useQuery({ - queryKey: [NAME_SPACE, 'members'], - queryFn: (params: Record) => get('/workspaces/current/members', { - params, - }), + queryKey: commonQueryKeys.members, + queryFn: () => get('/workspaces/current/members', { params: {} }), }) } @@ -87,7 +184,7 @@ type FilePreviewResponse = { export const useFilePreview = (fileID: string) => { return useQuery({ - queryKey: [NAME_SPACE, 'file-preview', fileID], + queryKey: commonQueryKeys.filePreview(fileID), queryFn: () => get(`/files/${fileID}/preview`), enabled: !!fileID, }) @@ -102,7 +199,7 @@ export type SchemaTypeDefinition = { export const useSchemaTypeDefinitions = () => { return useQuery({ - queryKey: [NAME_SPACE, 'schema-type-definitions'], + queryKey: commonQueryKeys.schemaDefinitions, queryFn: () => get('/spec/schema-definitions'), }) } @@ -113,7 +210,7 @@ type isLogin = { export const useIsLogin = () => { return useQuery({ - queryKey: [NAME_SPACE, 'is-login'], + queryKey: commonQueryKeys.isLogin, staleTime: 0, gcTime: 0, queryFn: async (): Promise => { @@ -138,3 +235,141 @@ export const useLogout = () => { mutationFn: () => post('/logout'), }) } + +type ForgotPasswordValidity = CommonResponse & { is_valid: boolean; email: string } +export const useVerifyForgotPasswordToken = (token?: string | null) => { + return useQuery({ + queryKey: commonQueryKeys.forgotPasswordValidity(token), + queryFn: () => post('/forgot-password/validity', { body: { token } }), + enabled: !!token, + staleTime: 0, + gcTime: 0, + retry: false, + }) +} + +type OneMoreStepPayload = { + invitation_code: string + interface_language: string + timezone: string +} +export const useOneMoreStep = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'one-more-step'], + mutationFn: (body: OneMoreStepPayload) => post('/account/init', { body }), + }) +} + +export const useModelProviders = () => { + return useQuery<{ data: ModelProvider[] }>({ + queryKey: commonQueryKeys.modelProviders, + queryFn: () => get<{ data: ModelProvider[] }>('/workspaces/current/model-providers'), + }) +} + +export const useModelListByType = (type: ModelTypeEnum, enabled = true) => { + return useQuery<{ data: Model[] }>({ + queryKey: commonQueryKeys.modelList(type), + queryFn: () => get<{ data: Model[] }>(`/workspaces/current/models/model-types/${type}`), + enabled, + }) +} + +export const useDefaultModelByType = (type: ModelTypeEnum, enabled = true) => { + return useQuery({ + queryKey: commonQueryKeys.defaultModel(type), + queryFn: () => get(`/workspaces/current/default-model?model_type=${type}`), + enabled, + }) +} + +export const useSupportRetrievalMethods = () => { + return useQuery<{ retrieval_method: RETRIEVE_METHOD[] }>({ + queryKey: commonQueryKeys.retrievalMethods, + queryFn: () => get<{ retrieval_method: RETRIEVE_METHOD[] }>('/datasets/retrieval-setting'), + }) +} + +export const useAccountIntegrates = () => { + return useQuery<{ data: AccountIntegrate[] | null }>({ + queryKey: commonQueryKeys.accountIntegrates, + queryFn: () => get<{ data: AccountIntegrate[] | null }>('/account/integrates'), + }) +} + +type DataSourceIntegratesOptions = { + enabled?: boolean + initialData?: { data: DataSourceNotion[] } +} + +export const useDataSourceIntegrates = (options: DataSourceIntegratesOptions = {}) => { + const { enabled = true, initialData } = options + return useQuery<{ data: DataSourceNotion[] }>({ + queryKey: commonQueryKeys.dataSourceIntegrates, + queryFn: () => get<{ data: DataSourceNotion[] }>('/data-source/integrates'), + enabled, + initialData, + }) +} + +export const useInvalidDataSourceIntegrates = () => { + return useInvalid(commonQueryKeys.dataSourceIntegrates) +} + +export const usePluginProviders = () => { + return useQuery({ + queryKey: commonQueryKeys.pluginProviders, + queryFn: () => get('/workspaces/current/tool-providers'), + }) +} + +export const useCodeBasedExtensions = (module: string) => { + return useQuery({ + queryKey: commonQueryKeys.codeBasedExtensions(module), + queryFn: () => get(`/code-based-extension?module=${module}`), + }) +} + +export const useNotionConnection = (enabled: boolean) => { + return useQuery<{ data: string }>({ + queryKey: commonQueryKeys.notionConnection, + queryFn: () => get<{ data: string }>('/oauth/data-source/notion'), + enabled, + }) +} + +export const useApiBasedExtensions = () => { + return useQuery({ + queryKey: commonQueryKeys.apiBasedExtensions, + queryFn: () => get('/api-based-extension'), + }) +} + +export const useInvitationCheck = (params?: { workspace_id?: string; email?: string; token?: string }, enabled?: boolean) => { + return useQuery({ + queryKey: commonQueryKeys.invitationCheck(params), + queryFn: () => get<{ + is_valid: boolean + data: { workspace_name: string; email: string; workspace_id: string } + result: string + }>('/activate/check', { params }), + enabled: enabled ?? !!params?.token, + retry: false, + }) +} + +export const useNotionBinding = (code?: string | null, enabled?: boolean) => { + return useQuery({ + queryKey: commonQueryKeys.notionBinding(code), + queryFn: () => get<{ result: string }>('/oauth/data-source/binding/notion', { params: { code } }), + enabled: !!code && (enabled ?? true), + }) +} + +export const useModelParameterRules = (provider?: string, model?: string, enabled?: boolean) => { + return useQuery<{ data: ModelParameterRule[] }>({ + queryKey: commonQueryKeys.modelParameterRules(provider, model), + queryFn: () => get<{ data: ModelParameterRule[] }>(`/workspaces/current/model-providers/${provider}/models/parameter-rules`, { params: { model } }), + enabled: !!provider && !!model && (enabled ?? true), + }) +}