| @@ -1,16 +1,18 @@ | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import Main from '@/app/components/explore/installed-app' | |||
| export type IInstalledAppProps = { | |||
| params: Promise<{ | |||
| params: { | |||
| appId: string | |||
| }> | |||
| } | |||
| } | |||
| const InstalledApp: FC<IInstalledAppProps> = async ({ params }) => { | |||
| // Using Next.js page convention for async server components | |||
| async function InstalledApp({ params }: IInstalledAppProps) { | |||
| const appId = (await params).appId | |||
| return ( | |||
| <Main id={(await params).appId} /> | |||
| <Main id={appId} /> | |||
| ) | |||
| } | |||
| export default React.memo(InstalledApp) | |||
| export default InstalledApp | |||
| @@ -1,10 +1,13 @@ | |||
| 'use client' | |||
| import React from 'react' | |||
| import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' | |||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||
| const Chat = () => { | |||
| return ( | |||
| <ChatWithHistoryWrap /> | |||
| <AuthenticatedLayout> | |||
| <ChatWithHistoryWrap /> | |||
| </AuthenticatedLayout> | |||
| ) | |||
| } | |||
| @@ -1,10 +1,13 @@ | |||
| 'use client' | |||
| import React from 'react' | |||
| import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' | |||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||
| const Chatbot = () => { | |||
| return ( | |||
| <EmbeddedChatbot /> | |||
| <AuthenticatedLayout> | |||
| <EmbeddedChatbot /> | |||
| </AuthenticatedLayout> | |||
| ) | |||
| } | |||
| @@ -1,9 +1,12 @@ | |||
| import React from 'react' | |||
| import Main from '@/app/components/share/text-generation' | |||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||
| const Completion = () => { | |||
| return ( | |||
| <Main /> | |||
| <AuthenticatedLayout> | |||
| <Main /> | |||
| </AuthenticatedLayout> | |||
| ) | |||
| } | |||
| @@ -0,0 +1,84 @@ | |||
| 'use client' | |||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { removeAccessToken } from '@/app/components/share/utils' | |||
| import { useWebAppStore } from '@/context/web-app-context' | |||
| import { useGetUserCanAccessApp } from '@/service/access-control' | |||
| import { useGetWebAppInfo, useGetWebAppMeta, useGetWebAppParams } from '@/service/use-share' | |||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||
| import React, { useCallback, useEffect } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { | |||
| const { t } = useTranslation() | |||
| const updateAppInfo = useWebAppStore(s => s.updateAppInfo) | |||
| 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: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false }) | |||
| useEffect(() => { | |||
| if (appInfo) | |||
| updateAppInfo(appInfo) | |||
| if (appParams) | |||
| updateAppParams(appParams) | |||
| if (appMeta) | |||
| updateWebAppMeta(appMeta) | |||
| updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) | |||
| }, [appInfo, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp]) | |||
| const router = useRouter() | |||
| const pathname = usePathname() | |||
| const searchParams = useSearchParams() | |||
| const getSigninUrl = useCallback(() => { | |||
| const params = new URLSearchParams(searchParams) | |||
| params.delete('message') | |||
| params.set('redirect_url', pathname) | |||
| return `/webapp-signin?${params.toString()}` | |||
| }, [searchParams, pathname]) | |||
| const backToHome = useCallback(() => { | |||
| removeAccessToken() | |||
| const url = getSigninUrl() | |||
| router.replace(url) | |||
| }, [getSigninUrl, router]) | |||
| if (appInfoError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={appInfoError.message} /> | |||
| </div> | |||
| } | |||
| if (appParamsError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={appParamsError.message} /> | |||
| </div> | |||
| } | |||
| if (appMetaError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={appMetaError.message} /> | |||
| </div> | |||
| } | |||
| if (useCanAccessAppError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={useCanAccessAppError.message} /> | |||
| </div> | |||
| } | |||
| if (userCanAccessApp && !userCanAccessApp.result) { | |||
| 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('common.userProfile.logout')}</span> | |||
| </div> | |||
| } | |||
| if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| return <>{children}</> | |||
| } | |||
| export default React.memo(AuthenticatedLayout) | |||
| @@ -0,0 +1,80 @@ | |||
| 'use client' | |||
| import type { FC, PropsWithChildren } from 'react' | |||
| import { useEffect } from 'react' | |||
| import { useCallback } from 'react' | |||
| import { useWebAppStore } from '@/context/web-app-context' | |||
| import { useRouter, useSearchParams } from 'next/navigation' | |||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||
| import { checkOrSetAccessToken, removeAccessToken, setAccessToken } from '@/app/components/share/utils' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { fetchAccessToken } from '@/service/share' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { AccessMode } from '@/models/access-control' | |||
| const Splash: FC<PropsWithChildren> = ({ children }) => { | |||
| const { t } = useTranslation() | |||
| const shareCode = useWebAppStore(s => s.shareCode) | |||
| const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) | |||
| const searchParams = useSearchParams() | |||
| const router = useRouter() | |||
| const redirectUrl = searchParams.get('redirect_url') | |||
| const tokenFromUrl = searchParams.get('web_sso_token') | |||
| const message = searchParams.get('message') | |||
| const code = searchParams.get('code') | |||
| const getSigninUrl = useCallback(() => { | |||
| const params = new URLSearchParams(searchParams) | |||
| params.delete('message') | |||
| params.delete('code') | |||
| return `/webapp-signin?${params.toString()}` | |||
| }, [searchParams]) | |||
| const backToHome = useCallback(() => { | |||
| removeAccessToken() | |||
| const url = getSigninUrl() | |||
| router.replace(url) | |||
| }, [getSigninUrl, router]) | |||
| useEffect(() => { | |||
| (async () => { | |||
| if (message) | |||
| return | |||
| if (shareCode && tokenFromUrl && redirectUrl) { | |||
| localStorage.setItem('webapp_access_token', tokenFromUrl) | |||
| const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: tokenFromUrl }) | |||
| await setAccessToken(shareCode, tokenResp.access_token) | |||
| router.replace(decodeURIComponent(redirectUrl)) | |||
| return | |||
| } | |||
| if (shareCode && redirectUrl && localStorage.getItem('webapp_access_token')) { | |||
| const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) | |||
| await setAccessToken(shareCode, tokenResp.access_token) | |||
| router.replace(decodeURIComponent(redirectUrl)) | |||
| return | |||
| } | |||
| if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { | |||
| await checkOrSetAccessToken(shareCode) | |||
| router.replace(decodeURIComponent(redirectUrl)) | |||
| } | |||
| })() | |||
| }, [shareCode, redirectUrl, router, tokenFromUrl, message, webAppAccessMode]) | |||
| if (message) { | |||
| return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | |||
| <AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} /> | |||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span> | |||
| </div> | |||
| } | |||
| if (tokenFromUrl) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| return <>{children}</> | |||
| } | |||
| export default Splash | |||
| @@ -1,54 +1,15 @@ | |||
| 'use client' | |||
| import React, { useEffect, useState } from 'react' | |||
| import type { FC } from 'react' | |||
| import { usePathname, useSearchParams } from 'next/navigation' | |||
| import Loading from '../components/base/loading' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { AccessMode } from '@/models/access-control' | |||
| import { getAppAccessModeByAppCode } from '@/service/share' | |||
| import type { FC, PropsWithChildren } from 'react' | |||
| import WebAppStoreProvider from '@/context/web-app-context' | |||
| import Splash from './components/splash' | |||
| const Layout: FC<{ | |||
| children: React.ReactNode | |||
| }> = ({ children }) => { | |||
| const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) | |||
| const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| const pathname = usePathname() | |||
| const searchParams = useSearchParams() | |||
| const redirectUrl = searchParams.get('redirect_url') | |||
| const [isLoading, setIsLoading] = useState(true) | |||
| useEffect(() => { | |||
| (async () => { | |||
| if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { | |||
| setIsLoading(false) | |||
| return | |||
| } | |||
| let appCode: string | null = null | |||
| if (redirectUrl) { | |||
| const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) | |||
| appCode = url.pathname.split('/').pop() || null | |||
| } | |||
| else { | |||
| appCode = pathname.split('/').pop() || null | |||
| } | |||
| if (!appCode) | |||
| return | |||
| setIsLoading(true) | |||
| const ret = await getAppAccessModeByAppCode(appCode) | |||
| setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) | |||
| setIsLoading(false) | |||
| })() | |||
| }, [pathname, redirectUrl, setWebAppAccessMode, isGlobalPending, systemFeatures.webapp_auth.enabled]) | |||
| if (isLoading || isGlobalPending) { | |||
| return <div className='flex h-full w-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| const Layout: FC<PropsWithChildren> = ({ children }) => { | |||
| return ( | |||
| <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> | |||
| {children} | |||
| <WebAppStoreProvider> | |||
| <Splash> | |||
| {children} | |||
| </Splash> | |||
| </WebAppStoreProvider> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -3,10 +3,13 @@ | |||
| import cn from '@/utils/classnames' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import useDocumentTitle from '@/hooks/use-document-title' | |||
| import type { PropsWithChildren } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| export default function SignInLayout({ children }: any) { | |||
| const { systemFeatures } = useGlobalPublicStore() | |||
| useDocumentTitle('') | |||
| export default function SignInLayout({ children }: PropsWithChildren) { | |||
| const { t } = useTranslation() | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| useDocumentTitle(t('login.webapp.login')) | |||
| return <> | |||
| <div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}> | |||
| <div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}> | |||
| @@ -1,3 +1,4 @@ | |||
| 'use client' | |||
| import React, { useCallback, useEffect, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import Link from 'next/link' | |||
| @@ -1,36 +1,30 @@ | |||
| 'use client' | |||
| import { useRouter, useSearchParams } from 'next/navigation' | |||
| import type { FC } from 'react' | |||
| import React, { useCallback, useEffect } from 'react' | |||
| import React, { useCallback } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import Toast from '@/app/components/base/toast' | |||
| import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' | |||
| import { removeAccessToken } from '@/app/components/share/utils' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import Loading from '@/app/components/base/loading' | |||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||
| import NormalForm from './normalForm' | |||
| import { AccessMode } from '@/models/access-control' | |||
| import ExternalMemberSsoAuth from './components/external-member-sso-auth' | |||
| import { fetchAccessToken } from '@/service/share' | |||
| import { useWebAppStore } from '@/context/web-app-context' | |||
| const WebSSOForm: FC = () => { | |||
| const { t } = useTranslation() | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) | |||
| const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) | |||
| const searchParams = useSearchParams() | |||
| const router = useRouter() | |||
| const redirectUrl = searchParams.get('redirect_url') | |||
| const tokenFromUrl = searchParams.get('web_sso_token') | |||
| const message = searchParams.get('message') | |||
| const code = searchParams.get('code') | |||
| const getSigninUrl = useCallback(() => { | |||
| const params = new URLSearchParams(searchParams) | |||
| params.delete('message') | |||
| params.delete('code') | |||
| const params = new URLSearchParams() | |||
| params.append('redirect_url', redirectUrl || '') | |||
| return `/webapp-signin?${params.toString()}` | |||
| }, [searchParams]) | |||
| }, [redirectUrl]) | |||
| const backToHome = useCallback(() => { | |||
| removeAccessToken() | |||
| @@ -38,73 +32,12 @@ const WebSSOForm: FC = () => { | |||
| router.replace(url) | |||
| }, [getSigninUrl, router]) | |||
| const showErrorToast = (msg: string) => { | |||
| Toast.notify({ | |||
| type: 'error', | |||
| message: msg, | |||
| }) | |||
| } | |||
| const getAppCodeFromRedirectUrl = useCallback(() => { | |||
| if (!redirectUrl) | |||
| return null | |||
| const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) | |||
| const appCode = url.pathname.split('/').pop() | |||
| if (!appCode) | |||
| return null | |||
| return appCode | |||
| }, [redirectUrl]) | |||
| useEffect(() => { | |||
| (async () => { | |||
| if (message) | |||
| return | |||
| const appCode = getAppCodeFromRedirectUrl() | |||
| if (appCode && tokenFromUrl && redirectUrl) { | |||
| localStorage.setItem('webapp_access_token', tokenFromUrl) | |||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl }) | |||
| await setAccessToken(appCode, tokenResp.access_token) | |||
| router.replace(decodeURIComponent(redirectUrl)) | |||
| return | |||
| } | |||
| if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) { | |||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) | |||
| await setAccessToken(appCode, tokenResp.access_token) | |||
| router.replace(decodeURIComponent(redirectUrl)) | |||
| } | |||
| })() | |||
| }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) | |||
| useEffect(() => { | |||
| if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) | |||
| router.replace(decodeURIComponent(redirectUrl)) | |||
| }, [webAppAccessMode, router, redirectUrl]) | |||
| if (tokenFromUrl) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| if (message) { | |||
| return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | |||
| <AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} /> | |||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span> | |||
| </div> | |||
| } | |||
| if (!redirectUrl) { | |||
| showErrorToast('redirect url is invalid.') | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' /> | |||
| </div> | |||
| } | |||
| if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| if (!systemFeatures.webapp_auth.enabled) { | |||
| return <div className="flex h-full items-center justify-center"> | |||
| <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> | |||
| @@ -1,10 +1,13 @@ | |||
| import React from 'react' | |||
| import Main from '@/app/components/share/text-generation' | |||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||
| const Workflow = () => { | |||
| return ( | |||
| <Main isWorkflow /> | |||
| <AuthenticatedLayout> | |||
| <Main isWorkflow /> | |||
| </AuthenticatedLayout> | |||
| ) | |||
| } | |||
| @@ -18,11 +18,8 @@ import type { | |||
| import { noop } from 'lodash-es' | |||
| export type ChatWithHistoryContextValue = { | |||
| appInfoError?: any | |||
| appInfoLoading?: boolean | |||
| appMeta?: AppMeta | |||
| appData?: AppData | |||
| userCanAccess?: boolean | |||
| appMeta?: AppMeta | null | |||
| appData?: AppData | null | |||
| appParams?: ChatConfig | |||
| appChatListDataLoading?: boolean | |||
| currentConversationId: string | |||
| @@ -62,7 +59,6 @@ export type ChatWithHistoryContextValue = { | |||
| } | |||
| export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ | |||
| userCanAccess: false, | |||
| currentConversationId: '', | |||
| appPrevChatTree: [], | |||
| pinnedConversationList: [], | |||
| @@ -21,9 +21,6 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils' | |||
| import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | |||
| import { | |||
| delConversation, | |||
| fetchAppInfo, | |||
| fetchAppMeta, | |||
| fetchAppParams, | |||
| fetchChatList, | |||
| fetchConversations, | |||
| generationConversationName, | |||
| @@ -43,8 +40,7 @@ import { useAppFavicon } from '@/hooks/use-app-favicon' | |||
| import { InputVarType } from '@/app/components/workflow/types' | |||
| import { TransferMethod } from '@/types/app' | |||
| import { noop } from 'lodash-es' | |||
| import { useGetUserCanAccessApp } from '@/service/access-control' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { useWebAppStore } from '@/context/web-app-context' | |||
| function getFormattedChatList(messages: any[]) { | |||
| const newChatList: ChatItem[] = [] | |||
| @@ -74,13 +70,9 @@ function getFormattedChatList(messages: any[]) { | |||
| export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |||
| const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) | |||
| const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ | |||
| appId: installedAppInfo?.app.id || appInfo?.app_id, | |||
| isInstalledApp, | |||
| enabled: systemFeatures.webapp_auth.enabled, | |||
| }) | |||
| const appInfo = useWebAppStore(s => s.appInfo) | |||
| const appParams = useWebAppStore(s => s.appParams) | |||
| const appMeta = useWebAppStore(s => s.appMeta) | |||
| useAppFavicon({ | |||
| enable: !installedAppInfo, | |||
| @@ -107,6 +99,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |||
| use_icon_as_answer_icon: app.use_icon_as_answer_icon, | |||
| }, | |||
| plan: 'basic', | |||
| custom_config: null, | |||
| } as AppData | |||
| } | |||
| @@ -166,8 +159,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |||
| return currentConversationId | |||
| }, [currentConversationId, newConversationId]) | |||
| const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId)) | |||
| const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId)) | |||
| const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) | |||
| const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) | |||
| const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) | |||
| @@ -485,9 +476,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |||
| }, [isInstalledApp, appId, t, notify]) | |||
| return { | |||
| appInfoError, | |||
| appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), | |||
| userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, | |||
| isInstalledApp, | |||
| appId, | |||
| currentConversationId, | |||
| @@ -1,7 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import { | |||
| useCallback, | |||
| useEffect, | |||
| useState, | |||
| } from 'react' | |||
| @@ -19,12 +18,10 @@ import ChatWrapper from './chat-wrapper' | |||
| import type { InstalledApp } from '@/models/explore' | |||
| import Loading from '@/app/components/base/loading' | |||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||
| import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' | |||
| import { checkOrSetAccessToken } from '@/app/components/share/utils' | |||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||
| import cn from '@/utils/classnames' | |||
| import useDocumentTitle from '@/hooks/use-document-title' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||
| type ChatWithHistoryProps = { | |||
| className?: string | |||
| @@ -33,16 +30,12 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({ | |||
| className, | |||
| }) => { | |||
| const { | |||
| userCanAccess, | |||
| appInfoError, | |||
| appData, | |||
| appInfoLoading, | |||
| appChatListDataLoading, | |||
| chatShouldReloadKey, | |||
| isMobile, | |||
| themeBuilder, | |||
| sidebarCollapseState, | |||
| isInstalledApp, | |||
| } = useChatWithHistoryContext() | |||
| const isSidebarCollapsed = sidebarCollapseState | |||
| const customConfig = appData?.custom_config | |||
| @@ -56,41 +49,6 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({ | |||
| useDocumentTitle(site?.title || 'Chat') | |||
| const { t } = useTranslation() | |||
| const searchParams = useSearchParams() | |||
| const router = useRouter() | |||
| const pathname = usePathname() | |||
| const getSigninUrl = useCallback(() => { | |||
| const params = new URLSearchParams(searchParams) | |||
| params.delete('message') | |||
| params.set('redirect_url', pathname) | |||
| return `/webapp-signin?${params.toString()}` | |||
| }, [searchParams, pathname]) | |||
| const backToHome = useCallback(() => { | |||
| removeAccessToken() | |||
| const url = getSigninUrl() | |||
| router.replace(url) | |||
| }, [getSigninUrl, router]) | |||
| if (appInfoLoading) { | |||
| return ( | |||
| <Loading type='app' /> | |||
| ) | |||
| } | |||
| if (!userCanAccess) { | |||
| 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.' /> | |||
| {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} | |||
| </div> | |||
| } | |||
| if (appInfoError) { | |||
| return ( | |||
| <AppUnavailable /> | |||
| ) | |||
| } | |||
| return ( | |||
| <div className={cn( | |||
| 'flex h-full bg-background-default-burn', | |||
| @@ -148,9 +106,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({ | |||
| const themeBuilder = useThemeContext() | |||
| const { | |||
| appInfoError, | |||
| appInfoLoading, | |||
| userCanAccess, | |||
| appData, | |||
| appParams, | |||
| appMeta, | |||
| @@ -191,10 +146,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({ | |||
| return ( | |||
| <ChatWithHistoryContext.Provider value={{ | |||
| appInfoError, | |||
| appInfoLoading, | |||
| appData, | |||
| userCanAccess, | |||
| appParams, | |||
| appMeta, | |||
| appChatListDataLoading, | |||
| @@ -1,10 +1,7 @@ | |||
| 'use client' | |||
| import { | |||
| useCallback, | |||
| useEffect, | |||
| useState, | |||
| } from 'react' | |||
| import { useAsyncEffect } from 'ahooks' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| EmbeddedChatbotContext, | |||
| @@ -14,8 +11,6 @@ import { useEmbeddedChatbot } from './hooks' | |||
| import { isDify } from './utils' | |||
| import { useThemeContext } from './theme/theme-context' | |||
| import { CssTransform } from './theme/utils' | |||
| import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' | |||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||
| import Loading from '@/app/components/base/loading' | |||
| import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header' | |||
| @@ -25,21 +20,16 @@ import DifyLogo from '@/app/components/base/logo/dify-logo' | |||
| import cn from '@/utils/classnames' | |||
| import useDocumentTitle from '@/hooks/use-document-title' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||
| const Chatbot = () => { | |||
| const { | |||
| userCanAccess, | |||
| isMobile, | |||
| allowResetChat, | |||
| appInfoError, | |||
| appInfoLoading, | |||
| appData, | |||
| appChatListDataLoading, | |||
| chatShouldReloadKey, | |||
| handleNewConversation, | |||
| themeBuilder, | |||
| isInstalledApp, | |||
| } = useEmbeddedChatbotContext() | |||
| const { t } = useTranslation() | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| @@ -55,58 +45,6 @@ const Chatbot = () => { | |||
| useDocumentTitle(site?.title || 'Chat') | |||
| const searchParams = useSearchParams() | |||
| const router = useRouter() | |||
| const pathname = usePathname() | |||
| const getSigninUrl = useCallback(() => { | |||
| const params = new URLSearchParams(searchParams) | |||
| params.delete('message') | |||
| params.set('redirect_url', pathname) | |||
| return `/webapp-signin?${params.toString()}` | |||
| }, [searchParams, pathname]) | |||
| const backToHome = useCallback(() => { | |||
| removeAccessToken() | |||
| const url = getSigninUrl() | |||
| router.replace(url) | |||
| }, [getSigninUrl, router]) | |||
| if (appInfoLoading) { | |||
| return ( | |||
| <> | |||
| {!isMobile && <Loading type='app' />} | |||
| {isMobile && ( | |||
| <div className={cn('relative')}> | |||
| <div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}> | |||
| <Loading type='app' /> | |||
| </div> | |||
| </div> | |||
| )} | |||
| </> | |||
| ) | |||
| } | |||
| if (!userCanAccess) { | |||
| 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.' /> | |||
| {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} | |||
| </div> | |||
| } | |||
| if (appInfoError) { | |||
| return ( | |||
| <> | |||
| {!isMobile && <AppUnavailable />} | |||
| {isMobile && ( | |||
| <div className={cn('relative')}> | |||
| <div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}> | |||
| <AppUnavailable /> | |||
| </div> | |||
| </div> | |||
| )} | |||
| </> | |||
| ) | |||
| } | |||
| return ( | |||
| <div className='relative'> | |||
| <div | |||
| @@ -162,8 +100,6 @@ const EmbeddedChatbotWrapper = () => { | |||
| const themeBuilder = useThemeContext() | |||
| const { | |||
| appInfoError, | |||
| appInfoLoading, | |||
| appData, | |||
| userCanAccess, | |||
| appParams, | |||
| @@ -200,8 +136,6 @@ const EmbeddedChatbotWrapper = () => { | |||
| return <EmbeddedChatbotContext.Provider value={{ | |||
| userCanAccess, | |||
| appInfoError, | |||
| appInfoLoading, | |||
| appData, | |||
| appParams, | |||
| appMeta, | |||
| @@ -241,34 +175,6 @@ const EmbeddedChatbotWrapper = () => { | |||
| } | |||
| const EmbeddedChatbot = () => { | |||
| const [initialized, setInitialized] = useState(false) | |||
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | |||
| const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false) | |||
| useAsyncEffect(async () => { | |||
| if (!initialized) { | |||
| try { | |||
| await checkOrSetAccessToken() | |||
| } | |||
| catch (e: any) { | |||
| if (e.status === 404) { | |||
| setAppUnavailable(true) | |||
| } | |||
| else { | |||
| setIsUnknownReason(true) | |||
| setAppUnavailable(true) | |||
| } | |||
| } | |||
| setInitialized(true) | |||
| } | |||
| }, []) | |||
| if (!initialized) | |||
| return null | |||
| if (appUnavailable) | |||
| return <AppUnavailable isUnknownReason={isUnknownReason} /> | |||
| return <EmbeddedChatbotWrapper /> | |||
| } | |||
| @@ -49,6 +49,16 @@ export type ChatConfig = Omit<ModelConfig, 'model'> & { | |||
| questionEditEnable?: boolean | |||
| supportFeedback?: boolean | |||
| supportCitationHitInfo?: boolean | |||
| system_parameters: { | |||
| audio_file_size_limit: number | |||
| file_size_limit: number | |||
| image_file_size_limit: number | |||
| video_file_size_limit: number | |||
| workflow_file_upload_limit: number | |||
| } | |||
| more_like_this: { | |||
| enabled: boolean | |||
| } | |||
| } | |||
| export type WorkflowProcess = { | |||
| @@ -22,6 +22,7 @@ const Explore: FC<IExploreProps> = ({ | |||
| const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() | |||
| const [hasEditPermission, setHasEditPermission] = useState(false) | |||
| const [installedApps, setInstalledApps] = useState<InstalledApp[]>([]) | |||
| const [isFetchingInstalledApps, setIsFetchingInstalledApps] = useState(false) | |||
| const { t } = useTranslation() | |||
| useDocumentTitle(t('common.menus.explore')) | |||
| @@ -51,6 +52,8 @@ const Explore: FC<IExploreProps> = ({ | |||
| hasEditPermission, | |||
| installedApps, | |||
| setInstalledApps, | |||
| isFetchingInstalledApps, | |||
| setIsFetchingInstalledApps, | |||
| } | |||
| } | |||
| > | |||
| @@ -1,11 +1,17 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import { useEffect } from 'react' | |||
| import React from 'react' | |||
| import { useContext } from 'use-context-selector' | |||
| import ExploreContext from '@/context/explore-context' | |||
| import TextGenerationApp from '@/app/components/share/text-generation' | |||
| import Loading from '@/app/components/base/loading' | |||
| import ChatWithHistory from '@/app/components/base/chat/chat-with-history' | |||
| import { useWebAppStore } from '@/context/web-app-context' | |||
| import AppUnavailable from '../../base/app-unavailable' | |||
| import { useGetUserCanAccessApp } from '@/service/access-control' | |||
| import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore' | |||
| import type { AppData } from '@/models/share' | |||
| export type IInstalledAppProps = { | |||
| id: string | |||
| @@ -14,26 +20,95 @@ export type IInstalledAppProps = { | |||
| const InstalledApp: FC<IInstalledAppProps> = ({ | |||
| id, | |||
| }) => { | |||
| const { installedApps } = useContext(ExploreContext) | |||
| const { installedApps, isFetchingInstalledApps } = useContext(ExploreContext) | |||
| const updateAppInfo = useWebAppStore(s => s.updateAppInfo) | |||
| const installedApp = installedApps.find(item => item.id === id) | |||
| const updateWebAppAccessMode = useWebAppStore(s => s.updateWebAppAccessMode) | |||
| const updateAppParams = useWebAppStore(s => s.updateAppParams) | |||
| const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) | |||
| const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) | |||
| const { isFetching: isFetchingWebAppAccessMode, data: webAppAccessMode, error: webAppAccessModeError } = useGetInstalledAppAccessModeByAppId(installedApp?.id ?? null) | |||
| const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetInstalledAppParams(installedApp?.id ?? null) | |||
| const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null) | |||
| const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: installedApp?.app.id, isInstalledApp: true }) | |||
| useEffect(() => { | |||
| if (!installedApp) { | |||
| updateAppInfo(null) | |||
| } | |||
| else { | |||
| const { id, app } = installedApp | |||
| updateAppInfo({ | |||
| app_id: id, | |||
| site: { | |||
| title: app.name, | |||
| icon_type: app.icon_type, | |||
| icon: app.icon, | |||
| icon_background: app.icon_background, | |||
| icon_url: app.icon_url, | |||
| prompt_public: false, | |||
| copyright: '', | |||
| show_workflow_steps: true, | |||
| use_icon_as_answer_icon: app.use_icon_as_answer_icon, | |||
| }, | |||
| plan: 'basic', | |||
| custom_config: null, | |||
| } as AppData) | |||
| } | |||
| if (appParams) | |||
| updateAppParams(appParams) | |||
| if (appMeta) | |||
| updateWebAppMeta(appMeta) | |||
| if (webAppAccessMode) | |||
| updateWebAppAccessMode(webAppAccessMode.accessMode) | |||
| updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) | |||
| }, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode]) | |||
| if (appParamsError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={appParamsError.message} /> | |||
| </div> | |||
| } | |||
| if (appMetaError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={appMetaError.message} /> | |||
| </div> | |||
| } | |||
| if (useCanAccessAppError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={useCanAccessAppError.message} /> | |||
| </div> | |||
| } | |||
| if (webAppAccessModeError) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable unknownReason={webAppAccessModeError.message} /> | |||
| </div> | |||
| } | |||
| if (userCanAccessApp && !userCanAccessApp.result) { | |||
| 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.' /> | |||
| </div> | |||
| } | |||
| if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || isFetchingInstalledApps) { | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| if (!installedApp) { | |||
| return ( | |||
| <div className='flex h-full items-center'> | |||
| <Loading type='area' /> | |||
| </div> | |||
| ) | |||
| return <div className='flex h-full items-center justify-center'> | |||
| <AppUnavailable code={404} isUnknownReason /> | |||
| </div> | |||
| } | |||
| return ( | |||
| <div className='h-full bg-background-default py-2 pl-0 pr-2 sm:p-2'> | |||
| {installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && ( | |||
| {installedApp?.app.mode !== 'completion' && installedApp?.app.mode !== 'workflow' && ( | |||
| <ChatWithHistory installedAppInfo={installedApp} className='overflow-hidden rounded-2xl shadow-md' /> | |||
| )} | |||
| {installedApp.app.mode === 'completion' && ( | |||
| {installedApp?.app.mode === 'completion' && ( | |||
| <TextGenerationApp isInstalledApp installedAppInfo={installedApp} /> | |||
| )} | |||
| {installedApp.app.mode === 'workflow' && ( | |||
| {installedApp?.app.mode === 'workflow' && ( | |||
| <TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} /> | |||
| )} | |||
| </div> | |||
| @@ -8,11 +8,11 @@ import Link from 'next/link' | |||
| import Toast from '../../base/toast' | |||
| import Item from './app-nav-item' | |||
| import cn from '@/utils/classnames' | |||
| import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore' | |||
| import ExploreContext from '@/context/explore-context' | |||
| import Confirm from '@/app/components/base/confirm' | |||
| import Divider from '@/app/components/base/divider' | |||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||
| import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore' | |||
| const SelectedDiscoveryIcon = () => ( | |||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg"> | |||
| @@ -50,16 +50,14 @@ const SideBar: FC<IExploreSideBarProps> = ({ | |||
| const lastSegment = segments.slice(-1)[0] | |||
| const isDiscoverySelected = lastSegment === 'apps' | |||
| const isChatSelected = lastSegment === 'chat' | |||
| const { installedApps, setInstalledApps } = useContext(ExploreContext) | |||
| const { installedApps, setInstalledApps, setIsFetchingInstalledApps } = useContext(ExploreContext) | |||
| const { isFetching: isFetchingInstalledApps, data: ret, refetch: fetchInstalledAppList } = useGetInstalledApps() | |||
| const { mutateAsync: uninstallApp } = useUninstallApp() | |||
| const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus() | |||
| const media = useBreakpoints() | |||
| const isMobile = media === MediaType.mobile | |||
| const fetchInstalledAppList = async () => { | |||
| const { installed_apps }: any = await doFetchInstalledAppList() | |||
| setInstalledApps(installed_apps) | |||
| } | |||
| const [showConfirm, setShowConfirm] = useState(false) | |||
| const [currId, setCurrId] = useState('') | |||
| const handleDelete = async () => { | |||
| @@ -70,25 +68,31 @@ const SideBar: FC<IExploreSideBarProps> = ({ | |||
| type: 'success', | |||
| message: t('common.api.remove'), | |||
| }) | |||
| fetchInstalledAppList() | |||
| } | |||
| const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { | |||
| await updatePinStatus(id, isPinned) | |||
| await updatePinStatus({ appId: id, isPinned }) | |||
| Toast.notify({ | |||
| type: 'success', | |||
| message: t('common.api.success'), | |||
| }) | |||
| fetchInstalledAppList() | |||
| } | |||
| useEffect(() => { | |||
| fetchInstalledAppList() | |||
| }, []) | |||
| const installed_apps = (ret as any)?.installed_apps | |||
| if (installed_apps && installed_apps.length > 0) | |||
| setInstalledApps(installed_apps) | |||
| else | |||
| setInstalledApps([]) | |||
| }, [ret, setInstalledApps]) | |||
| useEffect(() => { | |||
| setIsFetchingInstalledApps(isFetchingInstalledApps) | |||
| }, [isFetchingInstalledApps, setIsFetchingInstalledApps]) | |||
| useEffect(() => { | |||
| fetchInstalledAppList() | |||
| }, [controlUpdateInstalledApps]) | |||
| }, [controlUpdateInstalledApps, fetchInstalledAppList]) | |||
| const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length | |||
| return ( | |||
| @@ -7,16 +7,14 @@ import { | |||
| RiErrorWarningFill, | |||
| } from '@remixicon/react' | |||
| import { useBoolean } from 'ahooks' | |||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||
| import { useSearchParams } from 'next/navigation' | |||
| import TabHeader from '../../base/tab-header' | |||
| import { checkOrSetAccessToken, removeAccessToken } from '../utils' | |||
| import MenuDropdown from './menu-dropdown' | |||
| import RunBatch from './run-batch' | |||
| import ResDownload from './run-batch/res-download' | |||
| import AppUnavailable from '../../base/app-unavailable' | |||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |||
| import RunOnce from '@/app/components/share/text-generation/run-once' | |||
| import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share' | |||
| import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' | |||
| import type { SiteInfo } from '@/models/share' | |||
| import type { | |||
| MoreLikeThisConfig, | |||
| @@ -39,10 +37,10 @@ import { Resolution, TransferMethod } from '@/types/app' | |||
| import { useAppFavicon } from '@/hooks/use-app-favicon' | |||
| import DifyLogo from '@/app/components/base/logo/dify-logo' | |||
| import cn from '@/utils/classnames' | |||
| import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' | |||
| import { AccessMode } from '@/models/access-control' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import useDocumentTitle from '@/hooks/use-document-title' | |||
| import { useWebAppStore } from '@/context/web-app-context' | |||
| const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. | |||
| enum TaskStatus { | |||
| @@ -83,9 +81,6 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| const mode = searchParams.get('mode') || 'create' | |||
| const [currentTab, setCurrentTab] = useState<string>(['create', 'batch'].includes(mode) ? mode : 'create') | |||
| const router = useRouter() | |||
| const pathname = usePathname() | |||
| // Notice this situation isCallBatchAPI but not in batch tab | |||
| const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) | |||
| const isInBatchTab = currentTab === 'batch' | |||
| @@ -103,30 +98,19 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null) | |||
| const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) | |||
| const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ | |||
| appId, | |||
| isInstalledApp, | |||
| enabled: systemFeatures.webapp_auth.enabled, | |||
| }) | |||
| const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ | |||
| appId, | |||
| isInstalledApp, | |||
| enabled: systemFeatures.webapp_auth.enabled, | |||
| }) | |||
| // save message | |||
| const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([]) | |||
| const fetchSavedMessage = async () => { | |||
| const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) | |||
| const fetchSavedMessage = useCallback(async () => { | |||
| const res: any = await doFetchSavedMessage(isInstalledApp, appId) | |||
| setSavedMessages(res.data) | |||
| } | |||
| }, [isInstalledApp, appId]) | |||
| const handleSaveMessage = async (messageId: string) => { | |||
| await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) | |||
| await saveMessage(messageId, isInstalledApp, appId) | |||
| notify({ type: 'success', message: t('common.api.saved') }) | |||
| fetchSavedMessage() | |||
| } | |||
| const handleRemoveSavedMessage = async (messageId: string) => { | |||
| await removeMessage(messageId, isInstalledApp, installedAppInfo?.id) | |||
| await removeMessage(messageId, isInstalledApp, appId) | |||
| notify({ type: 'success', message: t('common.api.remove') }) | |||
| fetchSavedMessage() | |||
| } | |||
| @@ -375,34 +359,14 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| } | |||
| } | |||
| const fetchInitData = async () => { | |||
| if (!isInstalledApp) | |||
| await checkOrSetAccessToken() | |||
| return Promise.all([ | |||
| isInstalledApp | |||
| ? { | |||
| app_id: installedAppInfo?.id, | |||
| site: { | |||
| title: installedAppInfo?.app.name, | |||
| prompt_public: false, | |||
| copyright: '', | |||
| icon: installedAppInfo?.app.icon, | |||
| icon_background: installedAppInfo?.app.icon_background, | |||
| }, | |||
| plan: 'basic', | |||
| } | |||
| : fetchAppInfo(), | |||
| fetchAppParams(isInstalledApp, installedAppInfo?.id), | |||
| !isWorkflow | |||
| ? fetchSavedMessage() | |||
| : {}, | |||
| ]) | |||
| } | |||
| const appData = useWebAppStore(s => s.appInfo) | |||
| const appParams = useWebAppStore(s => s.appParams) | |||
| const accessMode = useWebAppStore(s => s.webAppAccessMode) | |||
| useEffect(() => { | |||
| (async () => { | |||
| const [appData, appParams]: any = await fetchInitData() | |||
| if (!appData || !appParams) | |||
| return | |||
| !isWorkflow && fetchSavedMessage() | |||
| const { app_id: appId, site: siteInfo, custom_config } = appData | |||
| setAppId(appId) | |||
| setSiteInfo(siteInfo as SiteInfo) | |||
| @@ -413,11 +377,11 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| setVisionConfig({ | |||
| // legacy of image upload compatible | |||
| ...file_upload, | |||
| transfer_methods: file_upload.allowed_file_upload_methods || file_upload.allowed_upload_methods, | |||
| transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods, | |||
| // legacy of image upload compatible | |||
| image_file_size_limit: appParams?.system_parameters?.image_file_size_limit, | |||
| image_file_size_limit: appParams?.system_parameters.image_file_size_limit, | |||
| fileUploadConfig: appParams?.system_parameters, | |||
| }) | |||
| } as any) | |||
| const prompt_variables = userInputsFormToPromptVariables(user_input_form) | |||
| setPromptConfig({ | |||
| prompt_template: '', // placeholder for future | |||
| @@ -426,7 +390,7 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| setMoreLikeThisConfig(more_like_this) | |||
| setTextToSpeechConfig(text_to_speech) | |||
| })() | |||
| }, []) | |||
| }, [appData, appParams, fetchSavedMessage, isWorkflow]) | |||
| // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. | |||
| useDocumentTitle(siteInfo?.title || t('share.generation.title')) | |||
| @@ -528,32 +492,12 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| </div> | |||
| ) | |||
| const getSigninUrl = useCallback(() => { | |||
| const params = new URLSearchParams(searchParams) | |||
| params.delete('message') | |||
| params.set('redirect_url', pathname) | |||
| return `/webapp-signin?${params.toString()}` | |||
| }, [searchParams, pathname]) | |||
| const backToHome = useCallback(() => { | |||
| removeAccessToken() | |||
| const url = getSigninUrl() | |||
| router.replace(url) | |||
| }, [getSigninUrl, router]) | |||
| if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { | |||
| if (!appId || !siteInfo || !promptConfig) { | |||
| return ( | |||
| <div className='flex h-screen items-center'> | |||
| <Loading type='app' /> | |||
| </div>) | |||
| } | |||
| if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) { | |||
| 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.' /> | |||
| {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} | |||
| </div> | |||
| } | |||
| return ( | |||
| <div className={cn( | |||
| 'bg-background-default-burn', | |||
| @@ -578,7 +522,7 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| imageUrl={siteInfo.icon_url} | |||
| /> | |||
| <div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div> | |||
| <MenuDropdown hideLogout={isInstalledApp || appAccessMode?.accessMode === AccessMode.PUBLIC} data={siteInfo} /> | |||
| <MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} data={siteInfo} /> | |||
| </div> | |||
| {siteInfo.description && ( | |||
| <div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div> | |||
| @@ -18,8 +18,8 @@ import { | |||
| import ThemeSwitcher from '@/app/components/base/theme-switcher' | |||
| import type { SiteInfo } from '@/models/share' | |||
| import cn from '@/utils/classnames' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { AccessMode } from '@/models/access-control' | |||
| import { useWebAppStore } from '@/context/web-app-context' | |||
| type Props = { | |||
| data?: SiteInfo | |||
| @@ -32,7 +32,7 @@ const MenuDropdown: FC<Props> = ({ | |||
| placement, | |||
| hideLogout, | |||
| }) => { | |||
| const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) | |||
| const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) | |||
| const router = useRouter() | |||
| const pathname = usePathname() | |||
| const { t } = useTranslation() | |||
| @@ -10,7 +10,7 @@ export const getInitialTokenV2 = (): Record<string, any> => ({ | |||
| version: 2, | |||
| }) | |||
| export const checkOrSetAccessToken = async (appCode?: string) => { | |||
| export const checkOrSetAccessToken = async (appCode?: string | null) => { | |||
| const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] | |||
| const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id | |||
| const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) | |||
| @@ -8,6 +8,8 @@ type IExplore = { | |||
| hasEditPermission: boolean | |||
| installedApps: InstalledApp[] | |||
| setInstalledApps: (installedApps: InstalledApp[]) => void | |||
| isFetchingInstalledApps: boolean | |||
| setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void | |||
| } | |||
| const ExploreContext = createContext<IExplore>({ | |||
| @@ -16,6 +18,8 @@ const ExploreContext = createContext<IExplore>({ | |||
| hasEditPermission: false, | |||
| installedApps: [], | |||
| setInstalledApps: noop, | |||
| isFetchingInstalledApps: false, | |||
| setIsFetchingInstalledApps: noop, | |||
| }) | |||
| export default ExploreContext | |||
| @@ -7,15 +7,12 @@ import type { SystemFeatures } from '@/types/feature' | |||
| import { defaultSystemFeatures } from '@/types/feature' | |||
| import { getSystemFeatures } from '@/service/common' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { AccessMode } from '@/models/access-control' | |||
| type GlobalPublicStore = { | |||
| isGlobalPending: boolean | |||
| setIsGlobalPending: (isPending: boolean) => void | |||
| systemFeatures: SystemFeatures | |||
| setSystemFeatures: (systemFeatures: SystemFeatures) => void | |||
| webAppAccessMode: AccessMode, | |||
| setWebAppAccessMode: (webAppAccessMode: AccessMode) => void | |||
| } | |||
| export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ | |||
| @@ -23,8 +20,6 @@ export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ | |||
| setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), | |||
| systemFeatures: defaultSystemFeatures, | |||
| setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), | |||
| webAppAccessMode: AccessMode.PUBLIC, | |||
| setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })), | |||
| })) | |||
| const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ | |||
| @@ -0,0 +1,87 @@ | |||
| 'use client' | |||
| import type { ChatConfig } from '@/app/components/base/chat/types' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { AccessMode } from '@/models/access-control' | |||
| import type { AppData, AppMeta } from '@/models/share' | |||
| import { useGetWebAppAccessModeByCode } from '@/service/use-share' | |||
| import { usePathname, useSearchParams } from 'next/navigation' | |||
| import type { FC, PropsWithChildren } from 'react' | |||
| import { useEffect } from 'react' | |||
| import { useState } from 'react' | |||
| import { create } from 'zustand' | |||
| type WebAppStore = { | |||
| shareCode: string | null | |||
| updateShareCode: (shareCode: string | null) => void | |||
| appInfo: AppData | null | |||
| updateAppInfo: (appInfo: AppData | null) => void | |||
| appParams: ChatConfig | null | |||
| updateAppParams: (appParams: ChatConfig | null) => void | |||
| webAppAccessMode: AccessMode | |||
| updateWebAppAccessMode: (accessMode: AccessMode) => void | |||
| appMeta: AppMeta | null | |||
| updateWebAppMeta: (appMeta: AppMeta | null) => void | |||
| userCanAccessApp: boolean | |||
| updateUserCanAccessApp: (canAccess: boolean) => void | |||
| } | |||
| export const useWebAppStore = create<WebAppStore>(set => ({ | |||
| shareCode: null, | |||
| updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })), | |||
| appInfo: null, | |||
| updateAppInfo: (appInfo: AppData | null) => set(() => ({ appInfo })), | |||
| appParams: null, | |||
| updateAppParams: (appParams: ChatConfig | null) => set(() => ({ appParams })), | |||
| webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, | |||
| updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })), | |||
| appMeta: null, | |||
| updateWebAppMeta: (appMeta: AppMeta | null) => set(() => ({ appMeta })), | |||
| userCanAccessApp: false, | |||
| updateUserCanAccessApp: (canAccess: boolean) => set(() => ({ userCanAccessApp: canAccess })), | |||
| })) | |||
| const getShareCodeFromRedirectUrl = (redirectUrl: string | null): string | null => { | |||
| if (!redirectUrl || redirectUrl.length === 0) | |||
| return null | |||
| const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) | |||
| return url.pathname.split('/').pop() || null | |||
| } | |||
| const getShareCodeFromPathname = (pathname: string): string | null => { | |||
| const code = pathname.split('/').pop() || null | |||
| if (code === 'webapp-signin') | |||
| return null | |||
| return code | |||
| } | |||
| const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => { | |||
| const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode) | |||
| const updateShareCode = useWebAppStore(state => state.updateShareCode) | |||
| const pathname = usePathname() | |||
| const searchParams = useSearchParams() | |||
| const redirectUrlParam = searchParams.get('redirect_url') | |||
| const [shareCode, setShareCode] = useState<string | null>(null) | |||
| useEffect(() => { | |||
| const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam) | |||
| const shareCodeFromPathname = getShareCodeFromPathname(pathname) | |||
| const newShareCode = shareCodeFromRedirect || shareCodeFromPathname | |||
| setShareCode(newShareCode) | |||
| updateShareCode(newShareCode) | |||
| }, [pathname, redirectUrlParam, updateShareCode]) | |||
| const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode) | |||
| useEffect(() => { | |||
| if (accessModeResult?.accessMode) | |||
| updateWebAppAccessMode(accessModeResult.accessMode) | |||
| }, [accessModeResult, updateWebAppAccessMode]) | |||
| if (isFetching) { | |||
| return <div className='flex h-full w-full items-center justify-center'> | |||
| <Loading /> | |||
| </div> | |||
| } | |||
| return ( | |||
| <> | |||
| {children} | |||
| </> | |||
| ) | |||
| } | |||
| export default WebAppStoreProvider | |||
| @@ -105,6 +105,7 @@ const translation = { | |||
| licenseInactive: 'License Inactive', | |||
| licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', | |||
| webapp: { | |||
| login: 'Login', | |||
| noLoginMethod: 'Authentication method not configured for web app', | |||
| noLoginMethodTip: 'Please contact the system admin to add an authentication method.', | |||
| disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.', | |||
| @@ -106,6 +106,7 @@ const translation = { | |||
| licenseExpired: 'ライセンスの有効期限が切れています', | |||
| licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。', | |||
| webapp: { | |||
| login: 'ログイン', | |||
| noLoginMethod: 'Web アプリに対して認証方法が構成されていません', | |||
| noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', | |||
| disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', | |||
| @@ -106,6 +106,7 @@ const translation = { | |||
| licenseInactive: '许可证未激活', | |||
| licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', | |||
| webapp: { | |||
| login: '登录', | |||
| noLoginMethod: 'Web 应用未配置身份认证方式', | |||
| noLoginMethodTip: '请联系系统管理员添加身份认证方式', | |||
| disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。', | |||
| @@ -35,7 +35,7 @@ export type AppMeta = { | |||
| export type AppData = { | |||
| app_id: string | |||
| can_replace_logo?: boolean | |||
| custom_config?: Record<string, any> | |||
| custom_config: Record<string, any> | null | |||
| enable_site?: boolean | |||
| end_user_id?: string | |||
| site: SiteInfo | |||
| @@ -1,8 +1,9 @@ | |||
| import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | |||
| import { get, post } from './base' | |||
| import { getAppAccessMode, getUserCanAccess } from './share' | |||
| import { getUserCanAccess } from './share' | |||
| import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control' | |||
| import type { App } from '@/types/app' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| const NAME_SPACE = 'access-control' | |||
| @@ -69,25 +70,18 @@ export const useUpdateAccessMode = () => { | |||
| }) | |||
| } | |||
| export const useGetAppAccessMode = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'app-access-mode', appId], | |||
| queryFn: () => getAppAccessMode(appId!, isInstalledApp), | |||
| enabled: !!appId && enabled, | |||
| staleTime: 0, | |||
| gcTime: 0, | |||
| }) | |||
| } | |||
| export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { | |||
| export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean; }) => { | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'user-can-access-app', appId], | |||
| queryFn: () => getUserCanAccess(appId!, isInstalledApp), | |||
| enabled: !!appId && enabled, | |||
| queryFn: () => { | |||
| if (systemFeatures.webapp_auth.enabled) | |||
| return getUserCanAccess(appId!, isInstalledApp) | |||
| else | |||
| return { result: true } | |||
| }, | |||
| enabled: !!appId, | |||
| staleTime: 0, | |||
| gcTime: 0, | |||
| initialData: { | |||
| result: !enabled, | |||
| }, | |||
| }) | |||
| } | |||
| @@ -413,7 +413,7 @@ export const ssePost = async ( | |||
| if (data.code === 'unauthorized') { | |||
| removeAccessToken() | |||
| globalThis.location.reload() | |||
| requiredWebSSOLogin() | |||
| } | |||
| } | |||
| }) | |||
| @@ -507,7 +507,7 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther | |||
| } = otherOptionsForBaseFetch | |||
| if (isPublicAPI && code === 'unauthorized') { | |||
| removeAccessToken() | |||
| globalThis.location.reload() | |||
| requiredWebSSOLogin() | |||
| return Promise.reject(err) | |||
| } | |||
| if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { | |||
| @@ -1,5 +1,6 @@ | |||
| import { del, get, patch, post } from './base' | |||
| import type { App, AppCategory } from '@/models/explore' | |||
| import type { AccessMode } from '@/models/access-control' | |||
| export const fetchAppList = () => { | |||
| return get<{ | |||
| @@ -39,3 +40,7 @@ export const updatePinStatus = (id: string, isPinned: boolean) => { | |||
| export const getToolProviders = () => { | |||
| return get('/workspaces/current/tool-providers') | |||
| } | |||
| export const getAppAccessModeByAppId = (appId: string) => { | |||
| return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) | |||
| } | |||
| @@ -296,13 +296,6 @@ export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { | |||
| return get(url, { headers }) as Promise<{ access_token: string }> | |||
| } | |||
| export const getAppAccessMode = (appId: string, isInstalledApp: boolean) => { | |||
| if (isInstalledApp) | |||
| return consoleGet<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) | |||
| return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appId=${appId}`) | |||
| } | |||
| export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { | |||
| if (isInstalledApp) | |||
| return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) | |||
| @@ -0,0 +1,81 @@ | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { AccessMode } from '@/models/access-control' | |||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | |||
| import { fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' | |||
| import { fetchAppMeta, fetchAppParams } from './share' | |||
| const NAME_SPACE = 'explore' | |||
| export const useGetInstalledApps = () => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'installedApps'], | |||
| queryFn: () => { | |||
| return fetchInstalledAppList() | |||
| }, | |||
| }) | |||
| } | |||
| export const useUninstallApp = () => { | |||
| const client = useQueryClient() | |||
| return useMutation({ | |||
| mutationKey: [NAME_SPACE, 'uninstallApp'], | |||
| mutationFn: (appId: string) => uninstallApp(appId), | |||
| onSuccess: () => { | |||
| client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] }) | |||
| }, | |||
| }) | |||
| } | |||
| export const useUpdateAppPinStatus = () => { | |||
| const client = useQueryClient() | |||
| return useMutation({ | |||
| mutationKey: [NAME_SPACE, 'updateAppPinStatus'], | |||
| mutationFn: ({ appId, isPinned }: { appId: string; isPinned: boolean }) => updatePinStatus(appId, isPinned), | |||
| onSuccess: () => { | |||
| client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] }) | |||
| }, | |||
| }) | |||
| } | |||
| export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => { | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'appAccessMode', appId], | |||
| queryFn: () => { | |||
| if (systemFeatures.webapp_auth.enabled === false) { | |||
| return { | |||
| accessMode: AccessMode.PUBLIC, | |||
| } | |||
| } | |||
| if (!appId || appId.length === 0) | |||
| return Promise.reject(new Error('App code is required to get access mode')) | |||
| return getAppAccessModeByAppId(appId) | |||
| }, | |||
| enabled: !!appId, | |||
| }) | |||
| } | |||
| export const useGetInstalledAppParams = (appId: string | null) => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'appParams', appId], | |||
| queryFn: () => { | |||
| if (!appId || appId.length === 0) | |||
| return Promise.reject(new Error('App ID is required to get app params')) | |||
| return fetchAppParams(true, appId) | |||
| }, | |||
| enabled: !!appId, | |||
| }) | |||
| } | |||
| export const useGetInstalledAppMeta = (appId: string | null) => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'appMeta', appId], | |||
| queryFn: () => { | |||
| if (!appId || appId.length === 0) | |||
| return Promise.reject(new Error('App ID is required to get app meta')) | |||
| return fetchAppMeta(true, appId) | |||
| }, | |||
| enabled: !!appId, | |||
| }) | |||
| } | |||
| @@ -1,17 +1,52 @@ | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import { AccessMode } from '@/models/access-control' | |||
| import { useQuery } from '@tanstack/react-query' | |||
| import { getAppAccessModeByAppCode } from './share' | |||
| import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share' | |||
| const NAME_SPACE = 'webapp' | |||
| export const useAppAccessModeByCode = (code: string | null) => { | |||
| export const useGetWebAppAccessModeByCode = (code: string | null) => { | |||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'appAccessMode', code], | |||
| queryFn: () => { | |||
| if (!code) | |||
| return null | |||
| if (systemFeatures.webapp_auth.enabled === false) { | |||
| return { | |||
| accessMode: AccessMode.PUBLIC, | |||
| } | |||
| } | |||
| if (!code || code.length === 0) | |||
| return Promise.reject(new Error('App code is required to get access mode')) | |||
| return getAppAccessModeByAppCode(code) | |||
| }, | |||
| enabled: !!code, | |||
| }) | |||
| } | |||
| export const useGetWebAppInfo = () => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'appInfo'], | |||
| queryFn: () => { | |||
| return fetchAppInfo() | |||
| }, | |||
| }) | |||
| } | |||
| export const useGetWebAppParams = () => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'appParams'], | |||
| queryFn: () => { | |||
| return fetchAppParams(false) | |||
| }, | |||
| }) | |||
| } | |||
| export const useGetWebAppMeta = () => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'appMeta'], | |||
| queryFn: () => { | |||
| return fetchAppMeta(false) | |||
| }, | |||
| }) | |||
| } | |||