mirror of
https://github.com/langgenius/dify.git
synced 2026-02-04 15:04:16 +00:00
Compare commits
8 Commits
1.12.1
...
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 Loading from '@/app/components/base/loading'
|
||||
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 useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import s from './style.module.css'
|
||||
|
||||
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
||||
@@ -108,7 +110,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
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'
|
||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||
// 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 MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import { isTriggerNode } from '@/app/components/workflow/types'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
updateAppSiteAccessToken,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export type ICardViewProps = {
|
||||
appId: string
|
||||
@@ -126,7 +127,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
}) as Promise<App>,
|
||||
)
|
||||
if (!err)
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||
|
||||
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 Loading from '@/app/components/base/loading'
|
||||
import ExtraInfo from '@/app/components/datasets/extra-info'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
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 { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
@@ -40,7 +42,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const pathname = usePathname()
|
||||
const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-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 { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
@@ -110,7 +112,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand)
|
||||
|
||||
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'
|
||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||
}, [isMobile, setAppSidebarExpand])
|
||||
|
||||
@@ -8,12 +8,14 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
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 { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t } = useTranslation()
|
||||
@@ -41,7 +43,7 @@ export default function CheckCode() {
|
||||
setIsLoading(true)
|
||||
const res = await sendResetPasswordCode(email, locale)
|
||||
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)
|
||||
params.set('token', encodeURIComponent(res.data))
|
||||
params.set('email', encodeURIComponent(email))
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
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 { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendWebAppEMailLoginCode } from '@/service/common'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export default function MailAndCodeAuth() {
|
||||
const { t } = useTranslation()
|
||||
@@ -36,7 +38,7 @@ export default function MailAndCodeAuth() {
|
||||
setIsLoading(true)
|
||||
const ret = await sendWebAppEMailLoginCode(email, locale)
|
||||
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)
|
||||
params.set('email', encodeURIComponent(email))
|
||||
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 Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import {
|
||||
checkEmailExisted,
|
||||
resetEmail,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
} from '@/service/common'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
@@ -172,7 +174,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
|
||||
localStorage.removeItem('setup_status')
|
||||
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
|
||||
router.push('/signin')
|
||||
|
||||
@@ -10,9 +10,11 @@ import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
@@ -28,7 +30,7 @@ export default function AppSelector() {
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
|
||||
localStorage.removeItem('setup_status')
|
||||
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||
resetUser()
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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 FeedBack from './components/feed-back'
|
||||
import VerifyEmail from './components/verify-email'
|
||||
@@ -21,7 +23,7 @@ export default function DeleteAccount(props: DeleteAccountProps) {
|
||||
const handleEmailCheckSuccess = useCallback(async () => {
|
||||
try {
|
||||
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) }
|
||||
}, [])
|
||||
|
||||
@@ -17,11 +17,12 @@ import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
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 { useIsLogin } from '@/service/use-common'
|
||||
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
|
||||
import { storage } from '@/utils/storage'
|
||||
import {
|
||||
OAUTH_AUTHORIZE_PENDING_KEY,
|
||||
OAUTH_AUTHORIZE_PENDING_TTL,
|
||||
REDIRECT_URL_KEY,
|
||||
} from './constants'
|
||||
@@ -31,7 +32,7 @@ function setItemWithExpiry(key: string, value: string, ttl: number) {
|
||||
value,
|
||||
expiry: dayjs().add(ttl, 'seconds').unix(),
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(item))
|
||||
storage.set(key, JSON.stringify(item))
|
||||
}
|
||||
|
||||
function buildReturnUrl(pathname: string, search: string) {
|
||||
@@ -86,7 +87,7 @@ export default function OAuthAuthorize() {
|
||||
const onLoginSwitchClick = () => {
|
||||
try {
|
||||
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)}`)
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -7,13 +7,16 @@ import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
import { LEGACY_KEY_MIGRATIONS, STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { sendGAEvent } from '@/utils/gtag'
|
||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
||||
import { trackEvent } from './base/amplitude'
|
||||
|
||||
storage.runMigrations(LEGACY_KEY_MIGRATIONS)
|
||||
|
||||
type AppInitializerProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
@@ -75,7 +78,7 @@ export const AppInitializer = ({
|
||||
}
|
||||
|
||||
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
storage.set(STORAGE_KEYS.EDUCATION.VERIFYING, 'yes')
|
||||
|
||||
try {
|
||||
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 ContentDialog from '@/app/components/base/content-dialog'
|
||||
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 { useProviderContext } from '@/context/provider-context'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
@@ -31,6 +31,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import AppIcon from '../base/app-icon'
|
||||
import AppOperations from './app-operations'
|
||||
|
||||
@@ -128,7 +129,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
type: 'success',
|
||||
message: t('newApp.appCreated', { ns: 'app' }),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||
onPlanInfoChanged()
|
||||
getRedirection(true, newApp, replace)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import Divider from '../base/divider'
|
||||
import { getKeyboardKeyCodeBySystem } from '../workflow/utils'
|
||||
import AppInfo from './app-info'
|
||||
@@ -53,7 +55,7 @@ const AppDetailNav = ({
|
||||
const pathname = usePathname()
|
||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||
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 { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
@@ -64,7 +66,7 @@ const AppDetailNav = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (appSidebarExpand) {
|
||||
localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand)
|
||||
storage.set(STORAGE_KEYS.APP.DETAIL_COLLAPSE, appSidebarExpand)
|
||||
setAppSidebarExpand(appSidebarExpand)
|
||||
}
|
||||
}, [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 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 { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||
import { storage } from '@/utils/storage'
|
||||
import IdeaOutput from './idea-output'
|
||||
import InstructionEditorInBasic from './instruction-editor'
|
||||
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
|
||||
@@ -83,9 +85,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
onFinished,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model
|
||||
: null
|
||||
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||
const [model, setModel] = React.useState<Model>(localModel || {
|
||||
name: '',
|
||||
provider: '',
|
||||
@@ -178,9 +178,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModel) {
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
|
||||
: null
|
||||
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||
if (localModel) {
|
||||
setModel(localModel)
|
||||
}
|
||||
@@ -209,7 +207,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
mode: newValue.mode as ModelModeType,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||
}, [model, setModel])
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||
@@ -218,7 +216,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
completion_params: newParams as CompletionParams,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||
}, [model, setModel])
|
||||
|
||||
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 { 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 { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { generateRule } from '@/service/debug'
|
||||
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
|
||||
import IdeaOutput from '../automatic/idea-output'
|
||||
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
|
||||
@@ -62,9 +64,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
}
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model
|
||||
: null
|
||||
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||
const [model, setModel] = React.useState<Model>(localModel || {
|
||||
name: '',
|
||||
provider: '',
|
||||
@@ -115,7 +115,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
mode: newValue.mode as ModelModeType,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||
}, [model, setModel])
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||
@@ -124,7 +124,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
completion_params: newParams as CompletionParams,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||
}, [model, setModel])
|
||||
|
||||
const onGenerate = async () => {
|
||||
@@ -168,9 +168,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModel) {
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
|
||||
: null
|
||||
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||
if (localModel) {
|
||||
setModel({
|
||||
...localModel,
|
||||
|
||||
@@ -14,27 +14,20 @@ import {
|
||||
} from 'react'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
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 { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import {
|
||||
AgentStrategy,
|
||||
} from '@/types/app'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { ORCHESTRATE_CHANGED } from './types'
|
||||
|
||||
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>({})
|
||||
|
||||
if (localeDebugWithSingleOrMultipleModelConfigs) {
|
||||
try {
|
||||
debugWithSingleOrMultipleModelConfigs.current = JSON.parse(localeDebugWithSingleOrMultipleModelConfigs) || {}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
const debugWithSingleOrMultipleModelConfigs = useRef<DebugWithSingleOrMultipleModelConfigs>(localeDebugWithSingleOrMultipleModelConfigs || {})
|
||||
|
||||
const [
|
||||
debugWithMultipleModel,
|
||||
@@ -55,7 +48,7 @@ export const useDebugWithSingleOrMultipleModel = (appId: string) => {
|
||||
configs: modelConfigs,
|
||||
}
|
||||
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)
|
||||
setMultipleModelConfigs(value.configs)
|
||||
}, [appId])
|
||||
|
||||
@@ -16,7 +16,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
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 { DSLImportMode } from '@/models/app'
|
||||
import { importDSL } from '@/service/apps'
|
||||
@@ -25,6 +25,7 @@ import { useExploreAppList } from '@/service/use-explore'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import AppCard from '../app-card'
|
||||
import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar'
|
||||
|
||||
@@ -145,7 +146,7 @@ const Apps = ({
|
||||
onSuccess()
|
||||
if (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)
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { createApp } from '@/service/apps'
|
||||
@@ -12,6 +11,8 @@ import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import CreateAppModal from './index'
|
||||
|
||||
const NEED_REFRESH_APP_LIST_KEY_PREFIXED = 'v1:needRefreshAppList'
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounceFn: (fn: (...args: any[]) => any) => {
|
||||
const run = (...args: any[]) => fn(...args)
|
||||
@@ -142,7 +143,7 @@ describe('CreateAppModal', () => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' })
|
||||
expect(onSuccess).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))
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
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 { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
@@ -27,6 +27,7 @@ import { createApp } from '@/service/apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { basePath } from '@/utils/var'
|
||||
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' }) })
|
||||
onSuccess()
|
||||
onClose()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||
}
|
||||
catch (e: any) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
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 { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from '@/service/apps'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import Uploader from './uploader'
|
||||
|
||||
type CreateFromDSLModalProps = {
|
||||
@@ -130,7 +131,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { 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)
|
||||
await handleCheckPluginDependencies(app_id)
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||
@@ -190,7 +191,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
})
|
||||
if (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)
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
|
||||
@@ -5,10 +5,11 @@ import * as React from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import SwitchAppModal from './index'
|
||||
|
||||
const NEED_REFRESH_APP_LIST_KEY_PREFIXED = 'v1:needRefreshAppList'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
const mockReplace = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
@@ -257,7 +258,7 @@ describe('SwitchAppModal', () => {
|
||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
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(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -17,13 +17,14 @@ import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
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 { useProviderContext } from '@/context/provider-context'
|
||||
import { deleteApp, switchApp } from '@/service/apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import AppIconPicker from '../../base/app-icon-picker'
|
||||
|
||||
type SwitchAppModalProps = {
|
||||
@@ -73,7 +74,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
setAppDetail()
|
||||
if (removeOriginal)
|
||||
await deleteApp(appDetail.id)
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||
getRedirection(
|
||||
isCurrentWorkspaceEditor,
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ import CustomPopover from '@/app/components/base/popover'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import Toast, { ToastContext } from '@/app/components/base/toast'
|
||||
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 { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@@ -33,6 +33,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { formatTime } from '@/utils/time'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
@@ -144,7 +145,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
type: 'success',
|
||||
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)
|
||||
onRefresh()
|
||||
onPlanInfoChanged()
|
||||
|
||||
@@ -434,13 +434,15 @@ describe('List', () => {
|
||||
})
|
||||
|
||||
describe('Local Storage Refresh', () => {
|
||||
it('should call refetch when refresh key is set in localStorage', () => {
|
||||
localStorage.setItem('needRefreshAppList', '1')
|
||||
it('should call refetch when refresh key is set in localStorage', async () => {
|
||||
localStorage.setItem('v1:needRefreshAppList', '1')
|
||||
|
||||
render(<List />)
|
||||
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
expect(localStorage.getItem('needRefreshAppList')).toBeNull()
|
||||
await vi.waitFor(() => {
|
||||
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 { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
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 { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import AppCard from './app-card'
|
||||
import { AppCardSkeleton } from './app-card-skeleton'
|
||||
import Empty from './empty'
|
||||
@@ -134,8 +135,8 @@ const List: FC<Props> = ({
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
||||
if (storage.get<string>(STORAGE_KEYS.APP.NEED_REFRESH_LIST) === '1') {
|
||||
storage.remove(STORAGE_KEYS.APP.NEED_REFRESH_LIST)
|
||||
refetch()
|
||||
}
|
||||
}, [refetch])
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
generationConversationName,
|
||||
} from '@/service/share'
|
||||
import { shareQueryKeys } from '@/service/use-share'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { useChatWithHistory } from './hooks'
|
||||
|
||||
const CONVERSATION_ID_INFO_KEY = 'v1:conversationIdInfo'
|
||||
|
||||
vi.mock('@/hooks/use-app-favicon', () => ({
|
||||
useAppFavicon: vi.fn(),
|
||||
}))
|
||||
@@ -120,14 +121,14 @@ const setConversationIdInfo = (appId: string, conversationId: string) => {
|
||||
'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.
|
||||
describe('useChatWithHistory', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||
mockStoreState.appInfo = {
|
||||
app_id: 'app-1',
|
||||
custom_config: null,
|
||||
@@ -144,7 +145,7 @@ describe('useChatWithHistory', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||
})
|
||||
|
||||
// Scenario: share query results populate conversation lists and trigger chat list fetch.
|
||||
@@ -268,7 +269,7 @@ describe('useChatWithHistory', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
|
||||
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO_KEY)
|
||||
const parsed = storedValue ? JSON.parse(storedValue) : {}
|
||||
const storedUserId = parsed['app-1']?.['user-1']
|
||||
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 { useToastContext } from '@/app/components/base/toast'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
useShareConversations,
|
||||
} from '@/service/use-share'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInputsFromUrlParams, getRawUserVariablesFromUrlParams } from '../utils'
|
||||
@@ -128,27 +130,15 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const localState = localStorage.getItem('webappSidebarCollapse')
|
||||
return localState === 'collapsed'
|
||||
}
|
||||
catch {
|
||||
// localStorage may be disabled in private browsing mode or by security settings
|
||||
// fallback to default value
|
||||
return false
|
||||
}
|
||||
const localState = storage.get<string>(STORAGE_KEYS.APP.SIDEBAR_COLLAPSE)
|
||||
return localState === 'collapsed'
|
||||
}
|
||||
return false
|
||||
})
|
||||
const handleSidebarCollapse = useCallback((state: boolean) => {
|
||||
if (appId) {
|
||||
setSidebarCollapseState(state)
|
||||
try {
|
||||
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
|
||||
}
|
||||
catch {
|
||||
// localStorage may be disabled, continue without persisting state
|
||||
}
|
||||
storage.set(STORAGE_KEYS.APP.SIDEBAR_COLLAPSE, state ? 'collapsed' : 'expanded')
|
||||
}
|
||||
}, [appId, setSidebarCollapseState])
|
||||
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'
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
generationConversationName,
|
||||
} from '@/service/share'
|
||||
import { shareQueryKeys } from '@/service/use-share'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { useEmbeddedChatbot } from './hooks'
|
||||
|
||||
const CONVERSATION_ID_INFO_KEY = 'v1:conversationIdInfo'
|
||||
|
||||
vi.mock('@/i18n-config/client', () => ({
|
||||
changeLanguage: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
@@ -113,7 +114,7 @@ const createConversationData = (overrides: Partial<AppConversationData> = {}): A
|
||||
describe('useEmbeddedChatbot', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||
mockStoreState.appInfo = {
|
||||
app_id: 'app-1',
|
||||
custom_config: null,
|
||||
@@ -131,7 +132,7 @@ describe('useEmbeddedChatbot', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO)
|
||||
localStorage.removeItem(CONVERSATION_ID_INFO_KEY)
|
||||
})
|
||||
|
||||
// Scenario: share query results populate conversation lists and trigger chat list fetch.
|
||||
@@ -251,7 +252,7 @@ describe('useEmbeddedChatbot', () => {
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
|
||||
const storedValue = localStorage.getItem(CONVERSATION_ID_INFO_KEY)
|
||||
const parsed = storedValue ? JSON.parse(storedValue) : {}
|
||||
const storedUserId = parsed['app-1']?.['embedded-user-1']
|
||||
const storedDefaultId = parsed['app-1']?.DEFAULT
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('PlanComp', () => {
|
||||
|
||||
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
||||
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 () => {
|
||||
|
||||
@@ -14,12 +14,13 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
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 { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useEducationVerify } from '@/service/use-education'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||
import { Loading } from '../../base/icons/src/public/thought'
|
||||
import { NUM_INFINITE } from '../config'
|
||||
@@ -72,7 +73,7 @@ const PlanComp: FC<Props> = ({
|
||||
if (isPending)
|
||||
return
|
||||
mutateAsync().then((res) => {
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
storage.remove(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||
if (unmountedRef.current)
|
||||
return
|
||||
router.push(`/education-apply?token=${res.token}`)
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useBoolean } from 'ahooks'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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 { isShowManageMetadataLocalStorageKey } from '../types'
|
||||
import { storage } from '@/utils/storage'
|
||||
import useCheckMetadataName from './use-check-metadata-name'
|
||||
|
||||
const useEditDatasetMetadata = ({
|
||||
@@ -24,10 +25,10 @@ const useEditDatasetMetadata = ({
|
||||
}] = useBoolean(false)
|
||||
|
||||
useEffect(() => {
|
||||
const isShowManageMetadata = localStorage.getItem(isShowManageMetadataLocalStorageKey)
|
||||
const isShowManageMetadata = storage.get<string>(STORAGE_KEYS.UI.SHOW_MANAGE_METADATA)
|
||||
if (isShowManageMetadata) {
|
||||
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 Divider from '@/app/components/base/divider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import AddMetadataButton from '../add-metadata-button'
|
||||
import InputCombined from '../edit-metadata-batch/input-combined'
|
||||
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
||||
import { DataType, isShowManageMetadataLocalStorageKey } from '../types'
|
||||
import { DataType } from '../types'
|
||||
import Field from './field'
|
||||
|
||||
type Props = {
|
||||
@@ -53,7 +55,7 @@ const InfoGroup: FC<Props> = ({
|
||||
const { formatTime: formatTimestamp } = useTimestamp()
|
||||
|
||||
const handleMangeMetadata = () => {
|
||||
localStorage.setItem(isShowManageMetadataLocalStorageKey, 'true')
|
||||
storage.set(STORAGE_KEYS.UI.SHOW_MANAGE_METADATA, 'true')
|
||||
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 { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
@@ -30,6 +31,7 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import AccountAbout from '../account-about'
|
||||
import GithubStar from '../github-star'
|
||||
import Indicator from '../indicator'
|
||||
@@ -55,13 +57,13 @@ export default function AppSelector() {
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
resetUser()
|
||||
localStorage.removeItem('setup_status')
|
||||
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
|
||||
// To avoid use other account's education notice info
|
||||
localStorage.removeItem('education-reverify-prev-expire-at')
|
||||
localStorage.removeItem('education-reverify-has-noticed')
|
||||
localStorage.removeItem('education-expired-has-noticed')
|
||||
storage.remove(STORAGE_KEYS.EDUCATION.REVERIFY_PREV_EXPIRE_AT)
|
||||
storage.remove(STORAGE_KEYS.EDUCATION.REVERIFY_HAS_NOTICED)
|
||||
storage.remove(STORAGE_KEYS.EDUCATION.EXPIRED_HAS_NOTICED)
|
||||
|
||||
router.push('/signin')
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
import { usePathname } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import s from './index.module.css'
|
||||
|
||||
type HeaderWrapperProps = {
|
||||
@@ -18,7 +20,7 @@ const HeaderWrapper = ({
|
||||
// Check if the current path is a workflow canvas & fullscreen
|
||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
|
||||
const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)
|
||||
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useState } from 'react'
|
||||
import { X } from '@/app/components/base/icons/src/vender/line/general'
|
||||
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 { storage } from '@/utils/storage'
|
||||
|
||||
const MaintenanceNotice = () => {
|
||||
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 = () => {
|
||||
window.open(NOTICE_I18N.href, '_blank')
|
||||
}
|
||||
|
||||
const handleCloseNotice = () => {
|
||||
localStorage.setItem('hide-maintenance-notice', '1')
|
||||
storage.set(STORAGE_KEYS.UI.HIDE_MAINTENANCE_NOTICE, '1')
|
||||
setShowNotice(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { useCountDown } from 'ahooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
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_KEY = 'leftTime'
|
||||
@@ -12,23 +14,23 @@ type CountdownProps = {
|
||||
|
||||
export default function Countdown({ onResend }: CountdownProps) {
|
||||
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({
|
||||
leftTime,
|
||||
onEnd: () => {
|
||||
setLeftTime(0)
|
||||
localStorage.removeItem(COUNT_DOWN_KEY)
|
||||
storage.remove(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME)
|
||||
},
|
||||
})
|
||||
|
||||
const resend = async function () {
|
||||
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?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(COUNT_DOWN_KEY, `${time}`)
|
||||
storage.set(STORAGE_KEYS.UI.COUNTDOWN_LEFT_TIME, time)
|
||||
}, [time])
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,9 +12,11 @@ import Loading from '@/app/components/base/loading'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
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 { isServer } from '@/utils/client'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
@@ -34,8 +36,6 @@ type FeaturedToolsProps = {
|
||||
onInstallSuccess?: () => void
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'workflow_tools_featured_collapsed'
|
||||
|
||||
const FeaturedTools = ({
|
||||
plugins,
|
||||
providerMap,
|
||||
@@ -50,14 +50,14 @@ const FeaturedTools = ({
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (isServer)
|
||||
return false
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TOOLS_FEATURED_COLLAPSED)
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TOOLS_FEATURED_COLLAPSED)
|
||||
if (stored !== null)
|
||||
setIsCollapsed(stored === 'true')
|
||||
}, [])
|
||||
@@ -65,7 +65,7 @@ const FeaturedTools = ({
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
storage.set(STORAGE_KEYS.WORKFLOW.TOOLS_FEATURED_COLLAPSED, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -11,9 +11,11 @@ import Loading from '@/app/components/base/loading'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
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 { isServer } from '@/utils/client'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
@@ -30,8 +32,6 @@ type FeaturedTriggersProps = {
|
||||
onInstallSuccess?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'workflow_triggers_featured_collapsed'
|
||||
|
||||
const FeaturedTriggers = ({
|
||||
plugins,
|
||||
providerMap,
|
||||
@@ -45,14 +45,14 @@ const FeaturedTriggers = ({
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (isServer)
|
||||
return false
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TRIGGERS_FEATURED_COLLAPSED)
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.TRIGGERS_FEATURED_COLLAPSED)
|
||||
if (stored !== null)
|
||||
setIsCollapsed(stored === 'true')
|
||||
}, [])
|
||||
@@ -60,7 +60,7 @@ const FeaturedTriggers = ({
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
storage.set(STORAGE_KEYS.WORKFLOW.TRIGGERS_FEATURED_COLLAPSED, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,8 +10,10 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useRAGRecommendedPlugins } from '@/service/use-tools'
|
||||
import { isServer } from '@/utils/client'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import List from './list'
|
||||
|
||||
@@ -21,8 +23,6 @@ type RAGToolRecommendationsProps = {
|
||||
onTagsChange: Dispatch<SetStateAction<string[]>>
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'workflow_rag_recommendations_collapsed'
|
||||
|
||||
const RAGToolRecommendations = ({
|
||||
viewType,
|
||||
onSelect,
|
||||
@@ -32,14 +32,14 @@ const RAGToolRecommendations = ({
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (isServer)
|
||||
return false
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.RAG_RECOMMENDATIONS_COLLAPSED)
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
const stored = storage.get<string>(STORAGE_KEYS.WORKFLOW.RAG_RECOMMENDATIONS_COLLAPSED)
|
||||
if (stored !== null)
|
||||
setIsCollapsed(stored === 'true')
|
||||
}, [])
|
||||
@@ -47,7 +47,7 @@ const RAGToolRecommendations = ({
|
||||
useEffect(() => {
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
storage.set(STORAGE_KEYS.WORKFLOW.RAG_RECOMMENDATIONS_COLLAPSED, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
|
||||
const {
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { storage } from '@/utils/storage'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
NODE_LAYOUT_HORIZONTAL_PADDING,
|
||||
@@ -342,7 +344,7 @@ export const useWorkflowCanvasMaximize = () => {
|
||||
return
|
||||
|
||||
setMaximizeCanvas(!maximizeCanvas)
|
||||
localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas))
|
||||
storage.set(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, !maximizeCanvas)
|
||||
eventEmitter?.emit({
|
||||
type: 'workflow-canvas-maximize',
|
||||
payload: !maximizeCanvas,
|
||||
|
||||
@@ -26,12 +26,12 @@ const Wrap = ({
|
||||
isExpand,
|
||||
children,
|
||||
}: Props) => {
|
||||
const panelWidth = useStore(state => state.panelWidth)
|
||||
const nodePanelWidth = useStore(state => state.nodePanelWidth)
|
||||
const wrapStyle = (() => {
|
||||
if (isExpand) {
|
||||
return {
|
||||
...style,
|
||||
width: panelWidth - 1,
|
||||
width: nodePanelWidth - 1,
|
||||
}
|
||||
}
|
||||
return style
|
||||
|
||||
@@ -26,7 +26,7 @@ const createPanelWidthManager = (storageKey: string) => {
|
||||
|
||||
describe('Workflow Panel Width Persistence', () => {
|
||||
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', () => {
|
||||
const manager = createPanelWidthManager(storageKey)
|
||||
@@ -74,7 +74,7 @@ describe('Workflow Panel Width Persistence', () => {
|
||||
|
||||
describe('Bug Scenario Reproduction', () => {
|
||||
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
|
||||
const buggyUpdate = (width: number) => {
|
||||
@@ -89,7 +89,7 @@ describe('Workflow Panel Width Persistence', () => {
|
||||
})
|
||||
|
||||
it('should verify fix prevents localStorage pollution', () => {
|
||||
const storageKey = 'workflow-node-panel-width'
|
||||
const storageKey = 'v1:workflow-node-panel-width'
|
||||
const manager = createPanelWidthManager(storageKey)
|
||||
|
||||
localStorage.setItem(storageKey, '500') // User preference
|
||||
@@ -101,7 +101,7 @@ describe('Workflow Panel Width Persistence', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
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
|
||||
manager.updateWidth(300, 'system')
|
||||
@@ -112,12 +112,12 @@ describe('Workflow Panel Width Persistence', () => {
|
||||
manager.updateWidth(550, 'user')
|
||||
|
||||
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', () => {
|
||||
localStorage.setItem('workflow-node-panel-width', '150') // Below minimum
|
||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||
localStorage.setItem('v1:workflow-node-panel-width', '150') // Below minimum
|
||||
const manager = createPanelWidthManager('v1:workflow-node-panel-width')
|
||||
|
||||
const storedWidth = manager.getStoredWidth()
|
||||
expect(storedWidth).toBe(150) // Returns raw value
|
||||
@@ -125,13 +125,13 @@ describe('Workflow Panel Width Persistence', () => {
|
||||
// User can correct the preference
|
||||
const correctedWidth = manager.updateWidth(500, 'user')
|
||||
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', () => {
|
||||
it('should enforce source parameter type', () => {
|
||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||
const manager = createPanelWidthManager('v1:workflow-node-panel-width')
|
||||
|
||||
// Valid source values
|
||||
manager.updateWidth(500, 'user')
|
||||
|
||||
@@ -59,12 +59,14 @@ import {
|
||||
hasRetryNode,
|
||||
isSupportCustomRunForm,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { useResizePanel } from '../../hooks/use-resize-panel'
|
||||
import BeforeRunForm from '../before-run-form'
|
||||
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))
|
||||
|
||||
if (source === 'user')
|
||||
localStorage.setItem('workflow-node-panel-width', `${newValue}`)
|
||||
storage.set(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, newValue)
|
||||
|
||||
setNodePanelWidth(newValue)
|
||||
}, [maxNodePanelWidth, setNodePanelWidth])
|
||||
|
||||
@@ -12,10 +12,12 @@ import {
|
||||
import Toast from '@/app/components/base/toast'
|
||||
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 { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useGenerateStructuredOutputRules } from '@/service/use-common'
|
||||
import { ModelModeType, Theme } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { useMittContext } from '../visual-editor/context'
|
||||
import { useVisualEditorStore } from '../visual-editor/store'
|
||||
import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets'
|
||||
@@ -36,9 +38,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
onApply,
|
||||
crossAxisOffset,
|
||||
}) => {
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model
|
||||
: null
|
||||
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [view, setView] = useState(GeneratorView.promptEditor)
|
||||
const [model, setModel] = useState<Model>(localModel || {
|
||||
@@ -60,9 +60,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModel) {
|
||||
const localModel = localStorage.getItem('auto-gen-model')
|
||||
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
|
||||
: null
|
||||
const localModel = storage.get<Model>(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL)
|
||||
if (localModel) {
|
||||
setModel(localModel)
|
||||
}
|
||||
@@ -95,7 +93,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
mode: newValue.mode as ModelModeType,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||
}, [model, setModel])
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||
@@ -104,7 +102,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
completion_params: newParams as CompletionParams,
|
||||
}
|
||||
setModel(newModel)
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
storage.set(STORAGE_KEYS.CONFIG.AUTO_GEN_MODEL, newModel)
|
||||
}, [model, setModel])
|
||||
|
||||
const { mutateAsync: generateStructuredOutputRules, isPending: isGenerating } = useGenerateStructuredOutputRules()
|
||||
|
||||
@@ -27,7 +27,7 @@ const createMockLocalStorage = () => {
|
||||
|
||||
// Preview panel width logic
|
||||
const createPreviewPanelManager = () => {
|
||||
const storageKey = 'debug-and-preview-panel-width'
|
||||
const storageKey = 'v1:debug-and-preview-panel-width'
|
||||
|
||||
return {
|
||||
updateWidth: (width: number, source: PanelWidthSource = 'user') => {
|
||||
@@ -63,7 +63,7 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
||||
const result = manager.updateWidth(450, 'user')
|
||||
|
||||
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', () => {
|
||||
@@ -80,17 +80,17 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
||||
|
||||
// Both user and system operations should behave consistently
|
||||
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')
|
||||
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', () => {
|
||||
it('should maintain independence from Node Panel', () => {
|
||||
localStorage.setItem('workflow-node-panel-width', '600')
|
||||
localStorage.setItem('debug-and-preview-panel-width', '450')
|
||||
localStorage.setItem('v1:workflow-node-panel-width', '600')
|
||||
localStorage.setItem('v1:debug-and-preview-panel-width', '450')
|
||||
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
@@ -98,8 +98,8 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
||||
manager.updateWidth(200, 'system')
|
||||
|
||||
// Only preview panel storage key should be unaffected
|
||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('450')
|
||||
expect(localStorage.getItem('workflow-node-panel-width')).toBe('600')
|
||||
expect(localStorage.getItem('v1:debug-and-preview-panel-width')).toBe('450')
|
||||
expect(localStorage.getItem('v1:workflow-node-panel-width')).toBe('600')
|
||||
})
|
||||
|
||||
it('should handle F12 scenario consistently', () => {
|
||||
@@ -107,13 +107,13 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
||||
|
||||
// User sets preference
|
||||
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
|
||||
manager.updateWidth(180, 'system')
|
||||
|
||||
// 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
|
||||
const result = manager.updateWidth(300, 'user')
|
||||
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', () => {
|
||||
@@ -132,7 +132,7 @@ describe('Debug and Preview Panel Width Persistence', () => {
|
||||
|
||||
// Default to 'user' when source not specified
|
||||
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
|
||||
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 { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import {
|
||||
useWorkflowInteractions,
|
||||
} from '../../hooks'
|
||||
@@ -56,7 +58,7 @@ const DebugAndPreview = () => {
|
||||
const setPanelWidth = useStore(s => s.setPreviewPanelWidth)
|
||||
const handleResize = useCallback((width: number, source: 'user' | 'system' = 'user') => {
|
||||
if (source === 'user')
|
||||
localStorage.setItem('debug-and-preview-panel-width', `${width}`)
|
||||
storage.set(STORAGE_KEYS.WORKFLOW.PREVIEW_PANEL_WIDTH, width)
|
||||
setPanelWidth(width)
|
||||
}, [setPanelWidth])
|
||||
const maxPanelWidth = useMemo(() => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export type LayoutSliceShape = {
|
||||
workflowCanvasWidth?: number
|
||||
workflowCanvasHeight?: number
|
||||
setWorkflowCanvasWidth: (width: number) => void
|
||||
setWorkflowCanvasHeight: (height: number) => void
|
||||
// rightPanelWidth - otherPanelWidth = nodePanelWidth
|
||||
rightPanelWidth?: number
|
||||
setRightPanelWidth: (width: number) => void
|
||||
nodePanelWidth: number
|
||||
@@ -14,11 +15,11 @@ export type LayoutSliceShape = {
|
||||
setPreviewPanelWidth: (width: number) => void
|
||||
otherPanelWidth: number
|
||||
setOtherPanelWidth: (width: number) => void
|
||||
bottomPanelWidth: number // min-width = 400px; default-width = auto || 480px;
|
||||
bottomPanelWidth: number
|
||||
setBottomPanelWidth: (width: number) => void
|
||||
bottomPanelHeight: number
|
||||
setBottomPanelHeight: (height: number) => void
|
||||
variableInspectPanelHeight: number // min-height = 120px; default-height = 320px;
|
||||
variableInspectPanelHeight: number
|
||||
setVariableInspectPanelHeight: (height: number) => void
|
||||
maximizeCanvas: boolean
|
||||
setMaximizeCanvas: (maximize: boolean) => void
|
||||
@@ -31,9 +32,9 @@ export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({
|
||||
setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })),
|
||||
rightPanelWidth: undefined,
|
||||
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 })),
|
||||
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 })),
|
||||
otherPanelWidth: 400,
|
||||
setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })),
|
||||
@@ -41,8 +42,8 @@ export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({
|
||||
setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })),
|
||||
bottomPanelHeight: 324,
|
||||
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 })),
|
||||
maximizeCanvas: localStorage.getItem('workflow-canvas-maximize') === 'true',
|
||||
maximizeCanvas: storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false),
|
||||
setMaximizeCanvas: maximize => set(() => ({ maximizeCanvas: maximize })),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
|
||||
export type PanelSliceShape = {
|
||||
panelWidth: number
|
||||
showFeaturesPanel: boolean
|
||||
setShowFeaturesPanel: (showFeaturesPanel: boolean) => void
|
||||
showWorkflowVersionHistoryPanel: boolean
|
||||
@@ -27,7 +26,6 @@ export type PanelSliceShape = {
|
||||
}
|
||||
|
||||
export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({
|
||||
panelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420,
|
||||
showFeaturesPanel: false,
|
||||
setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })),
|
||||
showWorkflowVersionHistoryPanel: false,
|
||||
|
||||
@@ -5,6 +5,8 @@ import type {
|
||||
WorkflowRunningData,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { FileUploadConfigResponse } from '@/models/common'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
type PreviewRunningData = WorkflowRunningData & {
|
||||
resultTabActive?: boolean
|
||||
@@ -63,10 +65,10 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
|
||||
setSelection: selection => set(() => ({ selection })),
|
||||
bundleNodeSize: null,
|
||||
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) => {
|
||||
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 },
|
||||
setMousePosition: mousePosition => set(() => ({ mousePosition })),
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
|
||||
import { useStore } from '../store'
|
||||
import Panel from './panel'
|
||||
@@ -21,8 +23,8 @@ const VariableInspectPanel: FC = () => {
|
||||
return workflowCanvasHeight - 60
|
||||
}, [workflowCanvasHeight])
|
||||
|
||||
const handleResize = useCallback((width: number, height: number) => {
|
||||
localStorage.setItem('workflow-variable-inpsect-panel-height', `${height}`)
|
||||
const handleResize = useCallback((_width: number, height: number) => {
|
||||
storage.set(STORAGE_KEYS.WORKFLOW.VARIABLE_INSPECT_PANEL_HEIGHT, height)
|
||||
setVariableInspectPanelHeight(height)
|
||||
}, [setVariableInspectPanelHeight])
|
||||
|
||||
|
||||
@@ -13,13 +13,14 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
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 { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
useEducationAdd,
|
||||
useInvalidateEducationStatus,
|
||||
} from '@/service/use-education'
|
||||
import { storage } from '@/utils/storage'
|
||||
import DifyLogo from '../components/base/logo/dify-logo'
|
||||
import RoleSelector from './role-selector'
|
||||
import SearchInput from './search-input'
|
||||
@@ -47,7 +48,7 @@ const EducationApplyAge = () => {
|
||||
setShowModal(undefined)
|
||||
onPlanInfoChanged()
|
||||
updateEducationStatus()
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
storage.remove(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||
router.replace('/')
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,15 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
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 { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education'
|
||||
import { storage } from '@/utils/storage'
|
||||
import {
|
||||
EDUCATION_RE_VERIFY_ACTION,
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from './constants'
|
||||
|
||||
dayjs.extend(utc)
|
||||
@@ -133,7 +134,7 @@ const useEducationReverifyNotice = ({
|
||||
export const useEducationInit = () => {
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
const setShowEducationExpireNoticeModal = useModalContextSelector(s => s.setShowEducationExpireNoticeModal)
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
const educationVerifying = storage.get<string>(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||
const searchParams = useSearchParams()
|
||||
const educationVerifyAction = searchParams.get('action')
|
||||
|
||||
@@ -156,7 +157,7 @@ export const useEducationInit = () => {
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
|
||||
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)
|
||||
handleVerify()
|
||||
|
||||
@@ -3,8 +3,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Triangle } from '@/app/components/base/icons/src/public/education'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
const UserInfo = () => {
|
||||
const router = useRouter()
|
||||
@@ -15,7 +17,7 @@ const UserInfo = () => {
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
|
||||
localStorage.removeItem('setup_status')
|
||||
storage.remove(STORAGE_KEYS.CONFIG.SETUP_STATUS)
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
|
||||
router.push('/signin')
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('InstallForm', () => {
|
||||
render(<InstallForm />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('setup_status', 'finished')
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('v1:setup_status', 'finished')
|
||||
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 Input from '@/app/components/base/input'
|
||||
import { validPassword } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
|
||||
import { LICENSE_LINK } from '@/constants/link'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { encryptPassword as encodePassword } from '@/utils/encryption'
|
||||
import { storage } from '@/utils/storage'
|
||||
import Loading from '../components/base/loading'
|
||||
|
||||
const accountFormSchema = z.object({
|
||||
@@ -85,7 +87,7 @@ const InstallForm = () => {
|
||||
useEffect(() => {
|
||||
fetchSetupStatus().then((res: SetupStatusResponse) => {
|
||||
if (res.step === 'finished') {
|
||||
localStorage.setItem('setup_status', 'finished')
|
||||
storage.set(STORAGE_KEYS.CONFIG.SETUP_STATUS, 'finished')
|
||||
router.push('/signin')
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -9,10 +9,12 @@ import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
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() {
|
||||
const { t } = useTranslation()
|
||||
@@ -40,7 +42,7 @@ export default function CheckCode() {
|
||||
setIsLoading(true)
|
||||
const res = await sendResetPasswordCode(email, locale)
|
||||
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)
|
||||
params.set('token', encodeURIComponent(res.data))
|
||||
params.set('email', encodeURIComponent(email))
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
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 { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendEMailLoginCode } from '@/service/common'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
type MailAndCodeAuthProps = {
|
||||
isInvite: boolean
|
||||
@@ -40,7 +42,7 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
||||
setIsLoading(true)
|
||||
const ret = await sendEMailLoginCode(email, locale)
|
||||
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)
|
||||
params.set('email', encodeURIComponent(email))
|
||||
params.set('token', encodeURIComponent(ret.data))
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { ReadonlyURLSearchParams } from 'next/navigation'
|
||||
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 {
|
||||
const itemStr = localStorage.getItem(key)
|
||||
const itemStr = storage.get<string>(key)
|
||||
if (!itemStr)
|
||||
return null
|
||||
|
||||
try {
|
||||
const item = JSON.parse(itemStr)
|
||||
localStorage.removeItem(key)
|
||||
storage.remove(key)
|
||||
if (!item?.value)
|
||||
return null
|
||||
|
||||
@@ -24,7 +26,7 @@ export const resolvePostLoginRedirect = (searchParams: ReadonlyURLSearchParams)
|
||||
const redirectUrl = searchParams.get(REDIRECT_URL_KEY)
|
||||
if (redirectUrl) {
|
||||
try {
|
||||
localStorage.removeItem(OAUTH_AUTHORIZE_PENDING_KEY)
|
||||
storage.remove(STORAGE_KEYS.AUTH.OAUTH_AUTHORIZE_PENDING)
|
||||
return decodeURIComponent(redirectUrl)
|
||||
}
|
||||
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 { InputVarType } from '@/app/components/workflow/types'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { PromptRole } from '@/models/debug'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import { AgentStrategy } from '@/types/app'
|
||||
@@ -179,7 +180,7 @@ export const CSRF_COOKIE_NAME = () => {
|
||||
return isSecure ? '__Host-csrf_token' : '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_HEADER_NAME = 'X-App-Passport'
|
||||
|
||||
@@ -229,7 +230,7 @@ export const VAR_ITEM_TEMPLATE_IN_PIPELINE = {
|
||||
|
||||
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 = {
|
||||
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 { IS_CLOUD_EDITION } from '@/config'
|
||||
import { isServer } from '@/utils/client'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export type TriggerEventsLimitModalPayload = {
|
||||
usage: number
|
||||
@@ -80,15 +81,10 @@ export const useTriggerEventsLimitModal = ({
|
||||
if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey])
|
||||
return
|
||||
|
||||
let persistDismiss = true
|
||||
const persistDismiss = storage.isAvailable()
|
||||
let hasDismissed = false
|
||||
try {
|
||||
if (localStorage.getItem(storageKey) === '1')
|
||||
hasDismissed = true
|
||||
}
|
||||
catch {
|
||||
persistDismiss = false
|
||||
}
|
||||
if (storage.get<string>(storageKey) === '1')
|
||||
hasDismissed = true
|
||||
if (hasDismissed)
|
||||
return
|
||||
|
||||
@@ -110,16 +106,9 @@ export const useTriggerEventsLimitModal = ({
|
||||
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
|
||||
if (!storageKey)
|
||||
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
|
||||
if (showTriggerEventsLimitModal?.payload.persistDismiss)
|
||||
storage.set(storageKey, '1')
|
||||
}, [showTriggerEventsLimitModal])
|
||||
|
||||
return {
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
expect(setItemSpy.mock.calls.length).toBeGreaterThan(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')
|
||||
})
|
||||
|
||||
|
||||
@@ -30,15 +30,15 @@ import {
|
||||
DEFAULT_ACCOUNT_SETTING_TAB,
|
||||
isValidAccountSettingTab,
|
||||
} from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
useAccountSettingModal,
|
||||
usePricingModal,
|
||||
} from '@/hooks/use-query-params'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
import {
|
||||
|
||||
@@ -183,10 +183,10 @@ export const ModalContextProvider = ({
|
||||
|
||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||
const handleCancelAccountSettingModal = () => {
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
const educationVerifying = storage.get<string>(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||
|
||||
if (educationVerifying === 'yes')
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
storage.remove(STORAGE_KEYS.EDUCATION.VERIFYING)
|
||||
|
||||
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
||||
accountSettingCallbacksRef.current = null
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { fetchCurrentPlanInfo } from '@/service/billing'
|
||||
import {
|
||||
useModelListByType,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
import {
|
||||
useEducationStatus,
|
||||
} from '@/service/use-education'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
export type ProviderContextState = {
|
||||
modelProviders: ModelProvider[]
|
||||
@@ -200,7 +202,7 @@ export const ProviderContextProvider = ({
|
||||
|
||||
const { t } = useTranslation()
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('anthropic_quota_notice') === 'true')
|
||||
if (storage.get<string>(STORAGE_KEYS.UI.ANTHROPIC_QUOTA_NOTICE) === 'true')
|
||||
return
|
||||
|
||||
if (dayjs().isAfter(dayjs('2025-03-17')))
|
||||
@@ -216,7 +218,7 @@ export const ProviderContextProvider = ({
|
||||
message: t('provider.anthropicHosted.trialQuotaTip', { ns: 'common' }),
|
||||
duration: 60000,
|
||||
onClose: () => {
|
||||
localStorage.setItem('anthropic_quota_notice', 'true')
|
||||
storage.set(STORAGE_KEYS.UI.ANTHROPIC_QUOTA_NOTICE, 'true')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
"no-console": {
|
||||
"count": 16
|
||||
},
|
||||
"no-restricted-properties": {
|
||||
"count": 5
|
||||
},
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 4
|
||||
},
|
||||
@@ -505,6 +508,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/app/create-app-modal/index.spec.tsx": {
|
||||
"no-restricted-properties": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
}
|
||||
@@ -579,6 +585,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/switch-app-modal/index.spec.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/switch-app-modal/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
@@ -629,6 +640,9 @@
|
||||
}
|
||||
},
|
||||
"app/components/apps/list.spec.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 3
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 5
|
||||
}
|
||||
@@ -758,6 +772,11 @@
|
||||
"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": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 4
|
||||
@@ -853,6 +872,11 @@
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"app/components/base/chat/embedded-chatbot/hooks.spec.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/base/chat/embedded-chatbot/hooks.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 3
|
||||
@@ -1526,6 +1550,11 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"app/components/billing/plan/index.spec.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/billing/plan/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -1551,6 +1580,11 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/browser-initializer.tsx": {
|
||||
"no-restricted-properties": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/custom/custom-web-app-brand/index.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
@@ -3093,6 +3127,11 @@
|
||||
"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": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 3
|
||||
@@ -3827,6 +3866,11 @@
|
||||
"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": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 4
|
||||
@@ -4097,6 +4141,9 @@
|
||||
}
|
||||
},
|
||||
"app/install/installForm.spec.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
}
|
||||
@@ -4137,6 +4184,12 @@
|
||||
}
|
||||
},
|
||||
"context/modal-context.test.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 4
|
||||
},
|
||||
"no-restricted-properties": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
@@ -4505,6 +4558,11 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"utils/setup-status.spec.ts": {
|
||||
"no-restricted-globals": {
|
||||
"count": 11
|
||||
}
|
||||
},
|
||||
"utils/tool-call.spec.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@@ -44,6 +44,40 @@ export default antfu(
|
||||
{
|
||||
rules: {
|
||||
'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 { useToastContext } from '@/app/components/base/toast'
|
||||
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 { DSLImportStatus } from '@/models/app'
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
importDSLConfirm,
|
||||
} from '@/service/apps'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
type DSLPayload = {
|
||||
mode: DSLImportMode
|
||||
@@ -83,7 +84,7 @@ export const useImportDSL = () => {
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
|
||||
})
|
||||
onSuccess?.()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
storage.set(STORAGE_KEYS.APP.NEED_REFRESH_LIST, '1')
|
||||
await handleCheckPluginDependencies(app_id)
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id, mode: app_mode }, push)
|
||||
}
|
||||
@@ -137,7 +138,7 @@ export const useImportDSL = () => {
|
||||
message: t('newApp.appCreated', { ns: 'app' }),
|
||||
})
|
||||
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)
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { fetchWithRetry } from '@/utils'
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'is_other_tab_refreshing'
|
||||
import { storage } from '@/utils/storage'
|
||||
|
||||
let isRefreshing = false
|
||||
function waitUntilTokenRefreshed() {
|
||||
return new Promise<void>((resolve) => {
|
||||
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) {
|
||||
setTimeout(() => {
|
||||
_check()
|
||||
@@ -23,35 +23,28 @@ function waitUntilTokenRefreshed() {
|
||||
|
||||
const isRefreshingSignAvailable = function (delta: number) {
|
||||
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
|
||||
}
|
||||
|
||||
// only one request can send
|
||||
async function getNewAccessToken(timeout: number): Promise<void> {
|
||||
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) {
|
||||
await waitUntilTokenRefreshed()
|
||||
}
|
||||
else {
|
||||
isRefreshing = true
|
||||
globalThis.localStorage.setItem(LOCAL_STORAGE_KEY, '1')
|
||||
globalThis.localStorage.setItem('last_refresh_time', new Date().getTime().toString())
|
||||
storage.set(STORAGE_KEYS.AUTH.REFRESH_LOCK, '1')
|
||||
storage.set(STORAGE_KEYS.AUTH.LAST_REFRESH_TIME, new Date().getTime().toString())
|
||||
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`, {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Important: include cookies in the request
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;utf-8',
|
||||
},
|
||||
// No body needed - refresh token is in cookie
|
||||
}))
|
||||
if (error) {
|
||||
return Promise.reject(error)
|
||||
@@ -72,11 +65,9 @@ async function getNewAccessToken(timeout: number): Promise<void> {
|
||||
}
|
||||
|
||||
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
|
||||
globalThis.localStorage.removeItem(LOCAL_STORAGE_KEY)
|
||||
globalThis.localStorage.removeItem('last_refresh_time')
|
||||
storage.remove(STORAGE_KEYS.AUTH.REFRESH_LOCK)
|
||||
storage.remove(STORAGE_KEYS.AUTH.LAST_REFRESH_TIME)
|
||||
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'
|
||||
|
||||
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) {
|
||||
localStorage.setItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode), token)
|
||||
storage.set(`passport-${shareCode}`, token)
|
||||
}
|
||||
|
||||
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) {
|
||||
return localStorage.getItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode)) || ''
|
||||
return storage.get<string>(`passport-${shareCode}`) || ''
|
||||
}
|
||||
|
||||
export function clearWebAppAccessToken() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME)
|
||||
storage.remove(STORAGE_KEYS.AUTH.ACCESS_TOKEN)
|
||||
}
|
||||
|
||||
export function clearWebAppPassport(shareCode: string) {
|
||||
localStorage.removeItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode))
|
||||
storage.remove(`passport-${shareCode}`)
|
||||
}
|
||||
|
||||
type isWebAppLogin = {
|
||||
@@ -31,8 +32,6 @@ type isWebAppLogin = {
|
||||
}
|
||||
|
||||
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 })
|
||||
if (userId)
|
||||
params.append('user_id', userId)
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('setup-status utilities', () => {
|
||||
describe('fetchSetupStatusWithCache', () => {
|
||||
describe('when cache exists', () => {
|
||||
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()
|
||||
|
||||
@@ -28,11 +28,11 @@ describe('setup-status utilities', () => {
|
||||
})
|
||||
|
||||
it('should not modify localStorage when returning cached value', async () => {
|
||||
localStorage.setItem('setup_status', 'finished')
|
||||
localStorage.setItem('v1:setup_status', 'finished')
|
||||
|
||||
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(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 () => {
|
||||
@@ -56,24 +56,24 @@ describe('setup-status utilities', () => {
|
||||
|
||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||
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 () => {
|
||||
localStorage.setItem('setup_status', 'some_invalid_value')
|
||||
localStorage.setItem('v1:setup_status', 'some_invalid_value')
|
||||
const apiResponse: SetupStatusResponse = { step: 'not_started' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(result).toEqual(apiResponse)
|
||||
expect(localStorage.getItem('setup_status')).toBeNull()
|
||||
expect(localStorage.getItem('v1:setup_status')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cache edge cases', () => {
|
||||
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' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('setup-status utilities', () => {
|
||||
})
|
||||
|
||||
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' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('setup-status utilities', () => {
|
||||
|
||||
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 { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { fetchSetupStatus } from '@/service/common'
|
||||
|
||||
const SETUP_STATUS_KEY = 'setup_status'
|
||||
import { storage } from './storage'
|
||||
|
||||
const isSetupStatusCached = (): boolean =>
|
||||
localStorage.getItem(SETUP_STATUS_KEY) === 'finished'
|
||||
storage.get<string>(STORAGE_KEYS.CONFIG.SETUP_STATUS) === 'finished'
|
||||
|
||||
export const fetchSetupStatusWithCache = async (): Promise<SetupStatusResponse> => {
|
||||
if (isSetupStatusCached())
|
||||
@@ -13,9 +13,9 @@ export const fetchSetupStatusWithCache = async (): Promise<SetupStatusResponse>
|
||||
const status = await fetchSetupStatus()
|
||||
|
||||
if (status.step === 'finished')
|
||||
localStorage.setItem(SETUP_STATUS_KEY, 'finished')
|
||||
storage.set(STORAGE_KEYS.CONFIG.SETUP_STATUS, 'finished')
|
||||
else
|
||||
localStorage.removeItem(SETUP_STATUS_KEY)
|
||||
storage.remove(STORAGE_KEYS.CONFIG.SETUP_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>
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
mockLocalStorage = createMockLocalStorage()
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
@@ -142,4 +142,7 @@ beforeEach(() => {
|
||||
writable: 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