mirror of
https://github.com/langgenius/dify.git
synced 2026-03-01 12:55:13 +00:00
Compare commits
8 Commits
move-token
...
refactor/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ca098eaa5 | ||
|
|
03faf8e91b | ||
|
|
1cf3e599df | ||
|
|
a85946d3e7 | ||
|
|
9e4d3c75ae | ||
|
|
70bea85624 | ||
|
|
b75b7d6c61 | ||
|
|
e819b804ba |
@@ -23,12 +23,14 @@ import AppSideBar from '@/app/components/app-sidebar'
|
|||||||
import { useStore } from '@/app/components/app/store'
|
import { useStore } from '@/app/components/app/store'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { fetchAppDetailDirect } from '@/service/apps'
|
import { fetchAppDetailDirect } from '@/service/apps'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import s from './style.module.css'
|
import s from './style.module.css'
|
||||||
|
|
||||||
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
||||||
@@ -108,7 +110,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appDetail) {
|
if (appDetail) {
|
||||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
const localeMode = storage.get<string>(STORAGE_KEYS.APP.DETAIL_COLLAPSE) || 'expand'
|
||||||
const mode = isMobile ? 'collapse' : 'expand'
|
const mode = isMobile ? 'collapse' : 'expand'
|
||||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||||
// TODO: consider screen size and mode
|
// TODO: consider screen size and mode
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||||
import { isTriggerNode } from '@/app/components/workflow/types'
|
import { isTriggerNode } from '@/app/components/workflow/types'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import {
|
import {
|
||||||
fetchAppDetail,
|
fetchAppDetail,
|
||||||
updateAppSiteAccessToken,
|
updateAppSiteAccessToken,
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { useAppWorkflow } from '@/service/use-workflow'
|
import { useAppWorkflow } from '@/service/use-workflow'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { asyncRunSafe } from '@/utils'
|
import { asyncRunSafe } from '@/utils'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export type ICardViewProps = {
|
export type ICardViewProps = {
|
||||||
appId: string
|
appId: string
|
||||||
@@ -126,7 +127,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||||||
}) as Promise<App>,
|
}) as Promise<App>,
|
||||||
)
|
)
|
||||||
if (!err)
|
if (!err)
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
|
|
||||||
handleCallbackResult(err)
|
handleCallbackResult(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useStore } from '@/app/components/app/store'
|
|||||||
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
|
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import ExtraInfo from '@/app/components/datasets/extra-info'
|
import ExtraInfo from '@/app/components/datasets/extra-info'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import DatasetDetailContext from '@/context/dataset-detail'
|
import DatasetDetailContext from '@/context/dataset-detail'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
@@ -25,6 +26,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
|||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export type IAppDetailLayoutProps = {
|
export type IAppDetailLayoutProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@@ -40,7 +42,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-pipeline')
|
const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-pipeline')
|
||||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||||
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
|
const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)
|
||||||
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand)
|
const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
const localeMode = storage.get<string>(STORAGE_KEYS.APP.DETAIL_COLLAPSE) || 'expand'
|
||||||
const mode = isMobile ? 'collapse' : 'expand'
|
const mode = isMobile ? 'collapse' : 'expand'
|
||||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||||
}, [isMobile, setAppSidebarExpand])
|
}, [isMobile, setAppSidebarExpand])
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
import { COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||||
import { emailRegex } from '@/config'
|
import { emailRegex } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { sendResetPasswordCode } from '@/service/common'
|
import { sendResetPasswordCode } from '@/service/common'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export default function CheckCode() {
|
export default function CheckCode() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -41,7 +43,7 @@ export default function CheckCode() {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const res = await sendResetPasswordCode(email, locale)
|
const res = await sendResetPasswordCode(email, locale)
|
||||||
if (res.result === 'success') {
|
if (res.result === 'success') {
|
||||||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, COUNT_DOWN_TIME_MS)
|
||||||
const params = new URLSearchParams(searchParams)
|
const params = new URLSearchParams(searchParams)
|
||||||
params.set('token', encodeURIComponent(res.data))
|
params.set('token', encodeURIComponent(res.data))
|
||||||
params.set('email', encodeURIComponent(email))
|
params.set('email', encodeURIComponent(email))
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
import { COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||||
import { emailRegex } from '@/config'
|
import { emailRegex } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { sendWebAppEMailLoginCode } from '@/service/common'
|
import { sendWebAppEMailLoginCode } from '@/service/common'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export default function MailAndCodeAuth() {
|
export default function MailAndCodeAuth() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -36,7 +38,7 @@ export default function MailAndCodeAuth() {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const ret = await sendWebAppEMailLoginCode(email, locale)
|
const ret = await sendWebAppEMailLoginCode(email, locale)
|
||||||
if (ret.result === 'success') {
|
if (ret.result === 'success') {
|
||||||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, COUNT_DOWN_TIME_MS)
|
||||||
const params = new URLSearchParams(searchParams)
|
const params = new URLSearchParams(searchParams)
|
||||||
params.set('email', encodeURIComponent(email))
|
params.set('email', encodeURIComponent(email))
|
||||||
params.set('token', encodeURIComponent(ret.data))
|
params.set('token', encodeURIComponent(ret.data))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from '@/app/components/base/button'
|
|||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Modal from '@/app/components/base/modal'
|
import Modal from '@/app/components/base/modal'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import {
|
import {
|
||||||
checkEmailExisted,
|
checkEmailExisted,
|
||||||
resetEmail,
|
resetEmail,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
} from '@/service/common'
|
} from '@/service/common'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import { asyncRunSafe } from '@/utils'
|
import { asyncRunSafe } from '@/utils'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -172,7 +174,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
|
|
||||||
localStorage.removeItem('setup_status')
|
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||||
// Tokens are now stored in cookies and cleared by backend
|
// Tokens are now stored in cookies and cleared by backend
|
||||||
|
|
||||||
router.push('/signin')
|
router.push('/signin')
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import { resetUser } from '@/app/components/base/amplitude/utils'
|
|||||||
import Avatar from '@/app/components/base/avatar'
|
import Avatar from '@/app/components/base/avatar'
|
||||||
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export type IAppSelector = {
|
export type IAppSelector = {
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
@@ -28,7 +30,7 @@ export default function AppSelector() {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
|
|
||||||
localStorage.removeItem('setup_status')
|
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||||
resetUser()
|
resetUser()
|
||||||
// Tokens are now stored in cookies and cleared by backend
|
// Tokens are now stored in cookies and cleared by backend
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import CustomDialog from '@/app/components/base/dialog'
|
import CustomDialog from '@/app/components/base/dialog'
|
||||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
import { COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import CheckEmail from './components/check-email'
|
import CheckEmail from './components/check-email'
|
||||||
import FeedBack from './components/feed-back'
|
import FeedBack from './components/feed-back'
|
||||||
import VerifyEmail from './components/verify-email'
|
import VerifyEmail from './components/verify-email'
|
||||||
@@ -21,7 +23,7 @@ export default function DeleteAccount(props: DeleteAccountProps) {
|
|||||||
const handleEmailCheckSuccess = useCallback(async () => {
|
const handleEmailCheckSuccess = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setShowVerifyEmail(true)
|
setShowVerifyEmail(true)
|
||||||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, COUNT_DOWN_TIME_MS)
|
||||||
}
|
}
|
||||||
catch (error) { console.error(error) }
|
catch (error) { console.error(error) }
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ import Button from '@/app/components/base/button'
|
|||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useIsLogin } from '@/service/use-common'
|
import { useIsLogin } from '@/service/use-common'
|
||||||
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import {
|
import {
|
||||||
OAUTH_AUTHORIZE_PENDING_KEY,
|
|
||||||
OAUTH_AUTHORIZE_PENDING_TTL,
|
OAUTH_AUTHORIZE_PENDING_TTL,
|
||||||
REDIRECT_URL_KEY,
|
REDIRECT_URL_KEY,
|
||||||
} from './constants'
|
} from './constants'
|
||||||
@@ -31,7 +32,7 @@ function setItemWithExpiry(key: string, value: string, ttl: number) {
|
|||||||
value,
|
value,
|
||||||
expiry: dayjs().add(ttl, 'seconds').unix(),
|
expiry: dayjs().add(ttl, 'seconds').unix(),
|
||||||
}
|
}
|
||||||
localStorage.setItem(key, JSON.stringify(item))
|
storage.set(key, JSON.stringify(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReturnUrl(pathname: string, search: string) {
|
function buildReturnUrl(pathname: string, search: string) {
|
||||||
@@ -86,7 +87,7 @@ export default function OAuthAuthorize() {
|
|||||||
const onLoginSwitchClick = () => {
|
const onLoginSwitchClick = () => {
|
||||||
try {
|
try {
|
||||||
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
|
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
|
||||||
setItemWithExpiry(OAUTH_AUTHORIZE_PENDING_KEY, returnUrl, OAUTH_AUTHORIZE_PENDING_TTL)
|
setItemWithExpiry(STORAGE_KEYS.AUTH.OAUTH_AUTHORIZE_PENDING, returnUrl, OAUTH_AUTHORIZE_PENDING_TTL)
|
||||||
router.push(`/signin?${REDIRECT_URL_KEY}=${encodeURIComponent(returnUrl)}`)
|
router.push(`/signin?${REDIRECT_URL_KEY}=${encodeURIComponent(returnUrl)}`)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ import { parseAsString, useQueryState } from 'nuqs'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
|
||||||
} from '@/app/education-apply/constants'
|
} from '@/app/education-apply/constants'
|
||||||
|
import { LEGACY_KEY_MIGRATIONS, STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { sendGAEvent } from '@/utils/gtag'
|
import { sendGAEvent } from '@/utils/gtag'
|
||||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
||||||
import { trackEvent } from './base/amplitude'
|
import { trackEvent } from './base/amplitude'
|
||||||
|
|
||||||
|
storage.runMigrations(LEGACY_KEY_MIGRATIONS)
|
||||||
|
|
||||||
type AppInitializerProps = {
|
type AppInitializerProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
@@ -75,7 +78,7 @@ export const AppInitializer = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
storage.set(STORAGE_KEYS.EDUCATION.VERIFYING, 'yes')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isFinished = await isSetupFinished()
|
const isFinished = await isSetupFinished()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import ContentDialog from '@/app/components/base/content-dialog'
|
import ContentDialog from '@/app/components/base/content-dialog'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||||
@@ -31,6 +31,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
|||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import AppIcon from '../base/app-icon'
|
import AppIcon from '../base/app-icon'
|
||||||
import AppOperations from './app-operations'
|
import AppOperations from './app-operations'
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('newApp.appCreated', { ns: 'app' }),
|
message: t('newApp.appCreated', { ns: 'app' }),
|
||||||
})
|
})
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
onPlanInfoChanged()
|
onPlanInfoChanged()
|
||||||
getRedirection(true, newApp, replace)
|
getRedirection(true, newApp, replace)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import * as React from 'react'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import Divider from '../base/divider'
|
import Divider from '../base/divider'
|
||||||
import { getKeyboardKeyCodeBySystem } from '../workflow/utils'
|
import { getKeyboardKeyCodeBySystem } from '../workflow/utils'
|
||||||
import AppInfo from './app-info'
|
import AppInfo from './app-info'
|
||||||
@@ -53,7 +55,7 @@ const AppDetailNav = ({
|
|||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||||
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
|
const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)
|
||||||
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@ const AppDetailNav = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appSidebarExpand) {
|
if (appSidebarExpand) {
|
||||||
localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand)
|
storage.set(STORAGE_KEYS.APP.DETAIL_COLLAPSE, appSidebarExpand)
|
||||||
setAppSidebarExpand(appSidebarExpand)
|
setAppSidebarExpand(appSidebarExpand)
|
||||||
}
|
}
|
||||||
}, [appSidebarExpand, setAppSidebarExpand])
|
}, [appSidebarExpand, setAppSidebarExpand])
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
|
|||||||
|
|
||||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug'
|
import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug'
|
||||||
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import IdeaOutput from './idea-output'
|
import IdeaOutput from './idea-output'
|
||||||
import InstructionEditorInBasic from './instruction-editor'
|
import InstructionEditorInBasic from './instruction-editor'
|
||||||
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
|
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
|
||||||
@@ -83,9 +85,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
onFinished,
|
onFinished,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const localModel = localStorage.getItem('auto-gen-model')
|
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||||
? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model
|
|
||||||
: null
|
|
||||||
const [model, setModel] = React.useState<Model>(localModel || {
|
const [model, setModel] = React.useState<Model>(localModel || {
|
||||||
name: '',
|
name: '',
|
||||||
provider: '',
|
provider: '',
|
||||||
@@ -178,9 +178,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultModel) {
|
if (defaultModel) {
|
||||||
const localModel = localStorage.getItem('auto-gen-model')
|
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||||
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
|
|
||||||
: null
|
|
||||||
if (localModel) {
|
if (localModel) {
|
||||||
setModel(localModel)
|
setModel(localModel)
|
||||||
}
|
}
|
||||||
@@ -209,7 +207,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
mode: newValue.mode as ModelModeType,
|
mode: newValue.mode as ModelModeType,
|
||||||
}
|
}
|
||||||
setModel(newModel)
|
setModel(newModel)
|
||||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||||
}, [model, setModel])
|
}, [model, setModel])
|
||||||
|
|
||||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||||
@@ -218,7 +216,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
completion_params: newParams as CompletionParams,
|
completion_params: newParams as CompletionParams,
|
||||||
}
|
}
|
||||||
setModel(newModel)
|
setModel(newModel)
|
||||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||||
}, [model, setModel])
|
}, [model, setModel])
|
||||||
|
|
||||||
const onGenerate = async () => {
|
const onGenerate = async () => {
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import Toast from '@/app/components/base/toast'
|
|||||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { generateRule } from '@/service/debug'
|
import { generateRule } from '@/service/debug'
|
||||||
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
|
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
|
||||||
import IdeaOutput from '../automatic/idea-output'
|
import IdeaOutput from '../automatic/idea-output'
|
||||||
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
|
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
|
||||||
@@ -62,9 +64,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
|||||||
presence_penalty: 0,
|
presence_penalty: 0,
|
||||||
frequency_penalty: 0,
|
frequency_penalty: 0,
|
||||||
}
|
}
|
||||||
const localModel = localStorage.getItem('auto-gen-model')
|
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||||
? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model
|
|
||||||
: null
|
|
||||||
const [model, setModel] = React.useState<Model>(localModel || {
|
const [model, setModel] = React.useState<Model>(localModel || {
|
||||||
name: '',
|
name: '',
|
||||||
provider: '',
|
provider: '',
|
||||||
@@ -115,7 +115,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
|||||||
mode: newValue.mode as ModelModeType,
|
mode: newValue.mode as ModelModeType,
|
||||||
}
|
}
|
||||||
setModel(newModel)
|
setModel(newModel)
|
||||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||||
}, [model, setModel])
|
}, [model, setModel])
|
||||||
|
|
||||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||||
@@ -124,7 +124,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
|||||||
completion_params: newParams as CompletionParams,
|
completion_params: newParams as CompletionParams,
|
||||||
}
|
}
|
||||||
setModel(newModel)
|
setModel(newModel)
|
||||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||||
}, [model, setModel])
|
}, [model, setModel])
|
||||||
|
|
||||||
const onGenerate = async () => {
|
const onGenerate = async () => {
|
||||||
@@ -168,9 +168,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultModel) {
|
if (defaultModel) {
|
||||||
const localModel = localStorage.getItem('auto-gen-model')
|
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||||
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
|
|
||||||
: null
|
|
||||||
if (localModel) {
|
if (localModel) {
|
||||||
setModel({
|
setModel({
|
||||||
...localModel,
|
...localModel,
|
||||||
|
|||||||
@@ -14,27 +14,20 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import {
|
import {
|
||||||
AgentStrategy,
|
AgentStrategy,
|
||||||
} from '@/types/app'
|
} from '@/types/app'
|
||||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { ORCHESTRATE_CHANGED } from './types'
|
import { ORCHESTRATE_CHANGED } from './types'
|
||||||
|
|
||||||
export const useDebugWithSingleOrMultipleModel = (appId: string) => {
|
export const useDebugWithSingleOrMultipleModel = (appId: string) => {
|
||||||
const localeDebugWithSingleOrMultipleModelConfigs = localStorage.getItem('app-debug-with-single-or-multiple-models')
|
const localeDebugWithSingleOrMultipleModelConfigs = storage.get<DebugWithSingleOrMultipleModelConfigs>(STORAGE_KEYS.CONFIG.DEBUG_MODELS)
|
||||||
|
|
||||||
const debugWithSingleOrMultipleModelConfigs = useRef<DebugWithSingleOrMultipleModelConfigs>({})
|
const debugWithSingleOrMultipleModelConfigs = useRef<DebugWithSingleOrMultipleModelConfigs>(localeDebugWithSingleOrMultipleModelConfigs || {})
|
||||||
|
|
||||||
if (localeDebugWithSingleOrMultipleModelConfigs) {
|
|
||||||
try {
|
|
||||||
debugWithSingleOrMultipleModelConfigs.current = JSON.parse(localeDebugWithSingleOrMultipleModelConfigs) || {}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
debugWithMultipleModel,
|
debugWithMultipleModel,
|
||||||
@@ -55,7 +48,7 @@ export const useDebugWithSingleOrMultipleModel = (appId: string) => {
|
|||||||
configs: modelConfigs,
|
configs: modelConfigs,
|
||||||
}
|
}
|
||||||
debugWithSingleOrMultipleModelConfigs.current[appId] = value
|
debugWithSingleOrMultipleModelConfigs.current[appId] = value
|
||||||
localStorage.setItem('app-debug-with-single-or-multiple-models', JSON.stringify(debugWithSingleOrMultipleModelConfigs.current))
|
storage.set(STORAGE_KEYS.CONFIG.DEBUG_MODELS, debugWithSingleOrMultipleModelConfigs.current)
|
||||||
setDebugWithMultipleModel(value.multiple)
|
setDebugWithMultipleModel(value.multiple)
|
||||||
setMultipleModelConfigs(value.configs)
|
setMultipleModelConfigs(value.configs)
|
||||||
}, [appId])
|
}, [appId])
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { DSLImportMode } from '@/models/app'
|
import { DSLImportMode } from '@/models/app'
|
||||||
import { importDSL } from '@/service/apps'
|
import { importDSL } from '@/service/apps'
|
||||||
@@ -25,6 +25,7 @@ import { useExploreAppList } from '@/service/use-explore'
|
|||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import AppCard from '../app-card'
|
import AppCard from '../app-card'
|
||||||
import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar'
|
import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar'
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ const Apps = ({
|
|||||||
onSuccess()
|
onSuccess()
|
||||||
if (app.app_id)
|
if (app.app_id)
|
||||||
await handleCheckPluginDependencies(app.app_id)
|
await handleCheckPluginDependencies(app.app_id)
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push)
|
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import { trackEvent } from '@/app/components/base/amplitude'
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { createApp } from '@/service/apps'
|
import { createApp } from '@/service/apps'
|
||||||
@@ -12,6 +11,8 @@ import { AppModeEnum } from '@/types/app'
|
|||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import CreateAppModal from './index'
|
import CreateAppModal from './index'
|
||||||
|
|
||||||
|
const NEED_REFRESH_APP_LIST_KEY_PREFIXED = 'v1:needRefreshAppList'
|
||||||
|
|
||||||
vi.mock('ahooks', () => ({
|
vi.mock('ahooks', () => ({
|
||||||
useDebounceFn: (fn: (...args: any[]) => any) => {
|
useDebounceFn: (fn: (...args: any[]) => any) => {
|
||||||
const run = (...args: any[]) => fn(...args)
|
const run = (...args: any[]) => fn(...args)
|
||||||
@@ -142,7 +143,7 @@ describe('CreateAppModal', () => {
|
|||||||
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' })
|
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' })
|
||||||
expect(onSuccess).toHaveBeenCalled()
|
expect(onSuccess).toHaveBeenCalled()
|
||||||
expect(onClose).toHaveBeenCalled()
|
expect(onClose).toHaveBeenCalled()
|
||||||
await waitFor(() => expect(mockSetItem).toHaveBeenCalledWith(NEED_REFRESH_APP_LIST_KEY, '1'))
|
await waitFor(() => expect(mockSetItem).toHaveBeenCalledWith(NEED_REFRESH_APP_LIST_KEY_PREFIXED, '1'))
|
||||||
await waitFor(() => expect(mockGetRedirection).toHaveBeenCalledWith(true, mockApp, mockPush))
|
await waitFor(() => expect(mockGetRedirection).toHaveBeenCalledWith(true, mockApp, mockPush))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import Input from '@/app/components/base/input'
|
|||||||
import Textarea from '@/app/components/base/textarea'
|
import Textarea from '@/app/components/base/textarea'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
@@ -27,6 +27,7 @@ import { createApp } from '@/service/apps'
|
|||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
import AppIconPicker from '../../base/app-icon-picker'
|
import AppIconPicker from '../../base/app-icon-picker'
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||||||
notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) })
|
notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) })
|
||||||
onSuccess()
|
onSuccess()
|
||||||
onClose()
|
onClose()
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (e: any) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import Modal from '@/app/components/base/modal'
|
|||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '@/service/apps'
|
} from '@/service/apps'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import Uploader from './uploader'
|
import Uploader from './uploader'
|
||||||
|
|
||||||
type CreateFromDSLModalProps = {
|
type CreateFromDSLModalProps = {
|
||||||
@@ -130,7 +131,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }),
|
message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }),
|
||||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
|
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
|
||||||
})
|
})
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
if (app_id)
|
if (app_id)
|
||||||
await handleCheckPluginDependencies(app_id)
|
await handleCheckPluginDependencies(app_id)
|
||||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||||
@@ -190,7 +191,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
})
|
})
|
||||||
if (app_id)
|
if (app_id)
|
||||||
await handleCheckPluginDependencies(app_id)
|
await handleCheckPluginDependencies(app_id)
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||||
}
|
}
|
||||||
else if (status === DSLImportStatus.FAILED) {
|
else if (status === DSLImportStatus.FAILED) {
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import * as React from 'react'
|
|||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import SwitchAppModal from './index'
|
import SwitchAppModal from './index'
|
||||||
|
|
||||||
|
const NEED_REFRESH_APP_LIST_KEY_PREFIXED = 'v1:needRefreshAppList'
|
||||||
|
|
||||||
const mockPush = vi.fn()
|
const mockPush = vi.fn()
|
||||||
const mockReplace = vi.fn()
|
const mockReplace = vi.fn()
|
||||||
vi.mock('next/navigation', () => ({
|
vi.mock('next/navigation', () => ({
|
||||||
@@ -257,7 +258,7 @@ describe('SwitchAppModal', () => {
|
|||||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||||
expect(onClose).toHaveBeenCalledTimes(1)
|
expect(onClose).toHaveBeenCalledTimes(1)
|
||||||
expect(notify).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' })
|
expect(notify).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' })
|
||||||
expect(localStorage.setItem).toHaveBeenCalledWith(NEED_REFRESH_APP_LIST_KEY, '1')
|
expect(localStorage.setItem).toHaveBeenCalledWith(NEED_REFRESH_APP_LIST_KEY_PREFIXED, '1')
|
||||||
expect(mockPush).toHaveBeenCalledWith('/app/new-app-001/workflow')
|
expect(mockPush).toHaveBeenCalledWith('/app/new-app-001/workflow')
|
||||||
expect(mockReplace).not.toHaveBeenCalled()
|
expect(mockReplace).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ import Input from '@/app/components/base/input'
|
|||||||
import Modal from '@/app/components/base/modal'
|
import Modal from '@/app/components/base/modal'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { deleteApp, switchApp } from '@/service/apps'
|
import { deleteApp, switchApp } from '@/service/apps'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import AppIconPicker from '../../base/app-icon-picker'
|
import AppIconPicker from '../../base/app-icon-picker'
|
||||||
|
|
||||||
type SwitchAppModalProps = {
|
type SwitchAppModalProps = {
|
||||||
@@ -73,7 +74,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
|||||||
setAppDetail()
|
setAppDetail()
|
||||||
if (removeOriginal)
|
if (removeOriginal)
|
||||||
await deleteApp(appDetail.id)
|
await deleteApp(appDetail.id)
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
getRedirection(
|
getRedirection(
|
||||||
isCurrentWorkspaceEditor,
|
isCurrentWorkspaceEditor,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import CustomPopover from '@/app/components/base/popover'
|
|||||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||||
import Toast, { ToastContext } from '@/app/components/base/toast'
|
import Toast, { ToastContext } from '@/app/components/base/toast'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
@@ -33,6 +33,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
|||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { formatTime } from '@/utils/time'
|
import { formatTime } from '@/utils/time'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('newApp.appCreated', { ns: 'app' }),
|
message: t('newApp.appCreated', { ns: 'app' }),
|
||||||
})
|
})
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
if (onRefresh)
|
if (onRefresh)
|
||||||
onRefresh()
|
onRefresh()
|
||||||
onPlanInfoChanged()
|
onPlanInfoChanged()
|
||||||
|
|||||||
@@ -434,13 +434,15 @@ describe('List', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Local Storage Refresh', () => {
|
describe('Local Storage Refresh', () => {
|
||||||
it('should call refetch when refresh key is set in localStorage', () => {
|
it('should call refetch when refresh key is set in localStorage', async () => {
|
||||||
localStorage.setItem('needRefreshAppList', '1')
|
localStorage.setItem('v1:needRefreshAppList', '1')
|
||||||
|
|
||||||
render(<List />)
|
render(<List />)
|
||||||
|
|
||||||
expect(mockRefetch).toHaveBeenCalled()
|
await vi.waitFor(() => {
|
||||||
expect(localStorage.getItem('needRefreshAppList')).toBeNull()
|
expect(mockRefetch).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
expect(localStorage.getItem('v1:needRefreshAppList')).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ import TabSliderNew from '@/app/components/base/tab-slider-new'
|
|||||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { CheckModal } from '@/hooks/use-pay'
|
import { CheckModal } from '@/hooks/use-pay'
|
||||||
import { useInfiniteAppList } from '@/service/use-apps'
|
import { useInfiniteAppList } from '@/service/use-apps'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import AppCard from './app-card'
|
import AppCard from './app-card'
|
||||||
import { AppCardSkeleton } from './app-card-skeleton'
|
import { AppCardSkeleton } from './app-card-skeleton'
|
||||||
import Empty from './empty'
|
import Empty from './empty'
|
||||||
@@ -134,8 +135,8 @@ const List: FC<Props> = ({
|
|||||||
]
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
if (storage.get<string>(STORAGE_KEYS.APP.NEED_REFRESH_LIST) === '1') {
|
||||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
storage.remove(STORAGE_KEYS.APP.NEED_REFRESH_LIST)
|
||||||
refetch()
|
refetch()
|
||||||
}
|
}
|
||||||
}, [refetch])
|
}, [refetch])
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import {
|
|||||||
generationConversationName,
|
generationConversationName,
|
||||||
} from '@/service/share'
|
} from '@/service/share'
|
||||||
import { shareQueryKeys } from '@/service/use-share'
|
import { shareQueryKeys } from '@/service/use-share'
|
||||||
import { CONVERSATION_ID_INFO } from '../constants'
|
|
||||||
import { useChatWithHistory } from './hooks'
|
import { useChatWithHistory } from './hooks'
|
||||||
|
|
||||||
|
const CONVERSATION_ID_INFO_KEY = 'v1:conversationIdInfo'
|
||||||
|
|
||||||
vi.mock('@/hooks/use-app-favicon', () => ({
|
vi.mock('@/hooks/use-app-favicon', () => ({
|
||||||
useAppFavicon: vi.fn(),
|
useAppFavicon: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -120,14 +121,14 @@ const setConversationIdInfo = (appId: string, conversationId: string) => {
|
|||||||
'DEFAULT': conversationId,
|
'DEFAULT': conversationId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
localStorage.setItem(CONVERSATION_ID_INFO, JSON.stringify(value))
|
localStorage.setItem(CONVERSATION_ID_INFO_KEY, JSON.stringify(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scenario: useChatWithHistory integrates share queries for conversations and chat list.
|
// Scenario: useChatWithHistory integrates share queries for conversations and chat list.
|
||||||
describe('useChatWithHistory', () => {
|
describe('useChatWithHistory', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||||
mockStoreState.appInfo = {
|
mockStoreState.appInfo = {
|
||||||
app_id: 'app-1',
|
app_id: 'app-1',
|
||||||
custom_config: null,
|
custom_config: null,
|
||||||
@@ -144,7 +145,7 @@ describe('useChatWithHistory', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Scenario: share query results populate conversation lists and trigger chat list fetch.
|
// Scenario: share query results populate conversation lists and trigger chat list fetch.
|
||||||
@@ -268,7 +269,7 @@ describe('useChatWithHistory', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
|
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO_KEY)
|
||||||
const parsed = storedValue ? JSON.parse(storedValue) : {}
|
const parsed = storedValue ? JSON.parse(storedValue) : {}
|
||||||
const storedUserId = parsed['app-1']?.['user-1']
|
const storedUserId = parsed['app-1']?.['user-1']
|
||||||
const storedDefaultId = parsed['app-1']?.DEFAULT
|
const storedDefaultId = parsed['app-1']?.DEFAULT
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import { InputVarType } from '@/app/components/workflow/types'
|
import { InputVarType } from '@/app/components/workflow/types'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useWebAppStore } from '@/context/web-app-context'
|
import { useWebAppStore } from '@/context/web-app-context'
|
||||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||||
import { changeLanguage } from '@/i18n-config/client'
|
import { changeLanguage } from '@/i18n-config/client'
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
useShareConversations,
|
useShareConversations,
|
||||||
} from '@/service/use-share'
|
} from '@/service/use-share'
|
||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
||||||
import { CONVERSATION_ID_INFO } from '../constants'
|
import { CONVERSATION_ID_INFO } from '../constants'
|
||||||
import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInputsFromUrlParams, getRawUserVariablesFromUrlParams } from '../utils'
|
import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInputsFromUrlParams, getRawUserVariablesFromUrlParams } from '../utils'
|
||||||
@@ -128,27 +130,15 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||||||
|
|
||||||
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(() => {
|
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
try {
|
const localState = storage.get<string>(STORAGE_KEYS.APP.SIDEBAR_COLLAPSE)
|
||||||
const localState = localStorage.getItem('webappSidebarCollapse')
|
return localState === 'collapsed'
|
||||||
return localState === 'collapsed'
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// localStorage may be disabled in private browsing mode or by security settings
|
|
||||||
// fallback to default value
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const handleSidebarCollapse = useCallback((state: boolean) => {
|
const handleSidebarCollapse = useCallback((state: boolean) => {
|
||||||
if (appId) {
|
if (appId) {
|
||||||
setSidebarCollapseState(state)
|
setSidebarCollapseState(state)
|
||||||
try {
|
storage.set(STORAGE_KEYS.APP.SIDEBAR_COLLAPSE, state ? 'collapsed' : 'expanded')
|
||||||
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// localStorage may be disabled, continue without persisting state
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [appId, setSidebarCollapseState])
|
}, [appId, setSidebarCollapseState])
|
||||||
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
|
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
|
export const CONVERSATION_ID_INFO = 'v1:conversationIdInfo'
|
||||||
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'
|
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import {
|
|||||||
generationConversationName,
|
generationConversationName,
|
||||||
} from '@/service/share'
|
} from '@/service/share'
|
||||||
import { shareQueryKeys } from '@/service/use-share'
|
import { shareQueryKeys } from '@/service/use-share'
|
||||||
import { CONVERSATION_ID_INFO } from '../constants'
|
|
||||||
import { useEmbeddedChatbot } from './hooks'
|
import { useEmbeddedChatbot } from './hooks'
|
||||||
|
|
||||||
|
const CONVERSATION_ID_INFO_KEY = 'v1:conversationIdInfo'
|
||||||
|
|
||||||
vi.mock('@/i18n-config/client', () => ({
|
vi.mock('@/i18n-config/client', () => ({
|
||||||
changeLanguage: vi.fn().mockResolvedValue(undefined),
|
changeLanguage: vi.fn().mockResolvedValue(undefined),
|
||||||
}))
|
}))
|
||||||
@@ -113,7 +114,7 @@ const createConversationData = (overrides: Partial<AppConversationData> = {}): A
|
|||||||
describe('useEmbeddedChatbot', () => {
|
describe('useEmbeddedChatbot', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||||
mockStoreState.appInfo = {
|
mockStoreState.appInfo = {
|
||||||
app_id: 'app-1',
|
app_id: 'app-1',
|
||||||
custom_config: null,
|
custom_config: null,
|
||||||
@@ -131,7 +132,7 @@ describe('useEmbeddedChatbot', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Scenario: share query results populate conversation lists and trigger chat list fetch.
|
// Scenario: share query results populate conversation lists and trigger chat list fetch.
|
||||||
@@ -251,7 +252,7 @@ describe('useEmbeddedChatbot', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
|
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO_KEY)
|
||||||
const parsed = storedValue ? JSON.parse(storedValue) : {}
|
const parsed = storedValue ? JSON.parse(storedValue) : {}
|
||||||
const storedUserId = parsed['app-1']?.['embedded-user-1']
|
const storedUserId = parsed['app-1']?.['embedded-user-1']
|
||||||
const storedDefaultId = parsed['app-1']?.DEFAULT
|
const storedDefaultId = parsed['app-1']?.DEFAULT
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ describe('PlanComp', () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
||||||
await waitFor(() => expect(push).toHaveBeenCalledWith('/education-apply?token=token'))
|
await waitFor(() => expect(push).toHaveBeenCalledWith('/education-apply?token=token'))
|
||||||
expect(localStorage.removeItem).toHaveBeenCalledWith(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
expect(localStorage.removeItem).toHaveBeenCalledWith(`v1:${EDUCATION_VERIFYING_LOCALSTORAGE_ITEM}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows modal when education verify fails', async () => {
|
it('shows modal when education verify fails', async () => {
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||||
import UsageInfo from '@/app/components/billing/usage-info'
|
import UsageInfo from '@/app/components/billing/usage-info'
|
||||||
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants'
|
|
||||||
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
|
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useEducationVerify } from '@/service/use-education'
|
import { useEducationVerify } from '@/service/use-education'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||||
import { Loading } from '../../base/icons/src/public/thought'
|
import { Loading } from '../../base/icons/src/public/thought'
|
||||||
import { NUM_INFINITE } from '../config'
|
import { NUM_INFINITE } from '../config'
|
||||||
@@ -72,7 +73,7 @@ const PlanComp: FC<Props> = ({
|
|||||||
if (isPending)
|
if (isPending)
|
||||||
return
|
return
|
||||||
mutateAsync().then((res) => {
|
mutateAsync().then((res) => {
|
||||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
storage.remove(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||||
if (unmountedRef.current)
|
if (unmountedRef.current)
|
||||||
return
|
return
|
||||||
router.push(`/education-apply?token=${res.token}`)
|
router.push(`/education-apply?token=${res.token}`)
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { useBoolean } from 'ahooks'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useBuiltInMetaDataFields, useCreateMetaData, useDatasetMetaData, useDeleteMetaData, useRenameMeta, useUpdateBuiltInStatus } from '@/service/knowledge/use-metadata'
|
import { useBuiltInMetaDataFields, useCreateMetaData, useDatasetMetaData, useDeleteMetaData, useRenameMeta, useUpdateBuiltInStatus } from '@/service/knowledge/use-metadata'
|
||||||
import { isShowManageMetadataLocalStorageKey } from '../types'
|
import { storage } from '@/utils/storage'
|
||||||
import useCheckMetadataName from './use-check-metadata-name'
|
import useCheckMetadataName from './use-check-metadata-name'
|
||||||
|
|
||||||
const useEditDatasetMetadata = ({
|
const useEditDatasetMetadata = ({
|
||||||
@@ -24,10 +25,10 @@ const useEditDatasetMetadata = ({
|
|||||||
}] = useBoolean(false)
|
}] = useBoolean(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isShowManageMetadata = localStorage.getItem(isShowManageMetadataLocalStorageKey)
|
const isShowManageMetadata = storage.get<string>(STORAGE_KEYS.UI.SHOW_MANAGE_METADATA)
|
||||||
if (isShowManageMetadata) {
|
if (isShowManageMetadata) {
|
||||||
showEditModal()
|
showEditModal()
|
||||||
localStorage.removeItem(isShowManageMetadataLocalStorageKey)
|
storage.remove(STORAGE_KEYS.UI.SHOW_MANAGE_METADATA)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import * as React from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import AddMetadataButton from '../add-metadata-button'
|
import AddMetadataButton from '../add-metadata-button'
|
||||||
import InputCombined from '../edit-metadata-batch/input-combined'
|
import InputCombined from '../edit-metadata-batch/input-combined'
|
||||||
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
||||||
import { DataType, isShowManageMetadataLocalStorageKey } from '../types'
|
import { DataType } from '../types'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -53,7 +55,7 @@ const InfoGroup: FC<Props> = ({
|
|||||||
const { formatTime: formatTimestamp } = useTimestamp()
|
const { formatTime: formatTimestamp } = useTimestamp()
|
||||||
|
|
||||||
const handleMangeMetadata = () => {
|
const handleMangeMetadata = () => {
|
||||||
localStorage.setItem(isShowManageMetadataLocalStorageKey, 'true')
|
storage.set(STORAGE_KEYS.UI.SHOW_MANAGE_METADATA, 'true')
|
||||||
router.push(`/datasets/${dataSetId}/documents`)
|
router.push(`/datasets/${dataSetId}/documents`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import PremiumBadge from '@/app/components/base/premium-badge'
|
|||||||
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
@@ -30,6 +31,7 @@ import { useModalContext } from '@/context/modal-context'
|
|||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import AccountAbout from '../account-about'
|
import AccountAbout from '../account-about'
|
||||||
import GithubStar from '../github-star'
|
import GithubStar from '../github-star'
|
||||||
import Indicator from '../indicator'
|
import Indicator from '../indicator'
|
||||||
@@ -55,13 +57,13 @@ export default function AppSelector() {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
resetUser()
|
resetUser()
|
||||||
localStorage.removeItem('setup_status')
|
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||||
// Tokens are now stored in cookies and cleared by backend
|
// Tokens are now stored in cookies and cleared by backend
|
||||||
|
|
||||||
// To avoid use other account's education notice info
|
// To avoid use other account's education notice info
|
||||||
localStorage.removeItem('education-reverify-prev-expire-at')
|
storage.remove(STORAGE_KEYS.EDUCATION.REVERIFY_PREV_EXPIRE_AT)
|
||||||
localStorage.removeItem('education-reverify-has-noticed')
|
storage.remove(STORAGE_KEYS.EDUCATION.REVERIFY_HAS_NOTICED)
|
||||||
localStorage.removeItem('education-expired-has-noticed')
|
storage.remove(STORAGE_KEYS.EDUCATION.EXPIRED_HAS_NOTICED)
|
||||||
|
|
||||||
router.push('/signin')
|
router.push('/signin')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import s from './index.module.css'
|
import s from './index.module.css'
|
||||||
|
|
||||||
type HeaderWrapperProps = {
|
type HeaderWrapperProps = {
|
||||||
@@ -18,7 +20,7 @@ const HeaderWrapper = ({
|
|||||||
// Check if the current path is a workflow canvas & fullscreen
|
// Check if the current path is a workflow canvas & fullscreen
|
||||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||||
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
|
const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)
|
||||||
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { X } from '@/app/components/base/icons/src/vender/line/general'
|
import { X } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { NOTICE_I18N } from '@/i18n-config/language'
|
import { NOTICE_I18N } from '@/i18n-config/language'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
const MaintenanceNotice = () => {
|
const MaintenanceNotice = () => {
|
||||||
const locale = useLanguage()
|
const locale = useLanguage()
|
||||||
|
|
||||||
const [showNotice, setShowNotice] = useState(() => localStorage.getItem('hide-maintenance-notice') !== '1')
|
const [showNotice, setShowNotice] = useState(() => storage.get<string>(STORAGE_KEYS.UI.HIDE_MAINTENANCE_NOTICE) !== '1')
|
||||||
const handleJumpNotice = () => {
|
const handleJumpNotice = () => {
|
||||||
window.open(NOTICE_I18N.href, '_blank')
|
window.open(NOTICE_I18N.href, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseNotice = () => {
|
const handleCloseNotice = () => {
|
||||||
localStorage.setItem('hide-maintenance-notice', '1')
|
storage.set(STORAGE_KEYS.UI.HIDE_MAINTENANCE_NOTICE, '1')
|
||||||
setShowNotice(false)
|
setShowNotice(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { useCountDown } from 'ahooks'
|
import { useCountDown } from 'ahooks'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export const COUNT_DOWN_TIME_MS = 59000
|
export const COUNT_DOWN_TIME_MS = 59000
|
||||||
export const COUNT_DOWN_KEY = 'leftTime'
|
export const COUNT_DOWN_KEY = 'leftTime'
|
||||||
@@ -12,23 +14,23 @@ type CountdownProps = {
|
|||||||
|
|
||||||
export default function Countdown({ onResend }: CountdownProps) {
|
export default function Countdown({ onResend }: CountdownProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [leftTime, setLeftTime] = useState(() => Number(localStorage.getItem(COUNT_DOWN_KEY) || COUNT_DOWN_TIME_MS))
|
const [leftTime, setLeftTime] = useState(() => storage.getNumber(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, COUNT_DOWN_TIME_MS))
|
||||||
const [time] = useCountDown({
|
const [time] = useCountDown({
|
||||||
leftTime,
|
leftTime,
|
||||||
onEnd: () => {
|
onEnd: () => {
|
||||||
setLeftTime(0)
|
setLeftTime(0)
|
||||||
localStorage.removeItem(COUNT_DOWN_KEY)
|
storage.remove(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const resend = async function () {
|
const resend = async function () {
|
||||||
setLeftTime(COUNT_DOWN_TIME_MS)
|
setLeftTime(COUNT_DOWN_TIME_MS)
|
||||||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, COUNT_DOWN_TIME_MS)
|
||||||
onResend?.()
|
onResend?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(COUNT_DOWN_KEY, `${time}`)
|
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, time)
|
||||||
}, [time])
|
}, [time])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
import { isServer } from '@/utils/client'
|
import { isServer } from '@/utils/client'
|
||||||
import { formatNumber } from '@/utils/format'
|
import { formatNumber } from '@/utils/format'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
import BlockIcon from '../block-icon'
|
import BlockIcon from '../block-icon'
|
||||||
import { BlockEnum } from '../types'
|
import { BlockEnum } from '../types'
|
||||||
@@ -34,8 +36,6 @@ type FeaturedToolsProps = {
|
|||||||
onInstallSuccess?: () => void
|
onInstallSuccess?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'workflow_tools_featured_collapsed'
|
|
||||||
|
|
||||||
const FeaturedTools = ({
|
const FeaturedTools = ({
|
||||||
plugins,
|
plugins,
|
||||||
providerMap,
|
providerMap,
|
||||||
@@ -50,14 +50,14 @@ const FeaturedTools = ({
|
|||||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return false
|
return false
|
||||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TOOLS_FEATURED_COLLAPSED)
|
||||||
return stored === 'true'
|
return stored === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return
|
return
|
||||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TOOLS_FEATURED_COLLAPSED)
|
||||||
if (stored !== null)
|
if (stored !== null)
|
||||||
setIsCollapsed(stored === 'true')
|
setIsCollapsed(stored === 'true')
|
||||||
}, [])
|
}, [])
|
||||||
@@ -65,7 +65,7 @@ const FeaturedTools = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return
|
return
|
||||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
storage.set(STORAGE_KEYS.WORKFLOW.TOOLS_FEATURED_COLLAPSED, String(isCollapsed))
|
||||||
}, [isCollapsed])
|
}, [isCollapsed])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
import { isServer } from '@/utils/client'
|
import { isServer } from '@/utils/client'
|
||||||
import { formatNumber } from '@/utils/format'
|
import { formatNumber } from '@/utils/format'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
import BlockIcon from '../block-icon'
|
import BlockIcon from '../block-icon'
|
||||||
import { BlockEnum } from '../types'
|
import { BlockEnum } from '../types'
|
||||||
@@ -30,8 +32,6 @@ type FeaturedTriggersProps = {
|
|||||||
onInstallSuccess?: () => void | Promise<void>
|
onInstallSuccess?: () => void | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'workflow_triggers_featured_collapsed'
|
|
||||||
|
|
||||||
const FeaturedTriggers = ({
|
const FeaturedTriggers = ({
|
||||||
plugins,
|
plugins,
|
||||||
providerMap,
|
providerMap,
|
||||||
@@ -45,14 +45,14 @@ const FeaturedTriggers = ({
|
|||||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return false
|
return false
|
||||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TRIGGERS_FEATURED_COLLAPSED)
|
||||||
return stored === 'true'
|
return stored === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return
|
return
|
||||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TRIGGERS_FEATURED_COLLAPSED)
|
||||||
if (stored !== null)
|
if (stored !== null)
|
||||||
setIsCollapsed(stored === 'true')
|
setIsCollapsed(stored === 'true')
|
||||||
}, [])
|
}, [])
|
||||||
@@ -60,7 +60,7 @@ const FeaturedTriggers = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return
|
return
|
||||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
storage.set(STORAGE_KEYS.WORKFLOW.TRIGGERS_FEATURED_COLLAPSED, String(isCollapsed))
|
||||||
}, [isCollapsed])
|
}, [isCollapsed])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/arrows'
|
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useRAGRecommendedPlugins } from '@/service/use-tools'
|
import { useRAGRecommendedPlugins } from '@/service/use-tools'
|
||||||
import { isServer } from '@/utils/client'
|
import { isServer } from '@/utils/client'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
import List from './list'
|
import List from './list'
|
||||||
|
|
||||||
@@ -21,8 +23,6 @@ type RAGToolRecommendationsProps = {
|
|||||||
onTagsChange: Dispatch<SetStateAction<string[]>>
|
onTagsChange: Dispatch<SetStateAction<string[]>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'workflow_rag_recommendations_collapsed'
|
|
||||||
|
|
||||||
const RAGToolRecommendations = ({
|
const RAGToolRecommendations = ({
|
||||||
viewType,
|
viewType,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -32,14 +32,14 @@ const RAGToolRecommendations = ({
|
|||||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return false
|
return false
|
||||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.RAG_RECOMMENDATIONS_COLLAPSED)
|
||||||
return stored === 'true'
|
return stored === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return
|
return
|
||||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.RAG_RECOMMENDATIONS_COLLAPSED)
|
||||||
if (stored !== null)
|
if (stored !== null)
|
||||||
setIsCollapsed(stored === 'true')
|
setIsCollapsed(stored === 'true')
|
||||||
}, [])
|
}, [])
|
||||||
@@ -47,7 +47,7 @@ const RAGToolRecommendations = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isServer)
|
if (isServer)
|
||||||
return
|
return
|
||||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
storage.set(STORAGE_KEYS.WORKFLOW.RAG_RECOMMENDATIONS_COLLAPSED, String(isCollapsed))
|
||||||
}, [isCollapsed])
|
}, [isCollapsed])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import {
|
import {
|
||||||
CUSTOM_NODE,
|
CUSTOM_NODE,
|
||||||
NODE_LAYOUT_HORIZONTAL_PADDING,
|
NODE_LAYOUT_HORIZONTAL_PADDING,
|
||||||
@@ -342,7 +344,7 @@ export const useWorkflowCanvasMaximize = () => {
|
|||||||
return
|
return
|
||||||
|
|
||||||
setMaximizeCanvas(!maximizeCanvas)
|
setMaximizeCanvas(!maximizeCanvas)
|
||||||
localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas))
|
storage.set(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, !maximizeCanvas)
|
||||||
eventEmitter?.emit({
|
eventEmitter?.emit({
|
||||||
type: 'workflow-canvas-maximize',
|
type: 'workflow-canvas-maximize',
|
||||||
payload: !maximizeCanvas,
|
payload: !maximizeCanvas,
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ const Wrap = ({
|
|||||||
isExpand,
|
isExpand,
|
||||||
children,
|
children,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const panelWidth = useStore(state => state.panelWidth)
|
const nodePanelWidth = useStore(state => state.nodePanelWidth)
|
||||||
const wrapStyle = (() => {
|
const wrapStyle = (() => {
|
||||||
if (isExpand) {
|
if (isExpand) {
|
||||||
return {
|
return {
|
||||||
...style,
|
...style,
|
||||||
width: panelWidth - 1,
|
width: nodePanelWidth - 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return style
|
return style
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const createPanelWidthManager = (storageKey: string) => {
|
|||||||
|
|
||||||
describe('Workflow Panel Width Persistence', () => {
|
describe('Workflow Panel Width Persistence', () => {
|
||||||
describe('Node Panel Width Management', () => {
|
describe('Node Panel Width Management', () => {
|
||||||
const storageKey = 'workflow-node-panel-width'
|
const storageKey = 'v1:workflow-node-panel-width'
|
||||||
|
|
||||||
it('should save user resize to localStorage', () => {
|
it('should save user resize to localStorage', () => {
|
||||||
const manager = createPanelWidthManager(storageKey)
|
const manager = createPanelWidthManager(storageKey)
|
||||||
@@ -74,7 +74,7 @@ describe('Workflow Panel Width Persistence', () => {
|
|||||||
|
|
||||||
describe('Bug Scenario Reproduction', () => {
|
describe('Bug Scenario Reproduction', () => {
|
||||||
it('should reproduce original bug behavior (for comparison)', () => {
|
it('should reproduce original bug behavior (for comparison)', () => {
|
||||||
const storageKey = 'workflow-node-panel-width'
|
const storageKey = 'v1:workflow-node-panel-width'
|
||||||
|
|
||||||
// Original buggy behavior - always saves regardless of source
|
// Original buggy behavior - always saves regardless of source
|
||||||
const buggyUpdate = (width: number) => {
|
const buggyUpdate = (width: number) => {
|
||||||
@@ -89,7 +89,7 @@ describe('Workflow Panel Width Persistence', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should verify fix prevents localStorage pollution', () => {
|
it('should verify fix prevents localStorage pollution', () => {
|
||||||
const storageKey = 'workflow-node-panel-width'
|
const storageKey = 'v1:workflow-node-panel-width'
|
||||||
const manager = createPanelWidthManager(storageKey)
|
const manager = createPanelWidthManager(storageKey)
|
||||||
|
|
||||||
localStorage.setItem(storageKey, '500') // User preference
|
localStorage.setItem(storageKey, '500') // User preference
|
||||||
@@ -101,7 +101,7 @@ describe('Workflow Panel Width Persistence', () => {
|
|||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
it('should handle multiple rapid operations correctly', () => {
|
it('should handle multiple rapid operations correctly', () => {
|
||||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
const manager = createPanelWidthManager('v1:workflow-node-panel-width')
|
||||||
|
|
||||||
// Rapid system adjustments
|
// Rapid system adjustments
|
||||||
manager.updateWidth(300, 'system')
|
manager.updateWidth(300, 'system')
|
||||||
@@ -112,12 +112,12 @@ describe('Workflow Panel Width Persistence', () => {
|
|||||||
manager.updateWidth(550, 'user')
|
manager.updateWidth(550, 'user')
|
||||||
|
|
||||||
expect(localStorage.setItem).toHaveBeenCalledTimes(1)
|
expect(localStorage.setItem).toHaveBeenCalledTimes(1)
|
||||||
expect(localStorage.setItem).toHaveBeenCalledWith('workflow-node-panel-width', '550')
|
expect(localStorage.setItem).toHaveBeenCalledWith('v1:workflow-node-panel-width', '550')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle corrupted localStorage gracefully', () => {
|
it('should handle corrupted localStorage gracefully', () => {
|
||||||
localStorage.setItem('workflow-node-panel-width', '150') // Below minimum
|
localStorage.setItem('v1:workflow-node-panel-width', '150') // Below minimum
|
||||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
const manager = createPanelWidthManager('v1:workflow-node-panel-width')
|
||||||
|
|
||||||
const storedWidth = manager.getStoredWidth()
|
const storedWidth = manager.getStoredWidth()
|
||||||
expect(storedWidth).toBe(150) // Returns raw value
|
expect(storedWidth).toBe(150) // Returns raw value
|
||||||
@@ -125,13 +125,13 @@ describe('Workflow Panel Width Persistence', () => {
|
|||||||
// User can correct the preference
|
// User can correct the preference
|
||||||
const correctedWidth = manager.updateWidth(500, 'user')
|
const correctedWidth = manager.updateWidth(500, 'user')
|
||||||
expect(correctedWidth).toBe(500)
|
expect(correctedWidth).toBe(500)
|
||||||
expect(localStorage.getItem('workflow-node-panel-width')).toBe('500')
|
expect(localStorage.getItem('v1:workflow-node-panel-width')).toBe('500')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('TypeScript Type Safety', () => {
|
describe('TypeScript Type Safety', () => {
|
||||||
it('should enforce source parameter type', () => {
|
it('should enforce source parameter type', () => {
|
||||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
const manager = createPanelWidthManager('v1:workflow-node-panel-width')
|
||||||
|
|
||||||
// Valid source values
|
// Valid source values
|
||||||
manager.updateWidth(500, 'user')
|
manager.updateWidth(500, 'user')
|
||||||
|
|||||||
@@ -59,12 +59,14 @@ import {
|
|||||||
hasRetryNode,
|
hasRetryNode,
|
||||||
isSupportCustomRunForm,
|
isSupportCustomRunForm,
|
||||||
} from '@/app/components/workflow/utils'
|
} from '@/app/components/workflow/utils'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||||
import { FlowType } from '@/types/common'
|
import { FlowType } from '@/types/common'
|
||||||
import { canFindTool } from '@/utils'
|
import { canFindTool } from '@/utils'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { useResizePanel } from '../../hooks/use-resize-panel'
|
import { useResizePanel } from '../../hooks/use-resize-panel'
|
||||||
import BeforeRunForm from '../before-run-form'
|
import BeforeRunForm from '../before-run-form'
|
||||||
import PanelWrap from '../before-run-form/panel-wrap'
|
import PanelWrap from '../before-run-form/panel-wrap'
|
||||||
@@ -137,7 +139,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
const newValue = Math.max(400, Math.min(width, maxNodePanelWidth))
|
const newValue = Math.max(400, Math.min(width, maxNodePanelWidth))
|
||||||
|
|
||||||
if (source === 'user')
|
if (source === 'user')
|
||||||
localStorage.setItem('workflow-node-panel-width', `${newValue}`)
|
storage.set(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, newValue)
|
||||||
|
|
||||||
setNodePanelWidth(newValue)
|
setNodePanelWidth(newValue)
|
||||||
}, [maxNodePanelWidth, setNodePanelWidth])
|
}, [maxNodePanelWidth, setNodePanelWidth])
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import {
|
|||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
import { useGenerateStructuredOutputRules } from '@/service/use-common'
|
import { useGenerateStructuredOutputRules } from '@/service/use-common'
|
||||||
import { ModelModeType, Theme } from '@/types/app'
|
import { ModelModeType, Theme } from '@/types/app'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { useMittContext } from '../visual-editor/context'
|
import { useMittContext } from '../visual-editor/context'
|
||||||
import { useVisualEditorStore } from '../visual-editor/store'
|
import { useVisualEditorStore } from '../visual-editor/store'
|
||||||
import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets'
|
import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets'
|
||||||
@@ -36,9 +38,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
|||||||
onApply,
|
onApply,
|
||||||
crossAxisOffset,
|
crossAxisOffset,
|
||||||
}) => {
|
}) => {
|
||||||
const localModel = localStorage.getItem('auto-gen-model')
|
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||||
? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model
|
|
||||||
: null
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [view, setView] = useState(GeneratorView.promptEditor)
|
const [view, setView] = useState(GeneratorView.promptEditor)
|
||||||
const [model, setModel] = useState<Model>(localModel || {
|
const [model, setModel] = useState<Model>(localModel || {
|
||||||
@@ -60,9 +60,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultModel) {
|
if (defaultModel) {
|
||||||
const localModel = localStorage.getItem('auto-gen-model')
|
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||||
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
|
|
||||||
: null
|
|
||||||
if (localModel) {
|
if (localModel) {
|
||||||
setModel(localModel)
|
setModel(localModel)
|
||||||
}
|
}
|
||||||
@@ -95,7 +93,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
|||||||
mode: newValue.mode as ModelModeType,
|
mode: newValue.mode as ModelModeType,
|
||||||
}
|
}
|
||||||
setModel(newModel)
|
setModel(newModel)
|
||||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||||
}, [model, setModel])
|
}, [model, setModel])
|
||||||
|
|
||||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||||
@@ -104,7 +102,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
|||||||
completion_params: newParams as CompletionParams,
|
completion_params: newParams as CompletionParams,
|
||||||
}
|
}
|
||||||
setModel(newModel)
|
setModel(newModel)
|
||||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||||
}, [model, setModel])
|
}, [model, setModel])
|
||||||
|
|
||||||
const { mutateAsync: generateStructuredOutputRules, isPending: isGenerating } = useGenerateStructuredOutputRules()
|
const { mutateAsync: generateStructuredOutputRules, isPending: isGenerating } = useGenerateStructuredOutputRules()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const createMockLocalStorage = () => {
|
|||||||
|
|
||||||
// Preview panel width logic
|
// Preview panel width logic
|
||||||
const createPreviewPanelManager = () => {
|
const createPreviewPanelManager = () => {
|
||||||
const storageKey = 'debug-and-preview-panel-width'
|
const storageKey = 'v1:debug-and-preview-panel-width'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateWidth: (width: number, source: PanelWidthSource = 'user') => {
|
updateWidth: (width: number, source: PanelWidthSource = 'user') => {
|
||||||
@@ -63,7 +63,7 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
|||||||
const result = manager.updateWidth(450, 'user')
|
const result = manager.updateWidth(450, 'user')
|
||||||
|
|
||||||
expect(result).toBe(450)
|
expect(result).toBe(450)
|
||||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '450')
|
expect(localStorage.setItem).toHaveBeenCalledWith('v1:debug-and-preview-panel-width', '450')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not save system compression to localStorage', () => {
|
it('should not save system compression to localStorage', () => {
|
||||||
@@ -80,17 +80,17 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
|||||||
|
|
||||||
// Both user and system operations should behave consistently
|
// Both user and system operations should behave consistently
|
||||||
manager.updateWidth(500, 'user')
|
manager.updateWidth(500, 'user')
|
||||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500')
|
expect(localStorage.setItem).toHaveBeenCalledWith('v1:debug-and-preview-panel-width', '500')
|
||||||
|
|
||||||
manager.updateWidth(200, 'system')
|
manager.updateWidth(200, 'system')
|
||||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
expect(localStorage.getItem('v1:debug-and-preview-panel-width')).toBe('500')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Dual Panel Scenario', () => {
|
describe('Dual Panel Scenario', () => {
|
||||||
it('should maintain independence from Node Panel', () => {
|
it('should maintain independence from Node Panel', () => {
|
||||||
localStorage.setItem('workflow-node-panel-width', '600')
|
localStorage.setItem('v1:workflow-node-panel-width', '600')
|
||||||
localStorage.setItem('debug-and-preview-panel-width', '450')
|
localStorage.setItem('v1:debug-and-preview-panel-width', '450')
|
||||||
|
|
||||||
const manager = createPreviewPanelManager()
|
const manager = createPreviewPanelManager()
|
||||||
|
|
||||||
@@ -98,8 +98,8 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
|||||||
manager.updateWidth(200, 'system')
|
manager.updateWidth(200, 'system')
|
||||||
|
|
||||||
// Only preview panel storage key should be unaffected
|
// Only preview panel storage key should be unaffected
|
||||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('450')
|
expect(localStorage.getItem('v1:debug-and-preview-panel-width')).toBe('450')
|
||||||
expect(localStorage.getItem('workflow-node-panel-width')).toBe('600')
|
expect(localStorage.getItem('v1:workflow-node-panel-width')).toBe('600')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle F12 scenario consistently', () => {
|
it('should handle F12 scenario consistently', () => {
|
||||||
@@ -107,13 +107,13 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
|||||||
|
|
||||||
// User sets preference
|
// User sets preference
|
||||||
manager.updateWidth(500, 'user')
|
manager.updateWidth(500, 'user')
|
||||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
expect(localStorage.getItem('v1:debug-and-preview-panel-width')).toBe('500')
|
||||||
|
|
||||||
// F12 opens causing viewport compression
|
// F12 opens causing viewport compression
|
||||||
manager.updateWidth(180, 'system')
|
manager.updateWidth(180, 'system')
|
||||||
|
|
||||||
// User preference preserved
|
// User preference preserved
|
||||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
expect(localStorage.getItem('v1:debug-and-preview-panel-width')).toBe('500')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
|||||||
// Same 400px minimum as Node Panel
|
// Same 400px minimum as Node Panel
|
||||||
const result = manager.updateWidth(300, 'user')
|
const result = manager.updateWidth(300, 'user')
|
||||||
expect(result).toBe(400)
|
expect(result).toBe(400)
|
||||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '400')
|
expect(localStorage.setItem).toHaveBeenCalledWith('v1:debug-and-preview-panel-width', '400')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use same source parameter pattern', () => {
|
it('should use same source parameter pattern', () => {
|
||||||
@@ -132,7 +132,7 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
|||||||
|
|
||||||
// Default to 'user' when source not specified
|
// Default to 'user' when source not specified
|
||||||
manager.updateWidth(500)
|
manager.updateWidth(500)
|
||||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500')
|
expect(localStorage.setItem).toHaveBeenCalledWith('v1:debug-and-preview-panel-width', '500')
|
||||||
|
|
||||||
// Explicit 'system' source
|
// Explicit 'system' source
|
||||||
manager.updateWidth(300, 'system')
|
manager.updateWidth(300, 'system')
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import Tooltip from '@/app/components/base/tooltip'
|
|||||||
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
|
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
|
||||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import {
|
import {
|
||||||
useWorkflowInteractions,
|
useWorkflowInteractions,
|
||||||
} from '../../hooks'
|
} from '../../hooks'
|
||||||
@@ -56,7 +58,7 @@ const DebugAndPreview = () => {
|
|||||||
const setPanelWidth = useStore(s => s.setPreviewPanelWidth)
|
const setPanelWidth = useStore(s => s.setPreviewPanelWidth)
|
||||||
const handleResize = useCallback((width: number, source: 'user' | 'system' = 'user') => {
|
const handleResize = useCallback((width: number, source: 'user' | 'system' = 'user') => {
|
||||||
if (source === 'user')
|
if (source === 'user')
|
||||||
localStorage.setItem('debug-and-preview-panel-width', `${width}`)
|
storage.set(STORAGE_KEYS.WORKFLOW.PREVIEW_PANEL_WIDTH, width)
|
||||||
setPanelWidth(width)
|
setPanelWidth(width)
|
||||||
}, [setPanelWidth])
|
}, [setPanelWidth])
|
||||||
const maxPanelWidth = useMemo(() => {
|
const maxPanelWidth = useMemo(() => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export type LayoutSliceShape = {
|
export type LayoutSliceShape = {
|
||||||
workflowCanvasWidth?: number
|
workflowCanvasWidth?: number
|
||||||
workflowCanvasHeight?: number
|
workflowCanvasHeight?: number
|
||||||
setWorkflowCanvasWidth: (width: number) => void
|
setWorkflowCanvasWidth: (width: number) => void
|
||||||
setWorkflowCanvasHeight: (height: number) => void
|
setWorkflowCanvasHeight: (height: number) => void
|
||||||
// rightPanelWidth - otherPanelWidth = nodePanelWidth
|
|
||||||
rightPanelWidth?: number
|
rightPanelWidth?: number
|
||||||
setRightPanelWidth: (width: number) => void
|
setRightPanelWidth: (width: number) => void
|
||||||
nodePanelWidth: number
|
nodePanelWidth: number
|
||||||
@@ -14,11 +15,11 @@ export type LayoutSliceShape = {
|
|||||||
setPreviewPanelWidth: (width: number) => void
|
setPreviewPanelWidth: (width: number) => void
|
||||||
otherPanelWidth: number
|
otherPanelWidth: number
|
||||||
setOtherPanelWidth: (width: number) => void
|
setOtherPanelWidth: (width: number) => void
|
||||||
bottomPanelWidth: number // min-width = 400px; default-width = auto || 480px;
|
bottomPanelWidth: number
|
||||||
setBottomPanelWidth: (width: number) => void
|
setBottomPanelWidth: (width: number) => void
|
||||||
bottomPanelHeight: number
|
bottomPanelHeight: number
|
||||||
setBottomPanelHeight: (height: number) => void
|
setBottomPanelHeight: (height: number) => void
|
||||||
variableInspectPanelHeight: number // min-height = 120px; default-height = 320px;
|
variableInspectPanelHeight: number
|
||||||
setVariableInspectPanelHeight: (height: number) => void
|
setVariableInspectPanelHeight: (height: number) => void
|
||||||
maximizeCanvas: boolean
|
maximizeCanvas: boolean
|
||||||
setMaximizeCanvas: (maximize: boolean) => void
|
setMaximizeCanvas: (maximize: boolean) => void
|
||||||
@@ -31,9 +32,9 @@ export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({
|
|||||||
setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })),
|
setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })),
|
||||||
rightPanelWidth: undefined,
|
rightPanelWidth: undefined,
|
||||||
setRightPanelWidth: width => set(() => ({ rightPanelWidth: width })),
|
setRightPanelWidth: width => set(() => ({ rightPanelWidth: width })),
|
||||||
nodePanelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 400,
|
nodePanelWidth: storage.getNumber(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, 420),
|
||||||
setNodePanelWidth: width => set(() => ({ nodePanelWidth: width })),
|
setNodePanelWidth: width => set(() => ({ nodePanelWidth: width })),
|
||||||
previewPanelWidth: localStorage.getItem('debug-and-preview-panel-width') ? Number.parseFloat(localStorage.getItem('debug-and-preview-panel-width')!) : 400,
|
previewPanelWidth: storage.getNumber(STORAGE_KEYS.WORKFLOW.PREVIEW_PANEL_WIDTH, 400),
|
||||||
setPreviewPanelWidth: width => set(() => ({ previewPanelWidth: width })),
|
setPreviewPanelWidth: width => set(() => ({ previewPanelWidth: width })),
|
||||||
otherPanelWidth: 400,
|
otherPanelWidth: 400,
|
||||||
setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })),
|
setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })),
|
||||||
@@ -41,8 +42,8 @@ export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({
|
|||||||
setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })),
|
setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })),
|
||||||
bottomPanelHeight: 324,
|
bottomPanelHeight: 324,
|
||||||
setBottomPanelHeight: height => set(() => ({ bottomPanelHeight: height })),
|
setBottomPanelHeight: height => set(() => ({ bottomPanelHeight: height })),
|
||||||
variableInspectPanelHeight: localStorage.getItem('workflow-variable-inpsect-panel-height') ? Number.parseFloat(localStorage.getItem('workflow-variable-inpsect-panel-height')!) : 320,
|
variableInspectPanelHeight: storage.getNumber(STORAGE_KEYS.WORKFLOW.VARIABLE_INSPECT_PANEL_HEIGHT, 320),
|
||||||
setVariableInspectPanelHeight: height => set(() => ({ variableInspectPanelHeight: height })),
|
setVariableInspectPanelHeight: height => set(() => ({ variableInspectPanelHeight: height })),
|
||||||
maximizeCanvas: localStorage.getItem('workflow-canvas-maximize') === 'true',
|
maximizeCanvas: storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false),
|
||||||
setMaximizeCanvas: maximize => set(() => ({ maximizeCanvas: maximize })),
|
setMaximizeCanvas: maximize => set(() => ({ maximizeCanvas: maximize })),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
|
|
||||||
export type PanelSliceShape = {
|
export type PanelSliceShape = {
|
||||||
panelWidth: number
|
|
||||||
showFeaturesPanel: boolean
|
showFeaturesPanel: boolean
|
||||||
setShowFeaturesPanel: (showFeaturesPanel: boolean) => void
|
setShowFeaturesPanel: (showFeaturesPanel: boolean) => void
|
||||||
showWorkflowVersionHistoryPanel: boolean
|
showWorkflowVersionHistoryPanel: boolean
|
||||||
@@ -27,7 +26,6 @@ export type PanelSliceShape = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({
|
export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({
|
||||||
panelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420,
|
|
||||||
showFeaturesPanel: false,
|
showFeaturesPanel: false,
|
||||||
setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })),
|
setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })),
|
||||||
showWorkflowVersionHistoryPanel: false,
|
showWorkflowVersionHistoryPanel: false,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type {
|
|||||||
WorkflowRunningData,
|
WorkflowRunningData,
|
||||||
} from '@/app/components/workflow/types'
|
} from '@/app/components/workflow/types'
|
||||||
import type { FileUploadConfigResponse } from '@/models/common'
|
import type { FileUploadConfigResponse } from '@/models/common'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
type PreviewRunningData = WorkflowRunningData & {
|
type PreviewRunningData = WorkflowRunningData & {
|
||||||
resultTabActive?: boolean
|
resultTabActive?: boolean
|
||||||
@@ -63,10 +65,10 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
|
|||||||
setSelection: selection => set(() => ({ selection })),
|
setSelection: selection => set(() => ({ selection })),
|
||||||
bundleNodeSize: null,
|
bundleNodeSize: null,
|
||||||
setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })),
|
setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })),
|
||||||
controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : 'hand',
|
controlMode: storage.get<'pointer' | 'hand'>(STORAGE_KEYS.WORKFLOW.OPERATION_MODE) === 'pointer' ? 'pointer' : 'hand',
|
||||||
setControlMode: (controlMode) => {
|
setControlMode: (controlMode) => {
|
||||||
set(() => ({ controlMode }))
|
set(() => ({ controlMode }))
|
||||||
localStorage.setItem('workflow-operation-mode', controlMode)
|
storage.set(STORAGE_KEYS.WORKFLOW.OPERATION_MODE, controlMode)
|
||||||
},
|
},
|
||||||
mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 },
|
mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 },
|
||||||
setMousePosition: mousePosition => set(() => ({ mousePosition })),
|
setMousePosition: mousePosition => set(() => ({ mousePosition })),
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
|
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
|
||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
import Panel from './panel'
|
import Panel from './panel'
|
||||||
@@ -21,8 +23,8 @@ const VariableInspectPanel: FC = () => {
|
|||||||
return workflowCanvasHeight - 60
|
return workflowCanvasHeight - 60
|
||||||
}, [workflowCanvasHeight])
|
}, [workflowCanvasHeight])
|
||||||
|
|
||||||
const handleResize = useCallback((width: number, height: number) => {
|
const handleResize = useCallback((_width: number, height: number) => {
|
||||||
localStorage.setItem('workflow-variable-inpsect-panel-height', `${height}`)
|
storage.set(STORAGE_KEYS.WORKFLOW.VARIABLE_INSPECT_PANEL_HEIGHT, height)
|
||||||
setVariableInspectPanelHeight(height)
|
setVariableInspectPanelHeight(height)
|
||||||
}, [setVariableInspectPanelHeight])
|
}, [setVariableInspectPanelHeight])
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Checkbox from '@/app/components/base/checkbox'
|
import Checkbox from '@/app/components/base/checkbox'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import {
|
import {
|
||||||
useEducationAdd,
|
useEducationAdd,
|
||||||
useInvalidateEducationStatus,
|
useInvalidateEducationStatus,
|
||||||
} from '@/service/use-education'
|
} from '@/service/use-education'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import DifyLogo from '../components/base/logo/dify-logo'
|
import DifyLogo from '../components/base/logo/dify-logo'
|
||||||
import RoleSelector from './role-selector'
|
import RoleSelector from './role-selector'
|
||||||
import SearchInput from './search-input'
|
import SearchInput from './search-input'
|
||||||
@@ -47,7 +48,7 @@ const EducationApplyAge = () => {
|
|||||||
setShowModal(undefined)
|
setShowModal(undefined)
|
||||||
onPlanInfoChanged()
|
onPlanInfoChanged()
|
||||||
updateEducationStatus()
|
updateEducationStatus()
|
||||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
storage.remove(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||||
router.replace('/')
|
router.replace('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education'
|
import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import {
|
import {
|
||||||
EDUCATION_RE_VERIFY_ACTION,
|
EDUCATION_RE_VERIFY_ACTION,
|
||||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
|
||||||
} from './constants'
|
} from './constants'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
@@ -133,7 +134,7 @@ const useEducationReverifyNotice = ({
|
|||||||
export const useEducationInit = () => {
|
export const useEducationInit = () => {
|
||||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||||
const setShowEducationExpireNoticeModal = useModalContextSelector(s => s.setShowEducationExpireNoticeModal)
|
const setShowEducationExpireNoticeModal = useModalContextSelector(s => s.setShowEducationExpireNoticeModal)
|
||||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
const educationVerifying = storage.get<string>(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const educationVerifyAction = searchParams.get('action')
|
const educationVerifyAction = searchParams.get('action')
|
||||||
|
|
||||||
@@ -156,7 +157,7 @@ export const useEducationInit = () => {
|
|||||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||||
|
|
||||||
if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
storage.set(STORAGE_KEYS.EDUCATION.VERIFYING, 'yes')
|
||||||
}
|
}
|
||||||
if (educationVerifyAction === EDUCATION_RE_VERIFY_ACTION)
|
if (educationVerifyAction === EDUCATION_RE_VERIFY_ACTION)
|
||||||
handleVerify()
|
handleVerify()
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Avatar from '@/app/components/base/avatar'
|
import Avatar from '@/app/components/base/avatar'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { Triangle } from '@/app/components/base/icons/src/public/education'
|
import { Triangle } from '@/app/components/base/icons/src/public/education'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
const UserInfo = () => {
|
const UserInfo = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -15,7 +17,7 @@ const UserInfo = () => {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
|
|
||||||
localStorage.removeItem('setup_status')
|
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||||
// Tokens are now stored in cookies and cleared by backend
|
// Tokens are now stored in cookies and cleared by backend
|
||||||
|
|
||||||
router.push('/signin')
|
router.push('/signin')
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ describe('InstallForm', () => {
|
|||||||
render(<InstallForm />)
|
render(<InstallForm />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(localStorage.setItem).toHaveBeenCalledWith('setup_status', 'finished')
|
expect(localStorage.setItem).toHaveBeenCalledWith('v1:setup_status', 'finished')
|
||||||
expect(mockPush).toHaveBeenCalledWith('/signin')
|
expect(mockPush).toHaveBeenCalledWith('/signin')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import { formContext, useAppForm } from '@/app/components/base/form'
|
|||||||
import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator'
|
import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-validator'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import { validPassword } from '@/config'
|
import { validPassword } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
|
||||||
import { LICENSE_LINK } from '@/constants/link'
|
import { LICENSE_LINK } from '@/constants/link'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import { encryptPassword as encodePassword } from '@/utils/encryption'
|
import { encryptPassword as encodePassword } from '@/utils/encryption'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import Loading from '../components/base/loading'
|
import Loading from '../components/base/loading'
|
||||||
|
|
||||||
const accountFormSchema = z.object({
|
const accountFormSchema = z.object({
|
||||||
@@ -85,7 +87,7 @@ const InstallForm = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSetupStatus().then((res: SetupStatusResponse) => {
|
fetchSetupStatus().then((res: SetupStatusResponse) => {
|
||||||
if (res.step === 'finished') {
|
if (res.step === 'finished') {
|
||||||
localStorage.setItem('setup_status', 'finished')
|
storage.set(STORAGE_KEYS.CONFIG.SETUP_STATUS, 'finished')
|
||||||
router.push('/signin')
|
router.push('/signin')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import Button from '@/app/components/base/button'
|
|||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { emailRegex } from '@/config'
|
import { emailRegex } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import useDocumentTitle from '@/hooks/use-document-title'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
import { sendResetPasswordCode } from '@/service/common'
|
import { sendResetPasswordCode } from '@/service/common'
|
||||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '../components/signin/countdown'
|
import { storage } from '@/utils/storage'
|
||||||
|
import { COUNT_DOWN_TIME_MS } from '../components/signin/countdown'
|
||||||
|
|
||||||
export default function CheckCode() {
|
export default function CheckCode() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -40,7 +42,7 @@ export default function CheckCode() {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const res = await sendResetPasswordCode(email, locale)
|
const res = await sendResetPasswordCode(email, locale)
|
||||||
if (res.result === 'success') {
|
if (res.result === 'success') {
|
||||||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, COUNT_DOWN_TIME_MS)
|
||||||
const params = new URLSearchParams(searchParams)
|
const params = new URLSearchParams(searchParams)
|
||||||
params.set('token', encodeURIComponent(res.data))
|
params.set('token', encodeURIComponent(res.data))
|
||||||
params.set('email', encodeURIComponent(email))
|
params.set('email', encodeURIComponent(email))
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
import { COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||||
import { emailRegex } from '@/config'
|
import { emailRegex } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { sendEMailLoginCode } from '@/service/common'
|
import { sendEMailLoginCode } from '@/service/common'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
type MailAndCodeAuthProps = {
|
type MailAndCodeAuthProps = {
|
||||||
isInvite: boolean
|
isInvite: boolean
|
||||||
@@ -40,7 +42,7 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const ret = await sendEMailLoginCode(email, locale)
|
const ret = await sendEMailLoginCode(email, locale)
|
||||||
if (ret.result === 'success') {
|
if (ret.result === 'success') {
|
||||||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, COUNT_DOWN_TIME_MS)
|
||||||
const params = new URLSearchParams(searchParams)
|
const params = new URLSearchParams(searchParams)
|
||||||
params.set('email', encodeURIComponent(email))
|
params.set('email', encodeURIComponent(email))
|
||||||
params.set('token', encodeURIComponent(ret.data))
|
params.set('token', encodeURIComponent(ret.data))
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import type { ReadonlyURLSearchParams } from 'next/navigation'
|
import type { ReadonlyURLSearchParams } from 'next/navigation'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { OAUTH_AUTHORIZE_PENDING_KEY, REDIRECT_URL_KEY } from '@/app/account/oauth/authorize/constants'
|
import { REDIRECT_URL_KEY } from '@/app/account/oauth/authorize/constants'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
function getItemWithExpiry(key: string): string | null {
|
function getItemWithExpiry(key: string): string | null {
|
||||||
const itemStr = localStorage.getItem(key)
|
const itemStr = storage.get<string>(key)
|
||||||
if (!itemStr)
|
if (!itemStr)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = JSON.parse(itemStr)
|
const item = JSON.parse(itemStr)
|
||||||
localStorage.removeItem(key)
|
storage.remove(key)
|
||||||
if (!item?.value)
|
if (!item?.value)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ export const resolvePostLoginRedirect = (searchParams: ReadonlyURLSearchParams)
|
|||||||
const redirectUrl = searchParams.get(REDIRECT_URL_KEY)
|
const redirectUrl = searchParams.get(REDIRECT_URL_KEY)
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(OAUTH_AUTHORIZE_PENDING_KEY)
|
storage.remove(STORAGE_KEYS.AUTH.OAUTH_AUTHORIZE_PENDING)
|
||||||
return decodeURIComponent(redirectUrl)
|
return decodeURIComponent(redirectUrl)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@@ -33,5 +35,5 @@ export const resolvePostLoginRedirect = (searchParams: ReadonlyURLSearchParams)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getItemWithExpiry(OAUTH_AUTHORIZE_PENDING_KEY)
|
return getItemWithExpiry(STORAGE_KEYS.AUTH.OAUTH_AUTHORIZE_PENDING)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { InputVarType } from '@/app/components/workflow/types'
|
import { InputVarType } from '@/app/components/workflow/types'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { PromptRole } from '@/models/debug'
|
import { PromptRole } from '@/models/debug'
|
||||||
import { PipelineInputVarType } from '@/models/pipeline'
|
import { PipelineInputVarType } from '@/models/pipeline'
|
||||||
import { AgentStrategy } from '@/types/app'
|
import { AgentStrategy } from '@/types/app'
|
||||||
@@ -179,7 +180,7 @@ export const CSRF_COOKIE_NAME = () => {
|
|||||||
return isSecure ? '__Host-csrf_token' : 'csrf_token'
|
return isSecure ? '__Host-csrf_token' : 'csrf_token'
|
||||||
}
|
}
|
||||||
export const CSRF_HEADER_NAME = 'X-CSRF-Token'
|
export const CSRF_HEADER_NAME = 'X-CSRF-Token'
|
||||||
export const ACCESS_TOKEN_LOCAL_STORAGE_NAME = 'access_token'
|
export const ACCESS_TOKEN_LOCAL_STORAGE_NAME = STORAGE_KEYS.AUTH.ACCESS_TOKEN
|
||||||
export const PASSPORT_LOCAL_STORAGE_NAME = (appCode: string) => `passport-${appCode}`
|
export const PASSPORT_LOCAL_STORAGE_NAME = (appCode: string) => `passport-${appCode}`
|
||||||
export const PASSPORT_HEADER_NAME = 'X-App-Passport'
|
export const PASSPORT_HEADER_NAME = 'X-App-Passport'
|
||||||
|
|
||||||
@@ -229,7 +230,7 @@ export const VAR_ITEM_TEMPLATE_IN_PIPELINE = {
|
|||||||
|
|
||||||
export const appDefaultIconBackground = '#D5F5F6'
|
export const appDefaultIconBackground = '#D5F5F6'
|
||||||
|
|
||||||
export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'
|
export const NEED_REFRESH_APP_LIST_KEY = STORAGE_KEYS.APP.NEED_REFRESH_LIST
|
||||||
|
|
||||||
export const DATASET_DEFAULT = {
|
export const DATASET_DEFAULT = {
|
||||||
top_k: 4,
|
top_k: 4,
|
||||||
|
|||||||
77
web/config/storage-keys.ts
Normal file
77
web/config/storage-keys.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
export const STORAGE_KEYS = {
|
||||||
|
WORKFLOW: {
|
||||||
|
NODE_PANEL_WIDTH: 'workflow-node-panel-width',
|
||||||
|
PREVIEW_PANEL_WIDTH: 'debug-and-preview-panel-width',
|
||||||
|
VARIABLE_INSPECT_PANEL_HEIGHT: 'workflow-variable-inspect-panel-height',
|
||||||
|
CANVAS_MAXIMIZE: 'workflow-canvas-maximize',
|
||||||
|
OPERATION_MODE: 'workflow-operation-mode',
|
||||||
|
RAG_RECOMMENDATIONS_COLLAPSED: 'workflow_rag_recommendations_collapsed',
|
||||||
|
TOOLS_FEATURED_COLLAPSED: 'workflow_tools_featured_collapsed',
|
||||||
|
TRIGGERS_FEATURED_COLLAPSED: 'workflow_triggers_featured_collapsed',
|
||||||
|
},
|
||||||
|
APP: {
|
||||||
|
SIDEBAR_COLLAPSE: 'webappSidebarCollapse',
|
||||||
|
NEED_REFRESH_LIST: 'needRefreshAppList',
|
||||||
|
DETAIL_COLLAPSE: 'app-detail-collapse-or-expand',
|
||||||
|
},
|
||||||
|
CONVERSATION: {
|
||||||
|
ID_INFO: 'conversationIdInfo',
|
||||||
|
},
|
||||||
|
AUTH: {
|
||||||
|
ACCESS_TOKEN: 'access_token',
|
||||||
|
REFRESH_LOCK: 'is_other_tab_refreshing',
|
||||||
|
LAST_REFRESH_TIME: 'last_refresh_time',
|
||||||
|
OAUTH_AUTHORIZE_PENDING: 'oauth_authorize_pending',
|
||||||
|
},
|
||||||
|
EDUCATION: {
|
||||||
|
VERIFYING: 'educationVerifying',
|
||||||
|
REVERIFY_PREV_EXPIRE_AT: 'education-reverify-prev-expire-at',
|
||||||
|
REVERIFY_HAS_NOTICED: 'education-reverify-has-noticed',
|
||||||
|
EXPIRED_HAS_NOTICED: 'education-expired-has-noticed',
|
||||||
|
},
|
||||||
|
CONFIG: {
|
||||||
|
AUTO_GEN_MODEL: 'auto-gen-model',
|
||||||
|
DEBUG_MODELS: 'app-debug-with-single-or-multiple-models',
|
||||||
|
SETUP_STATUS: 'setup_status',
|
||||||
|
},
|
||||||
|
UI: {
|
||||||
|
THEME: 'theme',
|
||||||
|
ANTHROPIC_QUOTA_NOTICE: 'anthropic_quota_notice',
|
||||||
|
HIDE_MAINTENANCE_NOTICE: 'hide-maintenance-notice',
|
||||||
|
COUNTDOWN_LEFT_TIME: 'leftTime',
|
||||||
|
SHOW_MANAGE_METADATA: 'dify-isShowManageMetadata',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type StorageKeys = typeof STORAGE_KEYS
|
||||||
|
|
||||||
|
export const LEGACY_KEY_MIGRATIONS: Array<{ old: string, new: string }> = [
|
||||||
|
{ old: 'workflow-node-panel-width', new: 'workflow-node-panel-width' },
|
||||||
|
{ old: 'debug-and-preview-panel-width', new: 'debug-and-preview-panel-width' },
|
||||||
|
{ old: 'workflow-variable-inspect-panel-height', new: 'workflow-variable-inspect-panel-height' },
|
||||||
|
{ old: 'workflow-canvas-maximize', new: 'workflow-canvas-maximize' },
|
||||||
|
{ old: 'workflow-operation-mode', new: 'workflow-operation-mode' },
|
||||||
|
{ old: 'workflow_rag_recommendations_collapsed', new: 'workflow_rag_recommendations_collapsed' },
|
||||||
|
{ old: 'workflow_tools_featured_collapsed', new: 'workflow_tools_featured_collapsed' },
|
||||||
|
{ old: 'workflow_triggers_featured_collapsed', new: 'workflow_triggers_featured_collapsed' },
|
||||||
|
{ old: 'webappSidebarCollapse', new: 'webappSidebarCollapse' },
|
||||||
|
{ old: 'needRefreshAppList', new: 'needRefreshAppList' },
|
||||||
|
{ old: 'app-detail-collapse-or-expand', new: 'app-detail-collapse-or-expand' },
|
||||||
|
{ old: 'conversationIdInfo', new: 'conversationIdInfo' },
|
||||||
|
{ old: 'access_token', new: 'access_token' },
|
||||||
|
{ old: 'is_other_tab_refreshing', new: 'is_other_tab_refreshing' },
|
||||||
|
{ old: 'last_refresh_time', new: 'last_refresh_time' },
|
||||||
|
{ old: 'oauth_authorize_pending', new: 'oauth_authorize_pending' },
|
||||||
|
{ old: 'educationVerifying', new: 'educationVerifying' },
|
||||||
|
{ old: 'education-reverify-prev-expire-at', new: 'education-reverify-prev-expire-at' },
|
||||||
|
{ old: 'education-reverify-has-noticed', new: 'education-reverify-has-noticed' },
|
||||||
|
{ old: 'education-expired-has-noticed', new: 'education-expired-has-noticed' },
|
||||||
|
{ old: 'auto-gen-model', new: 'auto-gen-model' },
|
||||||
|
{ old: 'app-debug-with-single-or-multiple-models', new: 'app-debug-with-single-or-multiple-models' },
|
||||||
|
{ old: 'setup_status', new: 'setup_status' },
|
||||||
|
{ old: 'theme', new: 'theme' },
|
||||||
|
{ old: 'anthropic_quota_notice', new: 'anthropic_quota_notice' },
|
||||||
|
{ old: 'hide-maintenance-notice', new: 'hide-maintenance-notice' },
|
||||||
|
{ old: 'leftTime', new: 'leftTime' },
|
||||||
|
{ old: 'dify-isShowManageMetadata', new: 'dify-isShowManageMetadata' },
|
||||||
|
]
|
||||||
@@ -6,6 +6,7 @@ import { NUM_INFINITE } from '@/app/components/billing/config'
|
|||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
import { isServer } from '@/utils/client'
|
import { isServer } from '@/utils/client'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export type TriggerEventsLimitModalPayload = {
|
export type TriggerEventsLimitModalPayload = {
|
||||||
usage: number
|
usage: number
|
||||||
@@ -80,15 +81,10 @@ export const useTriggerEventsLimitModal = ({
|
|||||||
if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey])
|
if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey])
|
||||||
return
|
return
|
||||||
|
|
||||||
let persistDismiss = true
|
const persistDismiss = storage.isAvailable()
|
||||||
let hasDismissed = false
|
let hasDismissed = false
|
||||||
try {
|
if (storage.get<string>(storageKey) === '1')
|
||||||
if (localStorage.getItem(storageKey) === '1')
|
hasDismissed = true
|
||||||
hasDismissed = true
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
persistDismiss = false
|
|
||||||
}
|
|
||||||
if (hasDismissed)
|
if (hasDismissed)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -110,16 +106,9 @@ export const useTriggerEventsLimitModal = ({
|
|||||||
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
|
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
|
||||||
if (!storageKey)
|
if (!storageKey)
|
||||||
return
|
return
|
||||||
if (showTriggerEventsLimitModal?.payload.persistDismiss) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(storageKey, '1')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// ignore error and fall back to in-memory guard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true
|
dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true
|
||||||
|
if (showTriggerEventsLimitModal?.payload.persistDismiss)
|
||||||
|
storage.set(storageKey, '1')
|
||||||
}, [showTriggerEventsLimitModal])
|
}, [showTriggerEventsLimitModal])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
|||||||
expect(setItemSpy.mock.calls.length).toBeGreaterThan(0)
|
expect(setItemSpy.mock.calls.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
const [key, value] = setItemSpy.mock.calls[0]
|
const [key, value] = setItemSpy.mock.calls[0]
|
||||||
expect(key).toContain('trigger-events-limit-dismissed-workspace-1-professional-3000-')
|
expect(key).toContain('v1:trigger-events-limit-dismissed-workspace-1-professional-3000-')
|
||||||
expect(value).toBe('1')
|
expect(value).toBe('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ import {
|
|||||||
DEFAULT_ACCOUNT_SETTING_TAB,
|
DEFAULT_ACCOUNT_SETTING_TAB,
|
||||||
isValidAccountSettingTab,
|
isValidAccountSettingTab,
|
||||||
} from '@/app/components/header/account-setting/constants'
|
} from '@/app/components/header/account-setting/constants'
|
||||||
import {
|
|
||||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
} from '@/app/education-apply/constants'
|
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import {
|
import {
|
||||||
useAccountSettingModal,
|
useAccountSettingModal,
|
||||||
usePricingModal,
|
usePricingModal,
|
||||||
} from '@/hooks/use-query-params'
|
} from '@/hooks/use-query-params'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
||||||
@@ -183,10 +183,10 @@ export const ModalContextProvider = ({
|
|||||||
|
|
||||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||||
const handleCancelAccountSettingModal = () => {
|
const handleCancelAccountSettingModal = () => {
|
||||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
const educationVerifying = storage.get<string>(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||||
|
|
||||||
if (educationVerifying === 'yes')
|
if (educationVerifying === 'yes')
|
||||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
storage.remove(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||||
|
|
||||||
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
||||||
accountSettingCallbacksRef.current = null
|
accountSettingCallbacksRef.current = null
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ModelTypeEnum,
|
ModelTypeEnum,
|
||||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { fetchCurrentPlanInfo } from '@/service/billing'
|
import { fetchCurrentPlanInfo } from '@/service/billing'
|
||||||
import {
|
import {
|
||||||
useModelListByType,
|
useModelListByType,
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
useEducationStatus,
|
useEducationStatus,
|
||||||
} from '@/service/use-education'
|
} from '@/service/use-education'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
export type ProviderContextState = {
|
export type ProviderContextState = {
|
||||||
modelProviders: ModelProvider[]
|
modelProviders: ModelProvider[]
|
||||||
@@ -200,7 +202,7 @@ export const ProviderContextProvider = ({
|
|||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem('anthropic_quota_notice') === 'true')
|
if (storage.get<string>(STORAGE_KEYS.UI.ANTHROPIC_QUOTA_NOTICE) === 'true')
|
||||||
return
|
return
|
||||||
|
|
||||||
if (dayjs().isAfter(dayjs('2025-03-17')))
|
if (dayjs().isAfter(dayjs('2025-03-17')))
|
||||||
@@ -216,7 +218,7 @@ export const ProviderContextProvider = ({
|
|||||||
message: t('provider.anthropicHosted.trialQuotaTip', { ns: 'common' }),
|
message: t('provider.anthropicHosted.trialQuotaTip', { ns: 'common' }),
|
||||||
duration: 60000,
|
duration: 60000,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
localStorage.setItem('anthropic_quota_notice', 'true')
|
storage.set(STORAGE_KEYS.UI.ANTHROPIC_QUOTA_NOTICE, 'true')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,9 @@
|
|||||||
"no-console": {
|
"no-console": {
|
||||||
"count": 16
|
"count": 16
|
||||||
},
|
},
|
||||||
|
"no-restricted-properties": {
|
||||||
|
"count": 5
|
||||||
|
},
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 4
|
"count": 4
|
||||||
},
|
},
|
||||||
@@ -505,6 +508,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/app/create-app-modal/index.spec.tsx": {
|
"app/components/app/create-app-modal/index.spec.tsx": {
|
||||||
|
"no-restricted-properties": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 7
|
"count": 7
|
||||||
}
|
}
|
||||||
@@ -579,6 +585,11 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/app/switch-app-modal/index.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/app/switch-app-modal/index.tsx": {
|
"app/components/app/switch-app-modal/index.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -629,6 +640,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/apps/list.spec.tsx": {
|
"app/components/apps/list.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 3
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 5
|
"count": 5
|
||||||
}
|
}
|
||||||
@@ -758,6 +772,11 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/base/chat/chat-with-history/hooks.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/base/chat/chat-with-history/hooks.tsx": {
|
"app/components/base/chat/chat-with-history/hooks.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 4
|
"count": 4
|
||||||
@@ -853,6 +872,11 @@
|
|||||||
"count": 7
|
"count": 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/base/chat/embedded-chatbot/hooks.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/base/chat/embedded-chatbot/hooks.tsx": {
|
"app/components/base/chat/embedded-chatbot/hooks.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 3
|
"count": 3
|
||||||
@@ -1526,6 +1550,11 @@
|
|||||||
"count": 4
|
"count": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/billing/plan/index.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/billing/plan/index.tsx": {
|
"app/components/billing/plan/index.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@@ -1551,6 +1580,11 @@
|
|||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/browser-initializer.tsx": {
|
||||||
|
"no-restricted-properties": {
|
||||||
|
"count": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/custom/custom-web-app-brand/index.spec.tsx": {
|
"app/components/custom/custom-web-app-brand/index.spec.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 7
|
"count": 7
|
||||||
@@ -3093,6 +3127,11 @@
|
|||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/workflow/nodes/_base/components/workflow-panel/index.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 18
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/workflow/nodes/_base/components/workflow-panel/index.tsx": {
|
"app/components/workflow/nodes/_base/components/workflow-panel/index.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 3
|
"count": 3
|
||||||
@@ -3827,6 +3866,11 @@
|
|||||||
"count": 7
|
"count": 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"app/components/workflow/panel/debug-and-preview/index.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 15
|
||||||
|
}
|
||||||
|
},
|
||||||
"app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
"app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 4
|
"count": 4
|
||||||
@@ -4097,6 +4141,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/install/installForm.spec.tsx": {
|
"app/install/installForm.spec.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 7
|
"count": 7
|
||||||
}
|
}
|
||||||
@@ -4137,6 +4184,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"context/modal-context.test.tsx": {
|
"context/modal-context.test.tsx": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 4
|
||||||
|
},
|
||||||
|
"no-restricted-properties": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
@@ -4505,6 +4558,11 @@
|
|||||||
"count": 4
|
"count": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"utils/setup-status.spec.ts": {
|
||||||
|
"no-restricted-globals": {
|
||||||
|
"count": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
"utils/tool-call.spec.ts": {
|
"utils/tool-call.spec.ts": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@@ -44,6 +44,40 @@ export default antfu(
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'node/prefer-global/process': 'off',
|
'node/prefer-global/process': 'off',
|
||||||
|
'no-restricted-globals': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
name: 'localStorage',
|
||||||
|
message: 'Use @/utils/storage instead. Direct localStorage access causes SSR issues.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sessionStorage',
|
||||||
|
message: 'Use @/utils/storage instead. Direct sessionStorage access causes SSR issues.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-restricted-properties': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'localStorage',
|
||||||
|
message: 'Use @/utils/storage instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'sessionStorage',
|
||||||
|
message: 'Use @/utils/storage instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
object: 'globalThis',
|
||||||
|
property: 'localStorage',
|
||||||
|
message: 'Use @/utils/storage instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
object: 'globalThis',
|
||||||
|
property: 'sessionStorage',
|
||||||
|
message: 'Use @/utils/storage instead.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { useSelector } from '@/context/app-context'
|
import { useSelector } from '@/context/app-context'
|
||||||
import { DSLImportStatus } from '@/models/app'
|
import { DSLImportStatus } from '@/models/app'
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
importDSLConfirm,
|
importDSLConfirm,
|
||||||
} from '@/service/apps'
|
} from '@/service/apps'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
|
|
||||||
type DSLPayload = {
|
type DSLPayload = {
|
||||||
mode: DSLImportMode
|
mode: DSLImportMode
|
||||||
@@ -83,7 +84,7 @@ export const useImportDSL = () => {
|
|||||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
|
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
|
||||||
})
|
})
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
await handleCheckPluginDependencies(app_id)
|
await handleCheckPluginDependencies(app_id)
|
||||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id, mode: app_mode }, push)
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id, mode: app_mode }, push)
|
||||||
}
|
}
|
||||||
@@ -137,7 +138,7 @@ export const useImportDSL = () => {
|
|||||||
message: t('newApp.appCreated', { ns: 'app' }),
|
message: t('newApp.appCreated', { ns: 'app' }),
|
||||||
})
|
})
|
||||||
await handleCheckPluginDependencies(app_id)
|
await handleCheckPluginDependencies(app_id)
|
||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||||
}
|
}
|
||||||
else if (status === DSLImportStatus.FAILED) {
|
else if (status === DSLImportStatus.FAILED) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { API_PREFIX } from '@/config'
|
import { API_PREFIX } from '@/config'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { fetchWithRetry } from '@/utils'
|
import { fetchWithRetry } from '@/utils'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
const LOCAL_STORAGE_KEY = 'is_other_tab_refreshing'
|
|
||||||
|
|
||||||
let isRefreshing = false
|
let isRefreshing = false
|
||||||
function waitUntilTokenRefreshed() {
|
function waitUntilTokenRefreshed() {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
function _check() {
|
function _check() {
|
||||||
const isRefreshingSign = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY)
|
const isRefreshingSign = storage.get<string>(STORAGE_KEYS.AUTH.REFRESH_LOCK)
|
||||||
if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) {
|
if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
_check()
|
_check()
|
||||||
@@ -23,35 +23,28 @@ function waitUntilTokenRefreshed() {
|
|||||||
|
|
||||||
const isRefreshingSignAvailable = function (delta: number) {
|
const isRefreshingSignAvailable = function (delta: number) {
|
||||||
const nowTime = new Date().getTime()
|
const nowTime = new Date().getTime()
|
||||||
const lastTime = globalThis.localStorage.getItem('last_refresh_time') || '0'
|
const lastTime = storage.get<string>(STORAGE_KEYS.AUTH.LAST_REFRESH_TIME) || '0'
|
||||||
return nowTime - Number.parseInt(lastTime) <= delta
|
return nowTime - Number.parseInt(lastTime) <= delta
|
||||||
}
|
}
|
||||||
|
|
||||||
// only one request can send
|
|
||||||
async function getNewAccessToken(timeout: number): Promise<void> {
|
async function getNewAccessToken(timeout: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const isRefreshingSign = globalThis.localStorage.getItem(LOCAL_STORAGE_KEY)
|
const isRefreshingSign = storage.get<string>(STORAGE_KEYS.AUTH.REFRESH_LOCK)
|
||||||
if ((isRefreshingSign && isRefreshingSign === '1' && isRefreshingSignAvailable(timeout)) || isRefreshing) {
|
if ((isRefreshingSign && isRefreshingSign === '1' && isRefreshingSignAvailable(timeout)) || isRefreshing) {
|
||||||
await waitUntilTokenRefreshed()
|
await waitUntilTokenRefreshed()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
globalThis.localStorage.setItem(LOCAL_STORAGE_KEY, '1')
|
storage.set(STORAGE_KEYS.AUTH.REFRESH_LOCK, '1')
|
||||||
globalThis.localStorage.setItem('last_refresh_time', new Date().getTime().toString())
|
storage.set(STORAGE_KEYS.AUTH.LAST_REFRESH_TIME, new Date().getTime().toString())
|
||||||
globalThis.addEventListener('beforeunload', releaseRefreshLock)
|
globalThis.addEventListener('beforeunload', releaseRefreshLock)
|
||||||
|
|
||||||
// Do not use baseFetch to refresh tokens.
|
|
||||||
// If a 401 response occurs and baseFetch itself attempts to refresh the token,
|
|
||||||
// it can lead to an infinite loop if the refresh attempt also returns 401.
|
|
||||||
// To avoid this, handle token refresh separately in a dedicated function
|
|
||||||
// that does not call baseFetch and uses a single retry mechanism.
|
|
||||||
const [error, ret] = await fetchWithRetry(globalThis.fetch(`${API_PREFIX}/refresh-token`, {
|
const [error, ret] = await fetchWithRetry(globalThis.fetch(`${API_PREFIX}/refresh-token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include', // Important: include cookies in the request
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json;utf-8',
|
'Content-Type': 'application/json;utf-8',
|
||||||
},
|
},
|
||||||
// No body needed - refresh token is in cookie
|
|
||||||
}))
|
}))
|
||||||
if (error) {
|
if (error) {
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
@@ -72,11 +65,9 @@ async function getNewAccessToken(timeout: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function releaseRefreshLock() {
|
function releaseRefreshLock() {
|
||||||
// Always clear the refresh lock to avoid cross-tab deadlocks.
|
|
||||||
// This is safe to call multiple times and from tabs that were only waiting.
|
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
globalThis.localStorage.removeItem(LOCAL_STORAGE_KEY)
|
storage.remove(STORAGE_KEYS.AUTH.REFRESH_LOCK)
|
||||||
globalThis.localStorage.removeItem('last_refresh_time')
|
storage.remove(STORAGE_KEYS.AUTH.LAST_REFRESH_TIME)
|
||||||
globalThis.removeEventListener('beforeunload', releaseRefreshLock)
|
globalThis.removeEventListener('beforeunload', releaseRefreshLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import { ACCESS_TOKEN_LOCAL_STORAGE_NAME, PASSPORT_LOCAL_STORAGE_NAME } from '@/config'
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { getPublic, postPublic } from './base'
|
import { getPublic, postPublic } from './base'
|
||||||
|
|
||||||
export function setWebAppAccessToken(token: string) {
|
export function setWebAppAccessToken(token: string) {
|
||||||
localStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME, token)
|
storage.set(STORAGE_KEYS.AUTH.ACCESS_TOKEN, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setWebAppPassport(shareCode: string, token: string) {
|
export function setWebAppPassport(shareCode: string, token: string) {
|
||||||
localStorage.setItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode), token)
|
storage.set(`passport-${shareCode}`, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebAppAccessToken() {
|
export function getWebAppAccessToken() {
|
||||||
return localStorage.getItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME) || ''
|
return storage.get<string>(STORAGE_KEYS.AUTH.ACCESS_TOKEN) || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebAppPassport(shareCode: string) {
|
export function getWebAppPassport(shareCode: string) {
|
||||||
return localStorage.getItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode)) || ''
|
return storage.get<string>(`passport-${shareCode}`) || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearWebAppAccessToken() {
|
export function clearWebAppAccessToken() {
|
||||||
localStorage.removeItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME)
|
storage.remove(STORAGE_KEYS.AUTH.ACCESS_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearWebAppPassport(shareCode: string) {
|
export function clearWebAppPassport(shareCode: string) {
|
||||||
localStorage.removeItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode))
|
storage.remove(`passport-${shareCode}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
type isWebAppLogin = {
|
type isWebAppLogin = {
|
||||||
@@ -31,8 +32,6 @@ type isWebAppLogin = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function webAppLoginStatus(shareCode: string, userId?: string) {
|
export async function webAppLoginStatus(shareCode: string, userId?: string) {
|
||||||
// always need to check login to prevent passport from being outdated
|
|
||||||
// check remotely, the access token could be in cookie (enterprise SSO redirected with https)
|
|
||||||
const params = new URLSearchParams({ app_code: shareCode })
|
const params = new URLSearchParams({ app_code: shareCode })
|
||||||
if (userId)
|
if (userId)
|
||||||
params.append('user_id', userId)
|
params.append('user_id', userId)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('setup-status utilities', () => {
|
|||||||
describe('fetchSetupStatusWithCache', () => {
|
describe('fetchSetupStatusWithCache', () => {
|
||||||
describe('when cache exists', () => {
|
describe('when cache exists', () => {
|
||||||
it('should return cached finished status without API call', async () => {
|
it('should return cached finished status without API call', async () => {
|
||||||
localStorage.setItem('setup_status', 'finished')
|
localStorage.setItem('v1:setup_status', 'finished')
|
||||||
|
|
||||||
const result = await fetchSetupStatusWithCache()
|
const result = await fetchSetupStatusWithCache()
|
||||||
|
|
||||||
@@ -28,11 +28,11 @@ describe('setup-status utilities', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not modify localStorage when returning cached value', async () => {
|
it('should not modify localStorage when returning cached value', async () => {
|
||||||
localStorage.setItem('setup_status', 'finished')
|
localStorage.setItem('v1:setup_status', 'finished')
|
||||||
|
|
||||||
await fetchSetupStatusWithCache()
|
await fetchSetupStatusWithCache()
|
||||||
|
|
||||||
expect(localStorage.getItem('setup_status')).toBe('finished')
|
expect(localStorage.getItem('v1:setup_status')).toBe('finished')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ describe('setup-status utilities', () => {
|
|||||||
|
|
||||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||||
expect(result).toEqual(apiResponse)
|
expect(result).toEqual(apiResponse)
|
||||||
expect(localStorage.getItem('setup_status')).toBe('finished')
|
expect(localStorage.getItem('v1:setup_status')).toBe('finished')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call API and remove cache when not finished', async () => {
|
it('should call API and remove cache when not finished', async () => {
|
||||||
@@ -56,24 +56,24 @@ describe('setup-status utilities', () => {
|
|||||||
|
|
||||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||||
expect(result).toEqual(apiResponse)
|
expect(result).toEqual(apiResponse)
|
||||||
expect(localStorage.getItem('setup_status')).toBeNull()
|
expect(localStorage.getItem('v1:setup_status')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should clear stale cache when API returns not_started', async () => {
|
it('should clear stale cache when API returns not_started', async () => {
|
||||||
localStorage.setItem('setup_status', 'some_invalid_value')
|
localStorage.setItem('v1:setup_status', 'some_invalid_value')
|
||||||
const apiResponse: SetupStatusResponse = { step: 'not_started' }
|
const apiResponse: SetupStatusResponse = { step: 'not_started' }
|
||||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||||
|
|
||||||
const result = await fetchSetupStatusWithCache()
|
const result = await fetchSetupStatusWithCache()
|
||||||
|
|
||||||
expect(result).toEqual(apiResponse)
|
expect(result).toEqual(apiResponse)
|
||||||
expect(localStorage.getItem('setup_status')).toBeNull()
|
expect(localStorage.getItem('v1:setup_status')).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('cache edge cases', () => {
|
describe('cache edge cases', () => {
|
||||||
it('should call API when cache value is empty string', async () => {
|
it('should call API when cache value is empty string', async () => {
|
||||||
localStorage.setItem('setup_status', '')
|
localStorage.setItem('v1:setup_status', '')
|
||||||
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
||||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ describe('setup-status utilities', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should call API when cache value is not "finished"', async () => {
|
it('should call API when cache value is not "finished"', async () => {
|
||||||
localStorage.setItem('setup_status', 'not_started')
|
localStorage.setItem('v1:setup_status', 'not_started')
|
||||||
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
||||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ describe('setup-status utilities', () => {
|
|||||||
|
|
||||||
await expect(fetchSetupStatusWithCache()).rejects.toThrow()
|
await expect(fetchSetupStatusWithCache()).rejects.toThrow()
|
||||||
|
|
||||||
expect(localStorage.getItem('setup_status')).toBeNull()
|
expect(localStorage.getItem('v1:setup_status')).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { SetupStatusResponse } from '@/models/common'
|
import type { SetupStatusResponse } from '@/models/common'
|
||||||
|
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||||
import { fetchSetupStatus } from '@/service/common'
|
import { fetchSetupStatus } from '@/service/common'
|
||||||
|
import { storage } from './storage'
|
||||||
const SETUP_STATUS_KEY = 'setup_status'
|
|
||||||
|
|
||||||
const isSetupStatusCached = (): boolean =>
|
const isSetupStatusCached = (): boolean =>
|
||||||
localStorage.getItem(SETUP_STATUS_KEY) === 'finished'
|
storage.get<string>(STORAGE_KEYS.CONFIG.SETUP_STATUS) === 'finished'
|
||||||
|
|
||||||
export const fetchSetupStatusWithCache = async (): Promise<SetupStatusResponse> => {
|
export const fetchSetupStatusWithCache = async (): Promise<SetupStatusResponse> => {
|
||||||
if (isSetupStatusCached())
|
if (isSetupStatusCached())
|
||||||
@@ -13,9 +13,9 @@ export const fetchSetupStatusWithCache = async (): Promise<SetupStatusResponse>
|
|||||||
const status = await fetchSetupStatus()
|
const status = await fetchSetupStatus()
|
||||||
|
|
||||||
if (status.step === 'finished')
|
if (status.step === 'finished')
|
||||||
localStorage.setItem(SETUP_STATUS_KEY, 'finished')
|
storage.set(STORAGE_KEYS.CONFIG.SETUP_STATUS, 'finished')
|
||||||
else
|
else
|
||||||
localStorage.removeItem(SETUP_STATUS_KEY)
|
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||||
|
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|||||||
190
web/utils/storage.ts
Normal file
190
web/utils/storage.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import { isClient } from './client'
|
||||||
|
|
||||||
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
|
||||||
|
|
||||||
|
const STORAGE_VERSION = 'v1'
|
||||||
|
const MIGRATION_FLAG_KEY = '__storage_migrated__'
|
||||||
|
|
||||||
|
let _isAvailable: boolean | null = null
|
||||||
|
|
||||||
|
function isLocalStorageAvailable(): boolean {
|
||||||
|
if (_isAvailable !== null)
|
||||||
|
return _isAvailable
|
||||||
|
|
||||||
|
if (!isClient) {
|
||||||
|
_isAvailable = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const testKey = '__storage_test__'
|
||||||
|
localStorage.setItem(testKey, 'test')
|
||||||
|
localStorage.removeItem(testKey)
|
||||||
|
_isAvailable = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
_isAvailable = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function versionedKey(key: string): string {
|
||||||
|
return `${STORAGE_VERSION}:${key}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRaw(key: string): string | null {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRaw(key: string, value: string): void {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Silent fail - localStorage may be full or disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRaw(key: string): void {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get<T extends JsonValue>(key: string, defaultValue?: T): T | null {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return defaultValue ?? null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = localStorage.getItem(versionedKey(key))
|
||||||
|
if (item === null)
|
||||||
|
return defaultValue ?? null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(item) as T
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return item as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return defaultValue ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function set<T extends JsonValue>(key: string, value: T): void {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
|
||||||
|
localStorage.setItem(versionedKey(key), stringValue)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Silent fail - localStorage may be full or disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(key: string): void {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(versionedKey(key))
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNumber(key: string): number | null
|
||||||
|
function getNumber(key: string, defaultValue: number): number
|
||||||
|
function getNumber(key: string, defaultValue?: number): number | null {
|
||||||
|
const value = get<string | number>(key)
|
||||||
|
if (value === null)
|
||||||
|
return defaultValue ?? null
|
||||||
|
|
||||||
|
const parsed = typeof value === 'number' ? value : Number.parseFloat(value as string)
|
||||||
|
return Number.isNaN(parsed) ? (defaultValue ?? null) : parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBoolean(key: string): boolean | null
|
||||||
|
function getBoolean(key: string, defaultValue: boolean): boolean
|
||||||
|
function getBoolean(key: string, defaultValue?: boolean): boolean | null {
|
||||||
|
const value = get<string | boolean>(key)
|
||||||
|
if (value === null)
|
||||||
|
return defaultValue ?? null
|
||||||
|
|
||||||
|
if (typeof value === 'boolean')
|
||||||
|
return value
|
||||||
|
|
||||||
|
return value === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
type MigrationEntry = { old: string, new: string }
|
||||||
|
|
||||||
|
function migrate(oldKey: string, newKey: string): boolean {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return false
|
||||||
|
|
||||||
|
const oldValue = getRaw(oldKey)
|
||||||
|
if (oldValue === null)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const newVersionedKey = versionedKey(newKey)
|
||||||
|
if (getRaw(newVersionedKey) !== null)
|
||||||
|
return false
|
||||||
|
|
||||||
|
setRaw(newVersionedKey, oldValue)
|
||||||
|
removeRaw(oldKey)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMigrations(migrations: MigrationEntry[]): void {
|
||||||
|
if (!isLocalStorageAvailable())
|
||||||
|
return
|
||||||
|
|
||||||
|
const migrationFlagValue = getRaw(MIGRATION_FLAG_KEY)
|
||||||
|
if (migrationFlagValue === STORAGE_VERSION)
|
||||||
|
return
|
||||||
|
|
||||||
|
for (const { old: oldKey, new: newKey } of migrations)
|
||||||
|
migrate(oldKey, newKey)
|
||||||
|
|
||||||
|
setRaw(MIGRATION_FLAG_KEY, STORAGE_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCache(): void {
|
||||||
|
_isAvailable = null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const storage = {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
remove,
|
||||||
|
getNumber,
|
||||||
|
getBoolean,
|
||||||
|
isAvailable: isLocalStorageAvailable,
|
||||||
|
migrate,
|
||||||
|
runMigrations,
|
||||||
|
resetCache,
|
||||||
|
}
|
||||||
@@ -134,7 +134,7 @@ const createMockLocalStorage = () => {
|
|||||||
|
|
||||||
let mockLocalStorage: ReturnType<typeof createMockLocalStorage>
|
let mockLocalStorage: ReturnType<typeof createMockLocalStorage>
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockLocalStorage = createMockLocalStorage()
|
mockLocalStorage = createMockLocalStorage()
|
||||||
Object.defineProperty(globalThis, 'localStorage', {
|
Object.defineProperty(globalThis, 'localStorage', {
|
||||||
@@ -142,4 +142,7 @@ beforeEach(() => {
|
|||||||
writable: true,
|
writable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
})
|
})
|
||||||
|
// Reset storage module cache to ensure fresh state for each test
|
||||||
|
const { storage } = await import('./utils/storage')
|
||||||
|
storage.resetCache()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user