mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 14:14:17 +08:00
refactor: no global loading
This commit is contained in:
parent
a87b928079
commit
1620e224bf
@ -55,7 +55,7 @@ describe('DatasetsLayout', () => {
|
||||
setAppContext()
|
||||
})
|
||||
|
||||
it('should render loading when workspace is still loading', () => {
|
||||
it('should keep rendering children when workspace is still loading', () => {
|
||||
setAppContext({
|
||||
isLoadingCurrentWorkspace: true,
|
||||
currentWorkspace: { id: '' },
|
||||
@ -67,8 +67,7 @@ describe('DatasetsLayout', () => {
|
||||
</DatasetsLayout>
|
||||
))
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('datasets-content')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('datasets-content')).toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ExternalApiPanelProvider } from '@/context/external-api-panel-context'
|
||||
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
|
||||
@ -19,9 +18,6 @@ export default function DatasetsLayout({ children }: { children: React.ReactNode
|
||||
router.replace('/apps')
|
||||
}, [shouldRedirect, router])
|
||||
|
||||
if (isLoadingCurrentWorkspace || !currentWorkspace.id)
|
||||
return <Loading type="app" />
|
||||
|
||||
if (shouldRedirect) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ describe('RoleRouteGuard', () => {
|
||||
setAppContext()
|
||||
})
|
||||
|
||||
it('should render loading while workspace is loading', () => {
|
||||
it('should keep rendering children while workspace is loading', () => {
|
||||
setAppContext({
|
||||
isLoadingCurrentWorkspace: true,
|
||||
})
|
||||
@ -52,8 +52,7 @@ describe('RoleRouteGuard', () => {
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('guarded-content')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('guarded-content')).toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
|
||||
@ -22,10 +21,6 @@ export default function RoleRouteGuard({ children }: { children: ReactNode }) {
|
||||
router.replace('/datasets')
|
||||
}, [shouldRedirect, router])
|
||||
|
||||
// Block rendering only for guarded routes to avoid permission flicker.
|
||||
if (shouldGuardRoute && isLoadingCurrentWorkspace)
|
||||
return <Loading type="app" />
|
||||
|
||||
if (shouldRedirect)
|
||||
return null
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { usePathname, useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
@ -18,9 +17,9 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const updateAppParams = useWebAppStore(s => s.updateAppParams)
|
||||
const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta)
|
||||
const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp)
|
||||
const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams()
|
||||
const { isFetching: isFetchingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo()
|
||||
const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta()
|
||||
const { data: appParams, error: appParamsError } = useGetWebAppParams()
|
||||
const { data: appInfo, error: appInfoError } = useGetWebAppInfo()
|
||||
const { data: appMeta, error: appMetaError } = useGetWebAppMeta()
|
||||
const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false })
|
||||
|
||||
useEffect(() => {
|
||||
@ -81,14 +80,7 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-2">
|
||||
<AppUnavailable className="h-auto w-auto" code={403} unknownReason="no permission." />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('userProfile.logout', { ns: 'common' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading />
|
||||
<span className="cursor-pointer text-text-tertiary system-sm-regular" onClick={backToHome}>{t('userProfile.logout', { ns: 'common' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
@ -12,7 +11,6 @@ import { setWebAppAccessToken, setWebAppPassport, webAppLoginStatus, webAppLogou
|
||||
const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
const shareCode = useWebAppStore(s => s.shareCode)
|
||||
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
|
||||
const embeddedUserId = useWebAppStore(s => s.embeddedUserId)
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
@ -32,13 +30,9 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
const url = getSigninUrl()
|
||||
router.replace(url)
|
||||
}, [getSigninUrl, router, shareCode])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
setIsLoading(false)
|
||||
if (message)
|
||||
return
|
||||
}
|
||||
|
||||
if (tokenFromUrl)
|
||||
setWebAppAccessToken(tokenFromUrl)
|
||||
@ -46,12 +40,6 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
const redirectOrFinish = () => {
|
||||
if (redirectUrl)
|
||||
router.replace(decodeURIComponent(redirectUrl))
|
||||
else
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const proceedToAuth = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
(async () => {
|
||||
@ -60,9 +48,6 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
if (userLoggedIn && appLoggedIn) {
|
||||
redirectOrFinish()
|
||||
}
|
||||
else if (!userLoggedIn && !appLoggedIn) {
|
||||
proceedToAuth()
|
||||
}
|
||||
else if (!userLoggedIn && appLoggedIn) {
|
||||
redirectOrFinish()
|
||||
}
|
||||
@ -77,7 +62,6 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
}
|
||||
catch {
|
||||
await webAppLogout(shareCode!)
|
||||
proceedToAuth()
|
||||
}
|
||||
}
|
||||
})()
|
||||
@ -86,7 +70,6 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
redirectUrl,
|
||||
router,
|
||||
message,
|
||||
webAppAccessMode,
|
||||
tokenFromUrl,
|
||||
embeddedUserId,
|
||||
])
|
||||
@ -95,18 +78,11 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-4">
|
||||
<AppUnavailable className="h-auto w-auto" code={code || t('common.appUnavailable', { ns: 'share' })} unknownReason={message} />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{code === '403' ? t('userProfile.logout', { ns: 'common' }) : t('login.backToHome', { ns: 'share' })}</span>
|
||||
<span className="cursor-pointer text-text-tertiary system-sm-regular" onClick={backToHome}>{code === '403' ? t('userProfile.logout', { ns: 'common' }) : t('login.backToHome', { ns: 'share' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
'use client'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
import Header from '@/app/signin/_header'
|
||||
import { AppContextProvider } from '@/context/app-context-provider'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
@ -11,16 +9,8 @@ import { cn } from '@/utils/classnames'
|
||||
export default function SignInLayout({ children }: any) {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
useDocumentTitle('')
|
||||
const { isLoading, data: loginData } = useIsLogin()
|
||||
const { data: loginData } = useIsLogin()
|
||||
const isLoggedIn = loginData?.logged_in
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full justify-center bg-background-default-burn">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
|
||||
@ -12,7 +12,6 @@ import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Avatar } from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect'
|
||||
@ -70,6 +69,7 @@ export default function OAuthAuthorize() {
|
||||
const { isLoading: isIsLoginLoading, data: loginData } = useIsLogin()
|
||||
const isLoggedIn = loginData?.logged_in
|
||||
const isLoading = isOAuthLoading || isIsLoginLoading
|
||||
const isActionDisabled = !client_id || !redirect_uri || isError || isLoading || authorizing
|
||||
const onLoginSwitchClick = () => {
|
||||
try {
|
||||
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
|
||||
@ -110,14 +110,6 @@ export default function OAuthAuthorize() {
|
||||
}
|
||||
}, [client_id, redirect_uri, isError])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-background-default-subtle">
|
||||
<Loading type="app" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-background-default-subtle">
|
||||
{authAppInfo?.app_icon && (
|
||||
@ -169,7 +161,7 @@ export default function OAuthAuthorize() {
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('continue', { ns: 'oauth' })}</Button>
|
||||
<Button variant="primary" size="large" className="w-full" onClick={onAuthorize} disabled={isActionDisabled} loading={authorizing}>{t('continue', { ns: 'oauth' })}</Button>
|
||||
<Button size="large" className="w-full" onClick={() => router.push('/apps')}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Cookies from 'js-cookie'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
@ -25,7 +25,6 @@ export const AppInitializer = ({
|
||||
const searchParams = useSearchParams()
|
||||
// Tokens are now stored in cookies, no need to check localStorage
|
||||
const pathname = usePathname()
|
||||
const [init, setInit] = useState(false)
|
||||
const [oauthNewUser] = useQueryState(
|
||||
'oauth_new_user',
|
||||
parseAsBoolean.withOptions({ history: 'replace' }),
|
||||
@ -87,10 +86,7 @@ export const AppInitializer = ({
|
||||
const redirectUrl = resolvePostLoginRedirect()
|
||||
if (redirectUrl) {
|
||||
location.replace(redirectUrl)
|
||||
return
|
||||
}
|
||||
|
||||
setInit(true)
|
||||
}
|
||||
catch {
|
||||
router.replace('/signin')
|
||||
@ -98,5 +94,5 @@ export const AppInitializer = ({
|
||||
})()
|
||||
}, [isSetupFinished, router, pathname, searchParams, oauthNewUser])
|
||||
|
||||
return init ? children : null
|
||||
return children
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ describe('convertToMp3', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should convert mono WAV data to an MP3 blob', () => {
|
||||
it('should convert mono WAV data to an MP3 blob', async () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 44100,
|
||||
@ -76,7 +76,7 @@ describe('convertToMp3', () => {
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([1, 2, 3]))
|
||||
mocks.flush.mockReturnValue(new Int8Array([4, 5]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
const result = await convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.type).toBe('audio/mp3')
|
||||
@ -87,7 +87,7 @@ describe('convertToMp3', () => {
|
||||
expect(mocks.flush).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should convert stereo WAV data to an MP3 blob', () => {
|
||||
it('should convert stereo WAV data to an MP3 blob', async () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 2,
|
||||
sampleRate: 48000,
|
||||
@ -98,7 +98,7 @@ describe('convertToMp3', () => {
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([10, 20]))
|
||||
mocks.flush.mockReturnValue(new Int8Array([30]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
const result = await convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.type).toBe('audio/mp3')
|
||||
@ -107,7 +107,7 @@ describe('convertToMp3', () => {
|
||||
expect(firstCall).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should skip empty encoded buffers', () => {
|
||||
it('should skip empty encoded buffers', async () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 44100,
|
||||
@ -117,14 +117,14 @@ describe('convertToMp3', () => {
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array(0))
|
||||
mocks.flush.mockReturnValue(new Int8Array(0))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
const result = await convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.type).toBe('audio/mp3')
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it('should include flush data when flush returns non-empty buffer', () => {
|
||||
it('should include flush data when flush returns non-empty buffer', async () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 22050,
|
||||
@ -134,13 +134,13 @@ describe('convertToMp3', () => {
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array(0))
|
||||
mocks.flush.mockReturnValue(new Int8Array([99, 98, 97]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
const result = await convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.size).toBe(3)
|
||||
})
|
||||
|
||||
it('should omit flush data when flush returns empty buffer', () => {
|
||||
it('should omit flush data when flush returns empty buffer', async () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
sampleRate: 44100,
|
||||
@ -150,13 +150,13 @@ describe('convertToMp3', () => {
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([1, 2]))
|
||||
mocks.flush.mockReturnValue(new Int8Array(0))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
const result = await convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
expect(result.size).toBe(2)
|
||||
})
|
||||
|
||||
it('should process multiple chunks when sample count exceeds maxSamples (1152)', () => {
|
||||
it('should process multiple chunks when sample count exceeds maxSamples (1152)', async () => {
|
||||
const samples = Array.from({ length: 2400 }, (_, i) => i % 32767)
|
||||
const recorder = createMockRecorder({
|
||||
channels: 1,
|
||||
@ -167,13 +167,13 @@ describe('convertToMp3', () => {
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([1]))
|
||||
mocks.flush.mockReturnValue(new Int8Array(0))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
const result = await convertToMp3(recorder)
|
||||
|
||||
expect(mocks.encodeBuffer.mock.calls.length).toBeGreaterThan(1)
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
it('should encode stereo with right channel subarray', () => {
|
||||
it('should encode stereo with right channel subarray', async () => {
|
||||
const recorder = createMockRecorder({
|
||||
channels: 2,
|
||||
sampleRate: 44100,
|
||||
@ -184,7 +184,7 @@ describe('convertToMp3', () => {
|
||||
mocks.encodeBuffer.mockReturnValue(new Int8Array([5, 6, 7]))
|
||||
mocks.flush.mockReturnValue(new Int8Array([8]))
|
||||
|
||||
const result = convertToMp3(recorder)
|
||||
const result = await convertToMp3(recorder)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
for (const call of mocks.encodeBuffer.mock.calls) {
|
||||
|
||||
@ -82,7 +82,7 @@ const VoiceInput = ({
|
||||
const canvas = canvasRef.current!
|
||||
const ctx = ctxRef.current!
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
const mp3Blob = convertToMp3(recorder.current)
|
||||
const mp3Blob = await convertToMp3(recorder.current as unknown as Parameters<typeof convertToMp3>[0])
|
||||
const mp3File = new File([mp3Blob], 'temp.mp3', { type: 'audio/mp3' })
|
||||
const formData = new FormData()
|
||||
formData.append('file', mp3File)
|
||||
|
||||
@ -1,24 +1,55 @@
|
||||
import lamejs from 'lamejs'
|
||||
import BitStream from 'lamejs/src/js/BitStream'
|
||||
import Lame from 'lamejs/src/js/Lame'
|
||||
import MPEGMode from 'lamejs/src/js/MPEGMode'
|
||||
|
||||
/* v8 ignore next - @preserve */
|
||||
if (globalThis) {
|
||||
(globalThis as any).MPEGMode = MPEGMode
|
||||
; (globalThis as any).Lame = Lame
|
||||
; (globalThis as any).BitStream = BitStream
|
||||
type RecorderLike = {
|
||||
getWAV: () => ArrayBufferLike
|
||||
getChannelData: () => {
|
||||
left: ArrayBufferView
|
||||
right?: ArrayBufferView | null
|
||||
}
|
||||
}
|
||||
|
||||
export const convertToMp3 = (recorder: any) => {
|
||||
const wav = lamejs.WavHeader.readHeader(recorder.getWAV())
|
||||
const toInt16Array = (view: ArrayBufferView) => {
|
||||
return new Int16Array(view.buffer, view.byteOffset, view.byteLength / 2)
|
||||
}
|
||||
|
||||
const loadLame = async () => {
|
||||
const [
|
||||
lamejsModule,
|
||||
bitStreamModule,
|
||||
lameModule,
|
||||
mpegModeModule,
|
||||
] = await Promise.all([
|
||||
import('lamejs'),
|
||||
import('lamejs/src/js/BitStream'),
|
||||
import('lamejs/src/js/Lame'),
|
||||
import('lamejs/src/js/MPEGMode'),
|
||||
])
|
||||
|
||||
const lamejs = lamejsModule.default
|
||||
const BitStream = bitStreamModule.default
|
||||
const Lame = lameModule.default
|
||||
const MPEGMode = mpegModeModule.default
|
||||
|
||||
/* v8 ignore next - @preserve */
|
||||
if (globalThis) {
|
||||
; (globalThis as any).MPEGMode = MPEGMode
|
||||
; (globalThis as any).Lame = Lame
|
||||
; (globalThis as any).BitStream = BitStream
|
||||
}
|
||||
|
||||
return lamejs
|
||||
}
|
||||
|
||||
export const convertToMp3 = async (recorder: RecorderLike) => {
|
||||
const lamejs = await loadLame()
|
||||
const wavBuffer = recorder.getWAV()
|
||||
const wavView = wavBuffer instanceof DataView ? wavBuffer : new DataView(wavBuffer)
|
||||
const wav = lamejs.WavHeader.readHeader(wavView)
|
||||
const { channels, sampleRate } = wav
|
||||
const mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128)
|
||||
const result = recorder.getChannelData()
|
||||
const buffer: BlobPart[] = []
|
||||
|
||||
const leftData = result.left && new Int16Array(result.left.buffer, 0, result.left.byteLength / 2)
|
||||
const rightData = result.right && new Int16Array(result.right.buffer, 0, result.right.byteLength / 2)
|
||||
const leftData = toInt16Array(result.left)
|
||||
const rightData = result.right ? toInt16Array(result.right) : null
|
||||
const remaining = leftData.length + (rightData ? rightData.length : 0)
|
||||
|
||||
const maxSamples = 1152
|
||||
@ -34,7 +65,7 @@ export const convertToMp3 = (recorder: any) => {
|
||||
let mp3buf = null
|
||||
|
||||
if (channels === 2) {
|
||||
right = rightData.subarray(i, i + maxSamples)
|
||||
right = rightData?.subarray(i, i + maxSamples) || null
|
||||
mp3buf = mp3enc.encodeBuffer(left, right)
|
||||
}
|
||||
else {
|
||||
|
||||
@ -24,7 +24,7 @@ const usePSInfo = () => {
|
||||
}] = useBoolean(false)
|
||||
const { mutateAsync } = useBindPartnerStackInfo()
|
||||
// Save to top domain. cloud.dify.ai => .dify.ai
|
||||
const domain = globalThis.location.hostname.replace('cloud', '')
|
||||
const domain = globalThis.location?.hostname?.replace('cloud', '')
|
||||
|
||||
const saveOrUpdate = useCallback(() => {
|
||||
if (!psPartnerKey || !psClickId)
|
||||
@ -37,9 +37,9 @@ const usePSInfo = () => {
|
||||
}), {
|
||||
expires: PARTNER_STACK_CONFIG.saveCookieDays,
|
||||
path: '/',
|
||||
domain,
|
||||
...(domain ? { domain } : {}),
|
||||
})
|
||||
}, [psPartnerKey, psClickId, isPSChanged])
|
||||
}, [psPartnerKey, psClickId, isPSChanged, domain])
|
||||
|
||||
const bind = useCallback(async () => {
|
||||
if (psPartnerKey && psClickId && !hasBind) {
|
||||
@ -55,11 +55,15 @@ const usePSInfo = () => {
|
||||
if ((error as { status: number })?.status === 400)
|
||||
shouldRemoveCookie = true
|
||||
}
|
||||
if (shouldRemoveCookie)
|
||||
Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain })
|
||||
if (shouldRemoveCookie) {
|
||||
Cookies.remove(PARTNER_STACK_CONFIG.cookieName, {
|
||||
path: '/',
|
||||
...(domain ? { domain } : {}),
|
||||
})
|
||||
}
|
||||
setBind()
|
||||
}
|
||||
}, [psPartnerKey, psClickId, mutateAsync, hasBind, setBind])
|
||||
}, [psPartnerKey, psClickId, mutateAsync, hasBind, setBind, domain])
|
||||
return {
|
||||
psPartnerKey,
|
||||
psClickId,
|
||||
|
||||
@ -10,6 +10,8 @@ type HeaderWrapperProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const getWorkflowCanvasMaximize = () => globalThis.localStorage?.getItem('workflow-canvas-maximize') === 'true'
|
||||
|
||||
const HeaderWrapper = ({
|
||||
children,
|
||||
}: HeaderWrapperProps) => {
|
||||
@ -18,8 +20,7 @@ const HeaderWrapper = ({
|
||||
// Check if the current path is a workflow canvas & fullscreen
|
||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
|
||||
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
||||
const [hideHeader, setHideHeader] = useState(getWorkflowCanvasMaximize)
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
|
||||
@ -3,16 +3,19 @@ import { X } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { NOTICE_I18N } from '@/i18n-config/language'
|
||||
|
||||
const getShowNotice = () => globalThis.localStorage?.getItem('hide-maintenance-notice') !== '1'
|
||||
|
||||
const MaintenanceNotice = () => {
|
||||
const locale = useLanguage()
|
||||
|
||||
const [showNotice, setShowNotice] = useState(() => localStorage.getItem('hide-maintenance-notice') !== '1')
|
||||
const [showNotice, setShowNotice] = useState(getShowNotice)
|
||||
|
||||
const handleJumpNotice = () => {
|
||||
window.open(NOTICE_I18N.href, '_blank')
|
||||
}
|
||||
|
||||
const handleCloseNotice = () => {
|
||||
localStorage.setItem('hide-maintenance-notice', '1')
|
||||
globalThis.localStorage?.setItem('hide-maintenance-notice', '1')
|
||||
setShowNotice(false)
|
||||
}
|
||||
|
||||
|
||||
@ -5,12 +5,12 @@ import { RiLoader2Line } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { gte } from 'semver'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins'
|
||||
import { gte } from '@/utils/semver'
|
||||
import Card from '../../../card'
|
||||
import { TaskStatus } from '../../../types'
|
||||
import checkTaskStatus from '../../base/check-task-status'
|
||||
@ -117,7 +117,7 @@ const Installed: FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<div className="text-text-secondary system-md-regular">
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
|
||||
<p>
|
||||
<Trans
|
||||
@ -127,7 +127,7 @@ const Installed: FC<Props> = ({
|
||||
/>
|
||||
</p>
|
||||
{!isDifyVersionCompatible && (
|
||||
<p className="system-md-regular flex items-center gap-1 text-text-warning">
|
||||
<p className="flex items-center gap-1 text-text-warning system-md-regular">
|
||||
{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: payload.meta.minimum_dify_version })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -5,11 +5,11 @@ import { RiLoader2Line } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { gte } from 'semver'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useInstallPackageFromMarketPlace, usePluginDeclarationFromMarketPlace, usePluginTaskList, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import { gte } from '@/utils/semver'
|
||||
import Card from '../../../card'
|
||||
// import { RiInformation2Line } from '@remixicon/react'
|
||||
import { TaskStatus } from '../../../types'
|
||||
@ -133,10 +133,10 @@ const Installed: FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<div className="text-text-secondary system-md-regular">
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
|
||||
{!isDifyVersionCompatible && (
|
||||
<p className="system-md-regular text-text-warning">
|
||||
<p className="text-text-warning system-md-regular">
|
||||
{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { gte } from 'semver'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import { API_PREFIX } from '@/config'
|
||||
@ -20,6 +19,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { gte } from '@/utils/semver'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import Badge from '../../base/badge'
|
||||
import { Github } from '../../base/icons/src/public/common'
|
||||
@ -164,8 +164,8 @@ const PluginItem: FC<Props> = ({
|
||||
/>
|
||||
{category === PluginCategoryEnum.extension && (
|
||||
<>
|
||||
<div className="system-xs-regular mx-2 text-text-quaternary">·</div>
|
||||
<div className="system-xs-regular flex items-center gap-x-1 overflow-hidden text-text-tertiary">
|
||||
<div className="mx-2 text-text-quaternary system-xs-regular">·</div>
|
||||
<div className="flex items-center gap-x-1 overflow-hidden text-text-tertiary system-xs-regular">
|
||||
<RiLoginCircleLine className="size-3 shrink-0" />
|
||||
<span
|
||||
className="truncate"
|
||||
@ -183,7 +183,7 @@ const PluginItem: FC<Props> = ({
|
||||
&& (
|
||||
<>
|
||||
<a href={`https://github.com/${meta!.repo}`} target="_blank" className="flex items-center gap-1">
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">{t('from', { ns: 'plugin' })}</div>
|
||||
<div className="text-text-tertiary system-2xs-medium-uppercase">{t('from', { ns: 'plugin' })}</div>
|
||||
<div className="flex items-center space-x-0.5 text-text-secondary">
|
||||
<Github className="h-3 w-3" />
|
||||
<div className="system-2xs-semibold-uppercase">GitHub</div>
|
||||
@ -196,7 +196,7 @@ const PluginItem: FC<Props> = ({
|
||||
&& (
|
||||
<>
|
||||
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target="_blank" className="flex items-center gap-0.5">
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">
|
||||
<div className="text-text-tertiary system-2xs-medium-uppercase">
|
||||
{t('from', { ns: 'plugin' })}
|
||||
{' '}
|
||||
<span className="text-text-secondary">marketplace</span>
|
||||
@ -210,7 +210,7 @@ const PluginItem: FC<Props> = ({
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<RiHardDrive3Line className="h-3 w-3 text-text-tertiary" />
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">Local Plugin</div>
|
||||
<div className="text-text-tertiary system-2xs-medium-uppercase">Local Plugin</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@ -219,14 +219,14 @@ const PluginItem: FC<Props> = ({
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<RiBugLine className="h-3 w-3 text-text-warning" />
|
||||
<div className="system-2xs-medium-uppercase text-text-warning">Debugging Plugin</div>
|
||||
<div className="text-text-warning system-2xs-medium-uppercase">Debugging Plugin</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Deprecated */}
|
||||
{source === PluginSource.marketplace && enable_marketplace && isDeprecated && (
|
||||
<div className="system-2xs-medium-uppercase flex shrink-0 items-center gap-x-2">
|
||||
<div className="flex shrink-0 items-center gap-x-2 system-2xs-medium-uppercase">
|
||||
<span className="text-text-tertiary">·</span>
|
||||
<span className="text-text-warning">
|
||||
{t('deprecated', { ns: 'plugin' })}
|
||||
|
||||
@ -4,7 +4,6 @@ import type { Placement } from '@/app/components/base/ui/placement'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { lt } from 'semver'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import {
|
||||
Popover,
|
||||
@ -14,6 +13,7 @@ import {
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { useVersionListOfPlugin } from '@/service/use-plugins'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { lt } from '@/utils/semver'
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
|
||||
@ -2,20 +2,15 @@
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useIsLogin } from '@/service/use-common'
|
||||
import Loading from './base/loading'
|
||||
|
||||
const Splash: FC<PropsWithChildren> = () => {
|
||||
// would auto redirect to signin page if not logged in
|
||||
const { isLoading, data: loginData } = useIsLogin()
|
||||
const isLoggedIn = loginData?.logged_in
|
||||
|
||||
if (isLoading || !isLoggedIn) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999999] flex h-full items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isLoading || !isLoggedIn)
|
||||
return null
|
||||
|
||||
return null
|
||||
}
|
||||
export default React.memo(Splash)
|
||||
|
||||
@ -47,7 +47,7 @@ const EducationApplyAge = () => {
|
||||
setShowModal(undefined)
|
||||
onPlanInfoChanged()
|
||||
updateEducationStatus()
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
globalThis.localStorage?.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
router.replace('/')
|
||||
}
|
||||
|
||||
|
||||
@ -133,7 +133,7 @@ const useEducationReverifyNotice = ({
|
||||
export const useEducationInit = () => {
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
const setShowEducationExpireNoticeModal = useModalContextSelector(s => s.setShowEducationExpireNoticeModal)
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
const educationVerifying = globalThis.localStorage?.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
const searchParams = useSearchParams()
|
||||
const educationVerifyAction = searchParams.get('action')
|
||||
|
||||
@ -156,7 +156,7 @@ export const useEducationInit = () => {
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
|
||||
if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
globalThis.localStorage?.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
}
|
||||
if (educationVerifyAction === EDUCATION_RE_VERIFY_ACTION)
|
||||
handleVerify()
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Link from '@/next/link'
|
||||
|
||||
const Home = async () => {
|
||||
@ -6,7 +5,6 @@ const Home = async () => {
|
||||
<div className="flex min-h-screen flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<Loading type="area" />
|
||||
<div className="mt-10 text-center">
|
||||
<Link href="/apps">🚀</Link>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,6 @@ import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { LICENSE_LINK } from '@/constants/link'
|
||||
@ -65,9 +64,7 @@ export default function InviteSettingsPage() {
|
||||
}
|
||||
}, [language, name, recheck, timezone, token, router, t])
|
||||
|
||||
if (!checkRes)
|
||||
return <Loading />
|
||||
if (!checkRes.is_valid) {
|
||||
if (checkRes?.is_valid === false) {
|
||||
return (
|
||||
<div className="flex flex-col md:w-[400px]">
|
||||
<div className="mx-auto w-full">
|
||||
@ -147,8 +144,9 @@ export default function InviteSettingsPage() {
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={handleActivate}
|
||||
disabled={!checkRes?.is_valid}
|
||||
>
|
||||
{`${t('join', { ns: 'login' })} ${checkRes?.data?.workspace_name}`}
|
||||
{`${t('join', { ns: 'login' })} ${checkRes?.data?.workspace_name ?? ''}`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
74
web/context/__tests__/global-public-context.spec.tsx
Normal file
74
web/context/__tests__/global-public-context.spec.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import GlobalPublicStoreProvider, { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
|
||||
const mockSystemFeatures = vi.fn()
|
||||
const mockFetchSetupStatusWithCache = vi.fn()
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleClient: {
|
||||
systemFeatures: () => mockSystemFeatures(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/setup-status', () => ({
|
||||
fetchSetupStatusWithCache: () => mockFetchSetupStatusWithCache(),
|
||||
}))
|
||||
|
||||
const createQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const renderProvider = () => {
|
||||
const queryClient = createQueryClient()
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GlobalPublicStoreProvider>
|
||||
<div>provider child</div>
|
||||
</GlobalPublicStoreProvider>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('GlobalPublicStoreProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useGlobalPublicStore.setState({ systemFeatures: defaultSystemFeatures })
|
||||
mockFetchSetupStatusWithCache.mockResolvedValue({ setup_status: 'finished' })
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render children when system features are still loading', async () => {
|
||||
mockSystemFeatures.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
renderProvider()
|
||||
|
||||
expect(screen.getByText('provider child')).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockSystemFeatures).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('State Updates', () => {
|
||||
it('should update the public store when system features query succeeds', async () => {
|
||||
mockSystemFeatures.mockResolvedValue({
|
||||
...defaultSystemFeatures,
|
||||
enable_marketplace: true,
|
||||
})
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useGlobalPublicStore.getState().systemFeatures.enable_marketplace).toBe(true)
|
||||
})
|
||||
expect(mockFetchSetupStatusWithCache).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -3,7 +3,6 @@ import type { FC, PropsWithChildren } from 'react'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { create } from 'zustand'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||
@ -53,13 +52,11 @@ const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
|
||||
}) => {
|
||||
// Fetch systemFeatures and setupStatus in parallel to reduce waterfall.
|
||||
// setupStatus is prefetched here and cached in localStorage for AppInitializer.
|
||||
const { isPending } = useSystemFeaturesQuery()
|
||||
useSystemFeaturesQuery()
|
||||
|
||||
// Prefetch setupStatus for AppInitializer (result not needed here)
|
||||
useSetupStatusQuery()
|
||||
|
||||
if (isPending)
|
||||
return <div className="flex h-screen w-screen items-center justify-center"><Loading /></div>
|
||||
return <>{children}</>
|
||||
}
|
||||
export default GlobalPublicStoreProvider
|
||||
|
||||
@ -106,10 +106,10 @@ export const ModalContextProvider = ({
|
||||
|
||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||
const handleCancelAccountSettingModal = () => {
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
const educationVerifying = globalThis.localStorage?.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
if (educationVerifying === 'yes')
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
globalThis.localStorage?.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
||||
accountSettingCallbacksRef.current = null
|
||||
|
||||
@ -6,11 +6,9 @@ import type { AppData, AppMeta } from '@/models/share'
|
||||
import { useEffect } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { usePathname, useSearchParams } from '@/next/navigation'
|
||||
import { useGetWebAppAccessModeByCode } from '@/service/use-share'
|
||||
import { useIsSystemFeaturesPending } from './global-public-context'
|
||||
|
||||
type WebAppStore = {
|
||||
shareCode: string | null
|
||||
@ -65,7 +63,6 @@ const getShareCodeFromPathname = (pathname: string): string | null => {
|
||||
}
|
||||
|
||||
const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const isGlobalPending = useIsSystemFeaturesPending()
|
||||
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
|
||||
const updateShareCode = useWebAppStore(state => state.updateShareCode)
|
||||
const updateEmbeddedUserId = useWebAppStore(state => state.updateEmbeddedUserId)
|
||||
@ -104,24 +101,13 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
}
|
||||
}, [searchParamsString, updateEmbeddedUserId, updateEmbeddedConversationId])
|
||||
|
||||
const { isLoading, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
|
||||
const { data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
|
||||
|
||||
useEffect(() => {
|
||||
if (accessModeResult?.accessMode)
|
||||
updateWebAppAccessMode(accessModeResult.accessMode)
|
||||
}, [accessModeResult, updateWebAppAccessMode, shareCode])
|
||||
|
||||
if (isGlobalPending || isLoading) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
return <>{children}</>
|
||||
}
|
||||
export default WebAppStoreProvider
|
||||
|
||||
@ -1,13 +1,93 @@
|
||||
import semver from 'semver'
|
||||
type SemverIdentifier = number | string
|
||||
|
||||
export const getLatestVersion = (versionList: string[]) => {
|
||||
return semver.rsort(versionList)[0]
|
||||
type ParsedSemver = {
|
||||
core: number[]
|
||||
prerelease: SemverIdentifier[]
|
||||
}
|
||||
|
||||
const parseIdentifier = (value: string): SemverIdentifier => {
|
||||
if (/^\d+$/.test(value))
|
||||
return Number(value)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const parseSemver = (version: string): ParsedSemver => {
|
||||
const normalized = version.trim().replace(/^v/i, '').split('+')[0]
|
||||
const [corePart, prereleasePart] = normalized.split('-', 2)
|
||||
|
||||
return {
|
||||
core: corePart.split('.').map(part => Number(part) || 0),
|
||||
prerelease: prereleasePart
|
||||
? prereleasePart.split('.').filter(Boolean).map(parseIdentifier)
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
const compareIdentifier = (left: SemverIdentifier, right: SemverIdentifier) => {
|
||||
if (typeof left === 'number' && typeof right === 'number')
|
||||
return Math.sign(left - right)
|
||||
|
||||
if (typeof left === 'number')
|
||||
return -1
|
||||
|
||||
if (typeof right === 'number')
|
||||
return 1
|
||||
|
||||
return left.localeCompare(right)
|
||||
}
|
||||
|
||||
const comparePrerelease = (left: SemverIdentifier[], right: SemverIdentifier[]) => {
|
||||
if (!left.length && !right.length)
|
||||
return 0
|
||||
|
||||
if (!left.length)
|
||||
return 1
|
||||
|
||||
if (!right.length)
|
||||
return -1
|
||||
|
||||
const maxLength = Math.max(left.length, right.length)
|
||||
for (let i = 0; i < maxLength; i += 1) {
|
||||
const leftId = left[i]
|
||||
const rightId = right[i]
|
||||
|
||||
if (leftId === undefined)
|
||||
return -1
|
||||
|
||||
if (rightId === undefined)
|
||||
return 1
|
||||
|
||||
const result = compareIdentifier(leftId, rightId)
|
||||
if (result !== 0)
|
||||
return result
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export const compareVersion = (v1: string, v2: string) => {
|
||||
return semver.compare(v1, v2)
|
||||
const left = parseSemver(v1)
|
||||
const right = parseSemver(v2)
|
||||
const maxCoreLength = Math.max(left.core.length, right.core.length)
|
||||
|
||||
for (let i = 0; i < maxCoreLength; i += 1) {
|
||||
const diff = (left.core[i] || 0) - (right.core[i] || 0)
|
||||
if (diff !== 0)
|
||||
return Math.sign(diff)
|
||||
}
|
||||
|
||||
return comparePrerelease(left.prerelease, right.prerelease)
|
||||
}
|
||||
|
||||
export const gte = (v1: string, v2: string) => compareVersion(v1, v2) >= 0
|
||||
|
||||
export const lt = (v1: string, v2: string) => compareVersion(v1, v2) < 0
|
||||
|
||||
export const getLatestVersion = (versionList: string[]) => {
|
||||
return [...versionList].sort((left, right) => compareVersion(right, left))[0]
|
||||
}
|
||||
|
||||
export const isEqualOrLaterThanVersion = (baseVersion: string, targetVersion: string) => {
|
||||
return semver.gte(baseVersion, targetVersion)
|
||||
return gte(baseVersion, targetVersion)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user