refactor(web): migrate shared localStorage to createLocalStorageState (#37408)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Sukka 2026-06-19 21:51:45 +08:00 committed by GitHub
parent 734211d735
commit 9eca75c7fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 488 additions and 448 deletions

View File

@ -228,9 +228,6 @@
}
},
"web/app/(shareLayout)/webapp-reset-password/page.tsx": {
"no-restricted-globals": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
@ -252,9 +249,6 @@
}
},
"web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx": {
"no-restricted-globals": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
@ -321,11 +315,6 @@
"count": 1
}
},
"web/app/account/(commonLayout)/delete-account/index.tsx": {
"no-restricted-globals": {
"count": 1
}
},
"web/app/account/oauth/authorize/layout.tsx": {
"ts/no-explicit-any": {
"count": 1
@ -378,11 +367,6 @@
"count": 1
}
},
"web/app/components/app-sidebar/index.tsx": {
"no-restricted-globals": {
"count": 1
}
},
"web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx": {
"erasable-syntax-only/enums": {
"count": 1
@ -650,9 +634,6 @@
"jsx-a11y/no-static-element-interactions": {
"count": 1
},
"no-restricted-globals": {
"count": 6
},
"react/set-state-in-effect": {
"count": 4
},
@ -832,14 +813,6 @@
"count": 1
}
},
"web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/app/create-app-dialog/app-list/index.tsx": {
"no-restricted-imports": {
"count": 1
@ -3940,11 +3913,6 @@
"count": 1
}
},
"web/app/components/header/index.tsx": {
"tailwindcss/no-duplicate-classes": {
"count": 1
}
},
"web/app/components/header/nav/index.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -4679,11 +4647,6 @@
"count": 2
}
},
"web/app/components/signin/countdown.tsx": {
"no-restricted-globals": {
"count": 4
}
},
"web/app/components/snippet-list/components/snippet-card.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -6213,11 +6176,6 @@
"count": 2
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx": {
"no-restricted-properties": {
"count": 3
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx": {
"react-refresh/only-export-components": {
"count": 2
@ -7339,9 +7297,6 @@
}
},
"web/app/reset-password/page.tsx": {
"no-restricted-globals": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}

View File

@ -33,9 +33,9 @@
- Use local component state for state owned by one component.
- Use feature-level Jotai atoms for simple client state shared across components in the same feature, especially when components need a shared source of truth, derived values, or shared actions.
- Use existing feature stores for complex or high-frequency interaction state such as workflow canvas, drag, resize, and panel runtime state.
- Use `foxact/use-local-storage` only for low-frequency, client-only persistence such as user preferences, dismissed notices, and UI defaults. Do not use localStorage as the live source of truth for app state.
- For shared low-frequency, client-only persistence such as user preferences, dismissed notices, and UI defaults, use feature-owned storage modules built with `createLocalStorageState`.
- For high-frequency interactions, update the feature state during interaction and persist storage only on commit or settled updates.
- Do not access `localStorage`, `window.localStorage`, or `globalThis.localStorage` directly in app code; use the storage hook boundary and preserve existing raw/custom storage formats.
- Keep storage keys and raw/custom formats in the owner module; callers should import the named storage hooks instead of scattering direct storage access.
- Do not add ad hoc global event listeners for shared state. Prefer atoms, existing stores, or a shared subscription hook so listeners are centralized and deduplicated.
## Agent V2 Frontend

View File

@ -4,9 +4,9 @@ import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AppDetailNav from '@/app/components/app-sidebar'
const mockSetAppSidebarExpand = vi.fn()
const mockSetDetailSidebarMode = vi.fn()
let mockAppSidebarExpand = 'expand'
let mockDetailSidebarMode = 'expand'
let mockPathname = '/app/app-1/logs'
let mockSelectedSegment = 'logs'
let mockIsHovering = true
@ -18,24 +18,8 @@ vi.mock('react-i18next', () => ({
}),
}))
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
appDetail: {
id: 'app-1',
name: 'Demo App',
mode: 'chat',
icon: '🤖',
icon_type: 'emoji',
icon_background: '#FFEAD5',
icon_url: null,
},
appSidebarExpand: mockAppSidebarExpand,
setAppSidebarExpand: mockSetAppSidebarExpand,
}),
}))
vi.mock('zustand/react/shallow', () => ({
useShallow: (selector: unknown) => selector,
vi.mock('@/app/components/main-nav/storage', () => ({
useDetailSidebarMode: () => [mockDetailSidebarMode, mockSetDetailSidebarMode],
}))
vi.mock('@/next/navigation', () => ({
@ -129,7 +113,7 @@ describe('App Sidebar Shell Flow', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
mockAppSidebarExpand = 'expand'
mockDetailSidebarMode = 'expand'
mockPathname = '/app/app-1/logs'
mockSelectedSegment = 'logs'
mockIsHovering = true
@ -145,13 +129,13 @@ describe('App Sidebar Shell Flow', () => {
expect(logsLink.className).toContain('bg-components-menu-item-bg-active')
fireEvent.click(screen.getByRole('button'))
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse')
expect(mockSetDetailSidebarMode).toHaveBeenCalledWith('collapse')
const preventDefault = vi.fn()
hotkeyHandler?.({ preventDefault })
expect(preventDefault).toHaveBeenCalled()
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse')
expect(mockSetDetailSidebarMode).toHaveBeenCalledWith('collapse')
})
it('keeps the normal sidebar on workflow routes', () => {

View File

@ -85,8 +85,8 @@ vi.mock('@/hooks/use-async-window-open', () => ({
useAsyncWindowOpen: () => vi.fn(),
}))
vi.mock('foxact/use-local-storage', () => ({
useSetLocalStorage: () => mockSetEducationVerifying,
vi.mock('@/app/education-apply/storage', () => ({
useSetEducationVerifying: () => mockSetEducationVerifying,
}))
// ─── External component mocks ───────────────────────────────────────────────

View File

@ -7,18 +7,17 @@ import type { App } from '@/types/app'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueryClient } from '@tanstack/react-query'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import AppCard from '@/app/components/app/overview/app-card'
import TriggerCard from '@/app/components/app/overview/trigger-card'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import Loading from '@/app/components/base/loading'
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
import { isTriggerNode } from '@/app/components/workflow/types'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import {
updateAppSiteAccessToken,
@ -85,7 +84,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' }))
: null
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const updateAppDetail = useCallback(async () => {
try {

View File

@ -6,7 +6,7 @@ import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage'
import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'
import useDocumentTitle from '@/hooks/use-document-title'
@ -23,6 +23,7 @@ export default function CheckCode() {
const [email, setEmail] = useState('')
const [loading, setIsLoading] = useState(false)
const locale = useLocale()
const setCountdownLeftTime = useSetCountdownLeftTime()
const handleGetEMailVerificationCode = async () => {
try {
@ -38,7 +39,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}`)
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(res.data))
params.set('email', encodeURIComponent(email))

View File

@ -4,7 +4,7 @@ import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage'
import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'
import { useRouter, useSearchParams } from '@/next/navigation'
@ -18,6 +18,7 @@ export default function MailAndCodeAuth() {
const [email, setEmail] = useState(emailFromLink)
const [loading, setIsLoading] = useState(false)
const locale = useLocale()
const setCountdownLeftTime = useSetCountdownLeftTime()
const handleGetEMailVerificationCode = async () => {
try {
@ -33,7 +34,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}`)
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
params.set('email', encodeURIComponent(email))
params.set('token', encodeURIComponent(ret.data))

View File

@ -2,7 +2,7 @@
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage'
import CheckEmail from './components/check-email'
import FeedBack from './components/feed-back'
import VerifyEmail from './components/verify-email'
@ -14,6 +14,7 @@ type DeleteAccountProps = {
export default function DeleteAccount(props: DeleteAccountProps) {
const { t } = useTranslation()
const setCountdownLeftTime = useSetCountdownLeftTime()
const [showVerifyEmail, setShowVerifyEmail] = useState(false)
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false)
@ -21,10 +22,10 @@ export default function DeleteAccount(props: DeleteAccountProps) {
const handleEmailCheckSuccess = useCallback(async () => {
try {
setShowVerifyEmail(true)
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
}
catch (error) { console.error(error) }
}, [])
}, [setCountdownLeftTime])
if (showFeedbackDialog)
return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} />

View File

@ -12,8 +12,8 @@ vi.mock('@/next/navigation', () => ({
useSearchParams: vi.fn(),
}))
vi.mock('foxact/use-local-storage', () => ({
useSetLocalStorage: () => setEducationVerifyingMock,
vi.mock('@/app/education-apply/storage', () => ({
useSetEducationVerifying: () => setEducationVerifyingMock,
}))
const mockUseSearchParams = vi.mocked(useSearchParams)

View File

@ -3,19 +3,11 @@ import userEvent from '@testing-library/user-event'
import * as React from 'react'
import AppDetailNav from '..'
let mockAppSidebarExpand = 'expand'
const mockSetAppSidebarExpand = vi.fn()
let mockDetailSidebarMode = 'expand'
const mockSetDetailSidebarMode = vi.fn()
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
appDetail: { id: 'app-1', name: 'Test', mode: 'chat', icon: '🤖', icon_type: 'emoji', icon_background: '#fff' },
appSidebarExpand: mockAppSidebarExpand,
setAppSidebarExpand: mockSetAppSidebarExpand,
}),
}))
vi.mock('zustand/react/shallow', () => ({
useShallow: (fn: unknown) => fn,
vi.mock('@/app/components/main-nav/storage', () => ({
useDetailSidebarMode: () => [mockDetailSidebarMode, mockSetDetailSidebarMode],
}))
let mockIsHovering = true
@ -79,7 +71,7 @@ const navigation = [
describe('AppDetailNav', () => {
beforeEach(() => {
vi.clearAllMocks()
mockAppSidebarExpand = 'expand'
mockDetailSidebarMode = 'expand'
mockIsHovering = true
mockKeyPressCallback = null
})
@ -114,7 +106,7 @@ describe('AppDetailNav', () => {
})
it('should apply collapsed width class', () => {
mockAppSidebarExpand = 'collapse'
mockDetailSidebarMode = 'collapse'
const { container } = render(<AppDetailNav navigation={navigation} />)
const sidebar = container.firstElementChild as HTMLElement
expect(sidebar).toHaveClass('w-14')
@ -164,37 +156,30 @@ describe('AppDetailNav', () => {
})
it('should pass collapse mode to nav links when collapsed', () => {
mockAppSidebarExpand = 'collapse'
mockDetailSidebarMode = 'collapse'
render(<AppDetailNav navigation={navigation} />)
expect(screen.getByTestId('nav-link-Overview')).toHaveAttribute('data-mode', 'collapse')
})
})
describe('Toggle behavior', () => {
it('should call setAppSidebarExpand on toggle', async () => {
it('should collapse detail sidebar on toggle', async () => {
const user = userEvent.setup()
render(<AppDetailNav navigation={navigation} />)
await user.click(screen.getByTestId('toggle-button'))
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse')
expect(mockSetDetailSidebarMode).toHaveBeenCalledWith('collapse')
})
it('should toggle from collapse to expand', async () => {
const user = userEvent.setup()
mockAppSidebarExpand = 'collapse'
mockDetailSidebarMode = 'collapse'
render(<AppDetailNav navigation={navigation} />)
await user.click(screen.getByTestId('toggle-button'))
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('expand')
})
})
describe('Sidebar persistence', () => {
it('should persist expand state to localStorage', () => {
render(<AppDetailNav navigation={navigation} />)
expect(localStorage.setItem).toHaveBeenCalledWith('app-detail-collapse-or-expand', 'expand')
expect(mockSetDetailSidebarMode).toHaveBeenCalledWith('expand')
})
})
@ -218,7 +203,7 @@ describe('AppDetailNav', () => {
act(() => {
cb!({ preventDefault: vi.fn() })
})
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse')
expect(mockSetDetailSidebarMode).toHaveBeenCalledWith('collapse')
})
})

View File

@ -106,10 +106,6 @@ vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', ()
},
}))
vi.mock('@/config', () => ({
NEED_REFRESH_APP_LIST_KEY: 'test-refresh-key',
}))
describe('useAppInfoActions', () => {
beforeEach(() => {
vi.clearAllMocks()

View File

@ -4,11 +4,10 @@ import type { CreateAppModalProps } from '@/app/components/explore/create-app-mo
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueryClient } from '@tanstack/react-query'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'
import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps'
@ -110,7 +109,7 @@ export function useAppInfoActions({ onDetailExpand, resetKey }: UseAppInfoAction
setActiveModal(null)
}, [setActiveModal])
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const emitAppMetaUpdate = useCallback(() => {
if (!appDetail?.id)

View File

@ -4,9 +4,8 @@ import { cn } from '@langgenius/dify-ui/cn'
import { useHotkey } from '@tanstack/react-hotkeys'
import { useHover } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useCallback } from 'react'
import { useDetailSidebarMode } from '@/app/components/main-nav/storage'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Divider from '../base/divider'
import AppInfo, { AppInfoView } from './app-info'
@ -37,28 +36,18 @@ const AppDetailNav = ({
iconType = 'app',
appInfoActions,
}: IAppDetailNavProps) => {
const { appSidebarExpand, setAppSidebarExpand } = useAppStore(useShallow(state => ({
appSidebarExpand: state.appSidebarExpand,
setAppSidebarExpand: state.setAppSidebarExpand,
})))
const [detailSidebarMode, setDetailSidebarMode] = useDetailSidebarMode()
const sidebarRef = React.useRef<HTMLDivElement>(null)
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const expand = appSidebarExpand === 'expand'
const expand = detailSidebarMode === 'expand'
const handleToggle = useCallback(() => {
setAppSidebarExpand(appSidebarExpand === 'expand' ? 'collapse' : 'expand')
}, [appSidebarExpand, setAppSidebarExpand])
setDetailSidebarMode(detailSidebarMode === 'expand' ? 'collapse' : 'expand')
}, [detailSidebarMode, setDetailSidebarMode])
const isHoveringSidebar = useHover(sidebarRef)
useEffect(() => {
if (appSidebarExpand) {
localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand)
setAppSidebarExpand(appSidebarExpand)
}
}, [appSidebarExpand, setAppSidebarExpand])
useHotkey('Mod+B', (e) => {
e.preventDefault()
handleToggle()
@ -81,7 +70,7 @@ const AppDetailNav = ({
)}
>
{renderHeader
? renderHeader(appSidebarExpand)
? renderHeader(detailSidebarMode)
: iconType === 'app' && (
appInfoActions
? (
@ -123,12 +112,12 @@ const AppDetailNav = ({
)}
>
{renderNavigation
? renderNavigation(appSidebarExpand)
? renderNavigation(detailSidebarMode)
: navigation.map((item, index) => {
return (
<NavLink
key={index}
mode={appSidebarExpand}
mode={detailSidebarMode}
iconMap={{ selected: item.selectedIcon, normal: item.icon }}
name={item.name}
href={item.href}
@ -137,7 +126,7 @@ const AppDetailNav = ({
)
})}
</nav>
{iconType !== 'app' && extraInfo && extraInfo(appSidebarExpand)}
{iconType !== 'app' && extraInfo && extraInfo(detailSidebarMode)}
</div>
)
}

View File

@ -3,7 +3,6 @@ import { useStore } from '../store'
const resetStore = () => {
useStore.setState({
appDetail: undefined,
appSidebarExpand: '',
currentLogItem: undefined,
currentLogModalActiveTab: 'DETAIL',
showPromptLogModal: false,
@ -21,7 +20,6 @@ describe('app store', () => {
it('should expose the default state', () => {
expect(useStore.getState()).toEqual(expect.objectContaining({
appDetail: undefined,
appSidebarExpand: '',
currentLogItem: undefined,
currentLogModalActiveTab: 'DETAIL',
showPromptLogModal: false,
@ -36,7 +34,6 @@ describe('app store', () => {
const currentLogItem = { id: 'message-1' } as ReturnType<typeof useStore.getState>['currentLogItem']
useStore.getState().setAppDetail(appDetail)
useStore.getState().setAppSidebarExpand('logs')
useStore.getState().setCurrentLogItem(currentLogItem)
useStore.getState().setCurrentLogModalActiveTab('MESSAGE')
useStore.getState().setShowPromptLogModal(true)
@ -45,7 +42,6 @@ describe('app store', () => {
expect(useStore.getState()).toEqual(expect.objectContaining({
appDetail,
appSidebarExpand: 'logs',
currentLogItem,
currentLogModalActiveTab: 'MESSAGE',
showPromptLogModal: true,

View File

@ -0,0 +1,12 @@
import type { Model } from '@/types/app'
import { createLocalStorageState } from 'foxact/create-local-storage-state'
const [
useAutoGenModel,
_useAutoGenModelValue,
_useSetAutoGenModel,
] = createLocalStorageState<Model>('auto-gen-model')
export {
useAutoGenModel,
}

View File

@ -39,6 +39,7 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug'
import { useGenerateRuleTemplate } from '@/service/use-apps'
import { useAutoGenModel } from '../auto-gen-model-storage'
import IdeaOutput from './idea-output'
import InstructionEditorInBasic from './instruction-editor'
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
@ -90,10 +91,8 @@ 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 [model, setModel] = React.useState<Model>(localModel || {
const [storedModel, setStoredModel] = useAutoGenModel()
const [model, setModel] = React.useState<Model>(storedModel || {
name: '',
provider: '',
mode: mode as unknown as ModelModeType,
@ -182,11 +181,8 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
useEffect(() => {
if (defaultModel) {
const localModel = localStorage.getItem('auto-gen-model')
? JSON.parse(localStorage.getItem('auto-gen-model') || '')
: null
if (localModel) {
setModel(localModel)
if (storedModel) {
setModel(storedModel)
}
else {
setModel(prev => ({
@ -196,7 +192,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
}))
}
}
}, [defaultModel])
}, [defaultModel, storedModel])
const renderLoading = (
<div className="flex h-full w-0 grow flex-col items-center justify-center space-y-3">
@ -213,8 +209,8 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
mode: newValue.mode as ModelModeType,
}
setModel(newModel)
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
}, [model, setModel])
setStoredModel(newModel)
}, [model, setModel, setStoredModel])
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
const newModel = {
@ -222,8 +218,8 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
completion_params: newParams as CompletionParams,
}
setModel(newModel)
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
}, [model, setModel])
setStoredModel(newModel)
}, [model, setModel, setStoredModel])
const onGenerate = async () => {
if (!isValid())

View File

@ -19,7 +19,6 @@ import {
useBoolean,
useSessionStorageState,
} from 'ahooks'
import { useLocalStorage } from 'foxact/use-local-storage'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -31,6 +30,7 @@ import ModelParameterModal from '@/app/components/header/account-setting/model-p
import { generateRule } from '@/service/debug'
import { useGenerateRuleTemplate } from '@/service/use-apps'
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
import { useAutoGenModel } from '../auto-gen-model-storage'
import IdeaOutput from '../automatic/idea-output'
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
import ResPlaceholder from '../automatic/res-placeholder'
@ -40,7 +40,6 @@ import { GeneratorType } from '../automatic/types'
import useGenData from '../automatic/use-gen-data'
const i18nPrefix = 'generate'
const AUTO_GEN_MODEL_STORAGE_KEY = 'auto-gen-model'
const defaultCompletionParams = {
temperature: 0.7,
max_tokens: 0,
@ -75,7 +74,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
},
) => {
const { t } = useTranslation()
const [storedModel, setStoredModel] = useLocalStorage<Model>(AUTO_GEN_MODEL_STORAGE_KEY)
const [storedModel, setStoredModel] = useAutoGenModel()
const [model, setModel] = React.useState<Model>(storedModel || {
name: '',
provider: '',

View File

@ -6,8 +6,8 @@ import { AppACLPermission } from '@/utils/permission'
import { useConfiguration } from '../use-configuration'
const mockSetShowAccountSettingModal = vi.fn()
const mockSetAppSidebarExpand = vi.fn()
const mockSetShowAppConfigureFeaturesModal = vi.fn()
const mockSetDetailSidebarMode = vi.fn()
const mockHandleMultipleModelConfigsChange = vi.fn()
const mockFetchCollectionList = vi.fn()
const mockFetchAppDetailDirect = vi.fn()
@ -85,12 +85,15 @@ vi.mock('@/app/components/app/store', () => ({
mode: AppModeEnum.CHAT,
permission_keys: mockAppPermissionKeys,
},
setAppSidebarExpand: mockSetAppSidebarExpand,
showAppConfigureFeaturesModal: false,
setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal,
}),
}))
vi.mock('@/app/components/main-nav/storage', () => ({
useSetDetailSidebarMode: () => mockSetDetailSidebarMode,
}))
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: () => ({
data: undefined,
@ -466,7 +469,7 @@ describe('useConfiguration', () => {
expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalledWith(true)
expect(mockFormattingChangedDispatcher).toHaveBeenCalled()
expect(mockHandleMultipleModelConfigsChange).toHaveBeenCalled()
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse')
expect(mockSetDetailSidebarMode).toHaveBeenCalledWith('collapse')
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
expect(mockSetConversationHistoriesRole).toHaveBeenCalledWith({
assistant_prefix: 'bot',

View File

@ -41,6 +41,7 @@ import {
useTextGenerationCurrentProviderAndModelAndModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
import { useSetDetailSidebarMode } from '@/app/components/main-nav/storage'
import { ANNOTATION_DEFAULT, DATASET_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
@ -112,12 +113,12 @@ export const useConfiguration = (): ConfigurationViewModel => {
const { isLoadingCurrentWorkspace, currentWorkspace, userProfile, workspacePermissionKeys } = useAppContext()
const openIntegrationsSetting = useIntegrationsSetting()
const { appDetail, showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
const { appDetail, showAppConfigureFeaturesModal, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
appDetail: state.appDetail,
setAppSidebarExpand: state.setAppSidebarExpand,
showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal,
setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal,
})))
const setDetailSidebarMode = useSetDetailSidebarMode()
const { data: fileUploadConfigResponse } = useFileUploadConfig()
const latestPublishedAt = useMemo(() => appDetail?.model_config?.updated_at, [appDetail])
@ -568,8 +569,8 @@ export const useConfiguration = (): ConfigurationViewModel => {
{ id: `${Date.now()}-no-repeat`, model: '', provider: '', parameters: {} },
],
)
setAppSidebarExpand('collapse')
}, [completionParamsState, handleMultipleModelConfigsChange, modelConfig.model_id, modelConfig.provider, setAppSidebarExpand])
setDetailSidebarMode('collapse')
}, [completionParamsState, handleMultipleModelConfigsChange, modelConfig.model_id, modelConfig.provider, setDetailSidebarMode])
const onAgentSettingChange = useCallback((config: ModelConfig['agentConfig']) => {
setModelConfig(produce(modelConfig, (draft: ModelConfig) => {

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage'
import { AppModeEnum } from '@/types/app'
import Apps from '../index'
@ -52,14 +52,15 @@ vi.mock('@/app/components/app/type-selector', () => ({
}))
vi.mock('../../app-card', () => ({
default: ({ app, canCreate, onCreate }: { app: { app: { name: string } }, canCreate: boolean, onCreate: () => void }) => (
<div
<button
type="button"
data-testid="app-card"
data-name={app.app.name}
data-can-create={canCreate ? 'true' : 'false'}
onClick={onCreate}
>
{app.app.name}
</div>
</button>
),
}))
vi.mock('@/app/components/explore/create-app-modal', () => ({

View File

@ -6,17 +6,16 @@ import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { RiRobot2Line } from '@remixicon/react'
import { useDebounceFn } from 'ahooks'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppTypeSelector from '@/app/components/app/type-selector'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import Loading from '@/app/components/base/loading'
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 { useAppContext } from '@/context/app-context'
import { DSLImportMode } from '@/models/app'
import { useRouter } from '@/next/navigation'
@ -52,7 +51,7 @@ const Apps = ({
const invalidateAppList = useInvalidateAppList()
const allCategoriesEn = AppCategories.RECOMMENDED
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')

View File

@ -2,7 +2,7 @@ import type { App } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'

View File

@ -10,15 +10,14 @@ import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys'
import { useDebounceFn } from 'ahooks'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import AppIcon from '@/app/components/base/app-icon'
import Divider from '@/app/components/base/divider'
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
import Input from '@/app/components/base/input'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import useTheme from '@/hooks/use-theme'
@ -63,7 +62,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
const isCreatingRef = useRef(false)
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const onCreate = useCallback(async () => {
if (!canCreateApp)

View File

@ -6,7 +6,7 @@ import {
screen,
waitFor,
} from '@testing-library/react'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage'
import { DSLImportMode, DSLImportStatus } from '@/models/app'
import { AppModeEnum } from '@/types/app'
import CreateFromDSLModal, { CreateFromDSLModalTab } from '../index'

View File

@ -8,13 +8,12 @@ import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import { toast } from '@langgenius/dify-ui/toast'
import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys'
import { useDebounceFn } from 'ahooks'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import Input from '@/app/components/base/input'
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 { useProviderContext } from '@/context/provider-context'
import {
DSLImportMode,
@ -55,7 +54,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const [versions, setVersions] = useState<{ importedVersion: string, systemVersion: string }>()
const [importId, setImportId] = useState<string>()
const { handleCheckPluginDependencies } = usePluginDependencies()
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const readFile = useCallback((file: File) => {
const reader = new FileReader()

View File

@ -4,7 +4,6 @@ import { create } from 'zustand'
type State = {
appDetail?: App & Partial<AppSSO>
appSidebarExpand: string
currentLogItem?: IChatItem
currentLogModalActiveTab: string
showPromptLogModal: boolean
@ -15,7 +14,6 @@ type State = {
type Action = {
setAppDetail: (appDetail?: App & Partial<AppSSO>) => void
setAppSidebarExpand: (state: string) => void
setCurrentLogItem: (item?: IChatItem) => void
setCurrentLogModalActiveTab: (tab: string) => void
setShowPromptLogModal: (showPromptLogModal: boolean) => void
@ -27,8 +25,6 @@ type Action = {
export const useStore = create<State & Action>(set => ({
appDetail: undefined,
setAppDetail: appDetail => set(() => ({ appDetail })),
appSidebarExpand: '',
setAppSidebarExpand: appSidebarExpand => set(() => ({ appSidebarExpand })),
currentLogItem: undefined,
currentLogModalActiveTab: 'DETAIL',
setCurrentLogItem: currentLogItem => set(() => ({ currentLogItem })),

View File

@ -3,8 +3,8 @@ import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage'
import { Plan } from '@/app/components/billing/type'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { AppModeEnum } from '@/types/app'
import SwitchAppModal from '../index'

View File

@ -16,15 +16,14 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import AppIcon from '@/app/components/base/app-icon'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Input from '@/app/components/base/input'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'
import { deleteApp, switchApp } from '@/service/apps'
@ -59,7 +58,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
const [removeOriginal, setRemoveOriginal] = useState<boolean>(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const goStart = async () => {
try {

View File

@ -238,14 +238,6 @@ vi.mock('@/service/use-apps', () => ({
}),
}))
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
...actual,
NEED_REFRESH_APP_LIST_KEY: 'needRefreshAppList',
}
})
vi.mock('@/hooks/use-pay', () => ({
CheckModal: () => null,
}))

View File

@ -31,15 +31,14 @@ import {
TooltipTrigger,
} from '@langgenius/dify-ui/tooltip'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useId, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import AppIcon from '@/app/components/base/app-icon'
import StarIcon from '@/app/components/base/icons/src/vender/Star'
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
import { buildInstalledAppPath } from '@/app/components/explore/installed-app/routes'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
@ -311,7 +310,7 @@ export function AppCardActionBar({ app, onRefresh }: AppCardActionBarProps) {
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation()
const { mutateAsync: mutateToggleAppStar, isPending: isTogglingStar } = useToggleAppStarMutation()
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const resourceMaintainer = getAppResourceMaintainer(app)
const maintainerPermissionOptions = useMemo(() => ({
currentUserId,
@ -738,7 +737,7 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation()
const { mutateAsync: mutateToggleAppStar, isPending: isTogglingStar } = useToggleAppStarMutation()
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const resourceMaintainer = getAppResourceMaintainer(app)
const maintainerPermissionOptions = useMemo(() => ({
currentUserId,

View File

@ -4,10 +4,9 @@ import type { AppListQuery, AppListSortBy } from '@/contract/console/apps'
import { cn } from '@langgenius/dify-ui/cn'
import { keepPreviousData, useInfiniteQuery, useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useNeedRefreshAppList } from '@/app/components/apps/storage'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
@ -63,7 +62,7 @@ function List({
const [showNewAppModal, setShowNewAppModal] = useState(false)
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
const [droppedDSLFile, setDroppedDSLFile] = useState<File | undefined>()
const [needsRefreshAppList, setNeedsRefreshAppList] = useLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, '0', { raw: true })
const [needsRefreshAppList, setNeedsRefreshAppList] = useNeedRefreshAppList()
const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management')
const handleDSLFileDropped = useCallback((file: File) => {

View File

@ -0,0 +1,14 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'
const [
useNeedRefreshAppList,
_useNeedRefreshAppListValue,
useSetNeedRefreshAppList,
] = createLocalStorageState<string>(NEED_REFRESH_APP_LIST_KEY, '0', { raw: true })
export {
useNeedRefreshAppList,
useSetNeedRefreshAppList,
}

View File

@ -5,10 +5,10 @@ import type { AppData, ConversationItem } from '@/models/share'
import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow'
import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { useLocalStorage } from 'foxact/use-local-storage'
import { produce } from 'immer'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useConversationIdInfo, useWebAppSidebarCollapseState } from '@/app/components/base/chat/storage'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import { InputVarType } from '@/app/components/workflow/types'
import { useWebAppStore } from '@/context/web-app-context'
@ -19,12 +19,8 @@ import { useInvalidateShareConversations, useShareChatList, useShareConversation
import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { enrichSubmittedHumanInputFormData } from '../chat/answer/human-input-content/submitted-utils'
import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInputsFromUrlParams, getRawUserVariablesFromUrlParams } from '../utils'
const WEBAPP_SIDEBAR_COLLAPSE_STORAGE_KEY = 'webappSidebarCollapse'
const rawStorageOptions = { raw: true } as const
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
messages.forEach((item) => {
@ -129,17 +125,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
setLocaleFromProps()
}, [appData])
const [storedSidebarCollapseState, setStoredSidebarCollapseState] = useLocalStorage<string>(
WEBAPP_SIDEBAR_COLLAPSE_STORAGE_KEY,
undefined,
rawStorageOptions,
)
const [storedSidebarCollapseState, setStoredSidebarCollapseState] = useWebAppSidebarCollapseState()
const sidebarCollapseState = storedSidebarCollapseState === 'collapsed'
const handleSidebarCollapse = useCallback((state: boolean) => {
if (appId)
setStoredSidebarCollapseState(state ? 'collapsed' : 'expanded')
}, [appId, setStoredSidebarCollapseState])
const [conversationIdInfo, setConversationIdInfo] = useLocalStorage<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {})
const [conversationIdInfo, setConversationIdInfo] = useConversationIdInfo()
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || '', [appId, conversationIdInfo, userId])
const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
if (appId) {

View File

@ -28,7 +28,6 @@ describe('Log', () => {
beforeEach(() => {
vi.mocked(useAppStore).mockImplementation(selector => selector({
// State properties
appSidebarExpand: 'expand',
currentLogModalActiveTab: 'question',
showPromptLogModal: false,
showAgentLogModal: false,

View File

@ -5,10 +5,10 @@ import type { Locale } from '@/i18n-config'
import type { AppData, ConversationItem } from '@/models/share'
import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { useLocalStorage } from 'foxact/use-local-storage'
import { produce } from 'immer'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useConversationIdInfo } from '@/app/components/base/chat/storage'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { InputVarType } from '@/app/components/workflow/types'
import { useWebAppStore } from '@/context/web-app-context'
@ -18,7 +18,6 @@ import { useInvalidateShareConversations, useShareChatList, useShareConversation
import { useGetTryAppInfo, useGetTryAppParams } from '@/service/use-try-app'
import { TransferMethod } from '@/types/app'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
function getFormattedChatList(messages: any[]) {
@ -102,7 +101,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
}
setLanguageFromParams()
}, [appInfo])
const [conversationIdInfo, setConversationIdInfo] = useLocalStorage<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {})
const [conversationIdInfo, setConversationIdInfo] = useConversationIdInfo()
const removeConversationIdInfo = useCallback((appId: string) => {
setConversationIdInfo((prev) => {
const newInfo = { ...prev }

View File

@ -0,0 +1,19 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
import { CONVERSATION_ID_INFO } from './constants'
const [
useConversationIdInfo,
_useConversationIdInfoValue,
_useSetConversationIdInfo,
] = createLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {})
const [
useWebAppSidebarCollapseState,
_useWebAppSidebarCollapseStateValue,
_useSetWebAppSidebarCollapseState,
] = createLocalStorageState<string>('webappSidebarCollapse', undefined, { raw: true })
export {
useConversationIdInfo,
useWebAppSidebarCollapseState,
}

View File

@ -47,8 +47,8 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('foxact/use-local-storage', () => ({
useSetLocalStorage: () => setEducationVerifyingMock,
vi.mock('@/app/education-apply/storage', () => ({
useSetEducationVerifying: () => setEducationVerifyingMock,
}))
vi.mock('@/service/billing', () => ({

View File

@ -7,13 +7,12 @@ import {
RiGroupLine,
} from '@remixicon/react'
import { useUnmountedRef } from 'ahooks'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
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 { useSetEducationVerifying } from '@/app/education-apply/storage'
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
@ -73,7 +72,7 @@ const PlanComp: FC<Props> = ({
const canManageBilling = hasPermission(workspacePermissionKeys, BillingPermission.Manage)
const { mutateAsync, isPending } = useEducationVerify()
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
const setEducationVerifying = useSetEducationVerifying()
const unmountedRef = useUnmountedRef()
const handleVerify = () => {
if (isPending)

View File

@ -1,16 +1,13 @@
'use client'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useEffect } from 'react'
import {
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION } from '@/app/education-apply/constants'
import { useSetEducationVerifying } from '@/app/education-apply/storage'
import { useSearchParams } from '@/next/navigation'
export function EducationVerifyActionRecorder() {
const searchParams = useSearchParams()
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
const setEducationVerifying = useSetEducationVerifying()
useEffect(() => {
if (searchParams.get('action') === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)

View File

@ -12,7 +12,7 @@ import { fetchAppDetail, fetchAppList, fetchBanners } from '@/service/explore'
import { renderWithNuqs } from '@/test/nuqs-testing'
import { AppModeEnum } from '@/types/app'
import { AppACLPermission } from '@/utils/permission'
import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '../../learn-dify/atoms'
import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '../../learn-dify/storage'
import AppList from '../index'
type MockAppContext = {

View File

@ -1,23 +0,0 @@
'use client'
import { atom, useAtomValue, useSetAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
export const LEARN_DIFY_HIDDEN_STORAGE_KEY = 'explore-learn-dify-hidden'
const learnDifyHiddenAtom = atomWithStorage<boolean>(
LEARN_DIFY_HIDDEN_STORAGE_KEY,
false,
undefined,
{ getOnInit: true },
)
const learnDifyVisibleAtom = atom(get => !get(learnDifyHiddenAtom))
export function useLearnDifyVisibleValue() {
return useAtomValue(learnDifyVisibleAtom)
}
export function useSetLearnDifyHidden() {
return useSetAtom(learnDifyHiddenAtom)
}

View File

@ -7,8 +7,8 @@ import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLearnDifyAppList } from '@/service/use-explore'
import { useLearnDifyVisibleValue, useSetLearnDifyHidden } from './atoms'
import LearnDifyItem from './item'
import { useLearnDifyHiddenValue, useSetLearnDifyHidden } from './storage'
type LearnDifyProps = {
canCreate?: boolean
@ -132,10 +132,10 @@ const LearnDifyContent = ({
}
const DismissibleLearnDify = (props: LearnDifyProps) => {
const visible = useLearnDifyVisibleValue()
const hidden = useLearnDifyHiddenValue()
const setHidden = useSetLearnDifyHidden()
if (!visible)
if (hidden)
return null
return <LearnDifyContent {...props} onHide={() => setHidden(true)} />

View File

@ -0,0 +1,16 @@
'use client'
import { createLocalStorageState } from 'foxact/create-local-storage-state'
export const LEARN_DIFY_HIDDEN_STORAGE_KEY = 'explore-learn-dify-hidden'
const [
_useLearnDifyHidden,
useLearnDifyHiddenValue,
useSetLearnDifyHidden,
] = createLocalStorageState<boolean>(LEARN_DIFY_HIDDEN_STORAGE_KEY, false)
export {
useLearnDifyHiddenValue,
useSetLearnDifyHidden,
}

View File

@ -8,10 +8,10 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
import { useSetEducationExpiredHasNoticed, useSetEducationReverifyHasNoticed, useSetEducationReverifyPrevExpireAt } from '@/app/education-apply/storage'
import { useAppContext } from '@/context/app-context'
import { useRouter } from '@/next/navigation'
import { useLogout } from '@/service/use-common'
@ -27,9 +27,6 @@ type AccountDropdownProps = {
variant?: 'default' | 'mainNav'
}
const EDUCATION_REVERIFY_PREV_EXPIRE_AT_KEY = 'education-reverify-prev-expire-at'
const EDUCATION_REVERIFY_HAS_NOTICED_KEY = 'education-reverify-has-noticed'
const EDUCATION_EXPIRED_HAS_NOTICED_KEY = 'education-expired-has-noticed'
const mainNavMenuPopupClassName = 'w-60 max-w-80 overflow-hidden bg-components-panel-bg-blur! p-0! backdrop-blur-[5px]'
export default function AppSelector({
@ -41,9 +38,9 @@ export default function AppSelector({
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
const { t } = useTranslation()
const { userProfile, langGeniusVersionInfo } = useAppContext()
const clearEducationReverifyPrevExpireAt = useSetLocalStorage<number>(EDUCATION_REVERIFY_PREV_EXPIRE_AT_KEY)
const clearEducationReverifyHasNoticed = useSetLocalStorage<boolean>(EDUCATION_REVERIFY_HAS_NOTICED_KEY)
const clearEducationExpiredHasNoticed = useSetLocalStorage<boolean>(EDUCATION_EXPIRED_HAS_NOTICED_KEY)
const clearEducationReverifyPrevExpireAt = useSetEducationReverifyPrevExpireAt()
const clearEducationReverifyHasNoticed = useSetEducationReverifyHasNoticed()
const clearEducationExpiredHasNoticed = useSetEducationExpiredHasNoticed()
const { mutateAsync: logout } = useLogout()

View File

@ -1,12 +1,12 @@
'use client'
import type { EventEmitterValue } from '@/context/event-emitter'
import { cn } from '@langgenius/dify-ui/cn'
import { useLocalStorage } from 'foxact/use-local-storage'
import * as React from 'react'
import { useState } from 'react'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { usePathname } from '@/next/navigation'
import s from './index.module.css'
import { useWorkflowCanvasMaximizeValue } from './storage'
type HeaderWrapperProps = {
children: React.ReactNode
@ -19,7 +19,7 @@ const HeaderWrapper = ({
const isBordered = ['/apps', '/snippets', '/datasets/create', '/tools'].includes(pathname)
const inWorkflowCanvas = pathname.endsWith('/workflow')
const isPipelineCanvas = pathname.endsWith('/pipeline')
const [storedHideHeader] = useLocalStorage<boolean>('workflow-canvas-maximize', false)
const storedHideHeader = useWorkflowCanvasMaximizeValue()
const [eventHideHeader, setEventHideHeader] = useState<boolean | null>(null)
const hideHeader = eventHideHeader ?? storedHideHeader
const { eventEmitter } = useEventEmitterContextContext()

View File

@ -97,7 +97,7 @@ export function Header() {
return (
<div className="flex h-14 items-center">
<div className="flex min-w-0 flex-1 items-center overflow-hidden overflow-hidden pr-2 pl-3 min-[1280px]:pr-3">
<div className="flex min-w-0 flex-1 items-center overflow-hidden pr-2 pl-3 min-[1280px]:pr-3">
{renderLogo()}
<div className="mx-1.5 shrink-0 font-light text-divider-deep">/</div>
<WorkplaceSelector />

View File

@ -1,15 +1,15 @@
import { useLocalStorage } from 'foxact/use-local-storage'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { X } from '@/app/components/base/icons/src/vender/line/general'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { NOTICE_I18N } from '@/i18n-config/language'
import { useHideMaintenanceNotice } from './storage'
const MaintenanceNotice = () => {
const { t } = useTranslation()
const locale = useLanguage()
const [hiddenNoticeValue, setHiddenNoticeValue] = useLocalStorage<string>('hide-maintenance-notice', '0', { raw: true })
const [hiddenNoticeValue, setHiddenNoticeValue] = useHideMaintenanceNotice()
const hiddenNotice = hiddenNoticeValue === '1'
const [closedInSession, setClosedInSession] = useState(false)
const showNotice = !hiddenNotice && !closedInSession

View File

@ -0,0 +1,18 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
const [
useHideMaintenanceNotice,
_useHideMaintenanceNoticeValue,
_useSetHideMaintenanceNotice,
] = createLocalStorageState<string>('hide-maintenance-notice', '0', { raw: true })
const [
_useWorkflowCanvasMaximize,
useWorkflowCanvasMaximizeValue,
_useSetWorkflowCanvasMaximize,
] = createLocalStorageState<boolean>('workflow-canvas-maximize', false)
export {
useHideMaintenanceNotice,
useWorkflowCanvasMaximizeValue,
}

View File

@ -10,7 +10,7 @@ import { createStore, Provider as JotaiProvider } from 'jotai'
import { createTestQueryClient, renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useStore as useAppStore } from '@/app/components/app/store'
import { Plan } from '@/app/components/billing/type'
import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '@/app/components/explore/learn-dify/atoms'
import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '@/app/components/explore/learn-dify/storage'
import { useGotoAnythingOpen } from '@/app/components/goto-anything/atoms'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
@ -21,6 +21,7 @@ import { consoleQuery } from '@/service/client'
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
import { AppModeEnum } from '@/types/app'
import MainNav from '../index'
import { DETAIL_SIDEBAR_STORAGE_KEY } from '../storage'
const activeEdgeClassName = 'before:pointer-events-none'
@ -356,7 +357,6 @@ describe('MainNav', () => {
})
mockSwitchWorkspace.mockReturnValue(new Promise(() => {}))
hotkeyRegistrations.clear()
useAppStore.getState().setAppSidebarExpand('')
useAppStore.getState().setAppDetail()
})
@ -418,7 +418,7 @@ describe('MainNav', () => {
})
it('keeps the global navigation account section expanded on home routes', () => {
localStorage.setItem('app-detail-collapse-or-expand', 'collapse')
localStorage.setItem(DETAIL_SIDEBAR_STORAGE_KEY, 'collapse')
mockPathname = '/'
renderMainNav()
@ -626,7 +626,7 @@ describe('MainNav', () => {
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it('shows app detail navigation as a floating preview when hovering the collapsed top toggle', () => {
@ -637,7 +637,7 @@ describe('MainNav', () => {
fireEvent.mouseEnter(screen.getByTestId('app-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
expect(screen.getAllByTestId('app-detail-top')).toHaveLength(1)
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
@ -655,7 +655,7 @@ describe('MainNav', () => {
expect(screen.getByRole('complementary')).not.toHaveClass('overflow-visible')
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('expand')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('expand')
})
it('replaces global navigation with dataset detail navigation on dataset routes', () => {
@ -685,7 +685,7 @@ describe('MainNav', () => {
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it('shows dataset detail navigation as a floating preview when hovering the collapsed top toggle', () => {
@ -696,7 +696,7 @@ describe('MainNav', () => {
fireEvent.mouseEnter(screen.getByTestId('dataset-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
expect(screen.getAllByTestId('dataset-detail-top')).toHaveLength(1)
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
@ -756,7 +756,7 @@ describe('MainNav', () => {
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it('collapses deployment detail navigation from the top-right toggle', () => {
@ -769,7 +769,7 @@ describe('MainNav', () => {
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('deployment-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('deployment-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it.each([
@ -804,7 +804,7 @@ describe('MainNav', () => {
fireEvent.mouseEnter(screen.getByTestId('agent-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
expect(screen.getAllByTestId('agent-detail-top')).toHaveLength(1)
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')

View File

@ -15,7 +15,7 @@ import {
import { useSuspenseQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLearnDifyVisibleValue, useSetLearnDifyHidden } from '@/app/components/explore/learn-dify/atoms'
import { useLearnDifyHiddenValue, useSetLearnDifyHidden } from '@/app/components/explore/learn-dify/storage'
import AccountAbout from '@/app/components/header/account-about'
import Compliance from '@/app/components/header/account-dropdown/compliance'
import { ExternalLinkIndicator, MenuItemContent } from '@/app/components/header/account-dropdown/menu-item-content'
@ -52,7 +52,7 @@ const HelpMenu = ({
const docLink = useDocLink()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
const learnDifyVisible = useLearnDifyVisibleValue()
const learnDifyHidden = useLearnDifyHiddenValue()
const setLearnDifyHidden = useSetLearnDifyHidden()
const [aboutVisible, setAboutVisible] = useState(false)
const [open, setOpen] = useState(false)
@ -96,7 +96,7 @@ const HelpMenu = ({
/>
</DropdownMenuLinkItem>
<DropdownMenuCheckboxItem
checked={learnDifyVisible}
checked={!learnDifyHidden}
closeOnClick={false}
className="mx-0 h-8 gap-1 px-0 py-1 pr-2 pl-3"
onCheckedChange={checked => setLearnDifyHidden(!checked)}
@ -109,13 +109,13 @@ const HelpMenu = ({
aria-hidden
className={cn(
'relative inline-flex h-4 w-7 shrink-0 items-center rounded-[5px] p-0.5 transition-colors',
learnDifyVisible ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked',
!learnDifyHidden ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked',
)}
>
<span
className={cn(
'block h-3 w-2.5 rounded-[3px] bg-components-toggle-knob shadow-sm transition-transform',
learnDifyVisible && 'translate-x-3.5',
!learnDifyHidden && 'translate-x-3.5',
)}
/>
</span>

View File

@ -4,7 +4,6 @@ import type { MainNavItem, MainNavProps } from './types'
import { cn } from '@langgenius/dify-ui/cn'
import { useHotkey } from '@tanstack/react-hotkeys'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
@ -29,11 +28,11 @@ import { MainNavSearchButton } from './components/search-button'
import WebAppsSection from './components/web-apps-section'
import { WorkspaceCard } from './components/workspace-card'
import { isMainNavRouteVisible, MAIN_NAV_ROUTES } from './routes'
import { useDetailSidebarMode } from './storage'
const DATASET_COLLECTION_ROUTES = new Set(['create', 'create-from-pipeline', 'connect'])
const DATASET_DOCUMENT_CREATION_ROUTES = new Set(['create', 'create-from-pipeline'])
const DEPLOYMENT_COLLECTION_ROUTES = new Set(['create'])
const DETAIL_SIDEBAR_STORAGE_KEY = 'app-detail-collapse-or-expand'
const secondarySidebarHelpTriggerIcon = <span aria-hidden className="i-ri-question-line size-4 shrink-0" />
function SecondarySidebarHelpMenu({
@ -98,14 +97,12 @@ const MainNav = ({
const showDeploymentDetailNavigation = canUseAppDeploy && !isCurrentWorkspaceDatasetOperator && isDeploymentDetailPathname(pathname)
const showSnippetDetailBottomNavigation = isSnippetDetailPathname(pathname)
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation || showDeploymentDetailNavigation
const { hasAppDetail, appSidebarExpand, setAppDetail, setAppSidebarExpand } = useAppStore(useShallow(state => ({
const { hasAppDetail, setAppDetail } = useAppStore(useShallow(state => ({
hasAppDetail: !!state.appDetail,
appSidebarExpand: state.appSidebarExpand,
setAppDetail: state.setAppDetail,
setAppSidebarExpand: state.setAppSidebarExpand,
})))
const [storedDetailSidebarExpand, setStoredDetailSidebarExpand] = useLocalStorage<string>(DETAIL_SIDEBAR_STORAGE_KEY, 'expand', { raw: true })
const detailNavigationMode = appSidebarExpand === 'collapse' || (!appSidebarExpand && storedDetailSidebarExpand === 'collapse') ? 'collapse' : 'expand'
const [storedDetailSidebarExpand, setStoredDetailSidebarExpand] = useDetailSidebarMode()
const detailNavigationMode = storedDetailSidebarExpand === 'collapse' ? 'collapse' : 'expand'
const detailNavigationExpanded = detailNavigationMode === 'expand'
const isCollapsedDetailNavigation = showDetailNavigation && !detailNavigationExpanded
const [detailNavigationHoverPreviewOpen, setDetailNavigationHoverPreviewOpen] = useState(false)
@ -124,16 +121,17 @@ const MainNav = ({
setDetailNavigationTransitionDisabled(true)
setDetailNavigationHoverPreviewOpen(false)
setAppSidebarExpand('expand')
setStoredDetailSidebarExpand('expand')
detailNavigationTransitionTimerRef.current = setTimeout(() => {
setDetailNavigationTransitionDisabled(false)
}, 200)
return
}
const nextMode = detailNavigationExpanded ? 'collapse' : 'expand'
setDetailNavigationHoverPreviewOpen(false)
setAppSidebarExpand(detailNavigationExpanded ? 'collapse' : 'expand')
}, [detailNavigationExpanded, isDetailNavigationHoverPreviewOpen, setAppSidebarExpand])
setStoredDetailSidebarExpand(nextMode)
}, [detailNavigationExpanded, isDetailNavigationHoverPreviewOpen, setStoredDetailSidebarExpand])
const openDetailNavigationHoverPreview = useCallback(() => {
if (!isCollapsedDetailNavigation)
return
@ -161,13 +159,6 @@ const MainNav = ({
}
}, [])
useEffect(() => {
if (!showDetailNavigation)
return
setStoredDetailSidebarExpand(detailNavigationMode)
}, [detailNavigationMode, setStoredDetailSidebarExpand, showDetailNavigation])
useEffect(() => {
if (pathname.startsWith('/app/') || !hasAppDetail)
return

View File

@ -0,0 +1,16 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
type DetailSidebarMode = 'expand' | 'collapse'
export const DETAIL_SIDEBAR_STORAGE_KEY = 'app-detail-collapse-or-expand'
const [
useDetailSidebarMode,
_useDetailSidebarModeValue,
useSetDetailSidebarMode,
] = createLocalStorageState<DetailSidebarMode>(DETAIL_SIDEBAR_STORAGE_KEY, 'expand', { raw: true })
export {
useDetailSidebarMode,
useSetDetailSidebarMode,
}

View File

@ -1,6 +1,7 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import Countdown, { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '../countdown'
import Countdown from '../countdown'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '../storage'
describe('Countdown', () => {
beforeEach(() => {

View File

@ -2,9 +2,7 @@
import { useCountDown } from 'ahooks'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export const COUNT_DOWN_TIME_MS = 59000
export const COUNT_DOWN_KEY = 'leftTime'
import { COUNT_DOWN_TIME_MS, useCountdownLeftTimeValue, useSetCountdownLeftTime } from './storage'
type CountdownProps = {
onResend?: () => void
@ -12,24 +10,26 @@ 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 storedLeftTime = useCountdownLeftTimeValue()
const setStoredLeftTime = useSetCountdownLeftTime()
const [leftTime, setLeftTime] = useState(() => Number(storedLeftTime || COUNT_DOWN_TIME_MS))
const [time] = useCountDown({
leftTime,
onEnd: () => {
setLeftTime(0)
localStorage.removeItem(COUNT_DOWN_KEY)
setStoredLeftTime(null)
},
})
const resend = async function () {
setLeftTime(COUNT_DOWN_TIME_MS)
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
setStoredLeftTime(`${COUNT_DOWN_TIME_MS}`)
onResend?.()
}
useEffect(() => {
localStorage.setItem(COUNT_DOWN_KEY, `${time}`)
}, [time])
setStoredLeftTime(`${time}`)
}, [setStoredLeftTime, time])
return (
<p className="system-xs-regular text-text-tertiary">

View File

@ -0,0 +1,15 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
export const COUNT_DOWN_TIME_MS = 59000
export const COUNT_DOWN_KEY = 'leftTime'
const [
_useCountdownLeftTime,
useCountdownLeftTimeValue,
useSetCountdownLeftTime,
] = createLocalStorageState<string>(COUNT_DOWN_KEY, undefined, { raw: true })
export {
useCountdownLeftTimeValue,
useSetCountdownLeftTime,
}

View File

@ -3,7 +3,6 @@ import { render, screen } from '@testing-library/react'
import SnippetPage from '..'
const mockUseSnippetInit = vi.fn()
const mockSetAppSidebarExpand = vi.fn()
let capturedWorkflowDefaultContextProps: {
nodes: unknown[]
edges: unknown[]
@ -39,12 +38,6 @@ vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({
setAppSidebarExpand: mockSetAppSidebarExpand,
}),
}))
vi.mock('@/app/components/workflow', () => ({
default: ({
children,

View File

@ -6,7 +6,6 @@ import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { cn } from '@langgenius/dify-ui/cn'
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { useLocalStorage } from 'foxact/use-local-storage'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
@ -15,6 +14,7 @@ import useWorkspacePluginInstallPermission from '@/app/components/plugins/instal
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { getMarketplaceCategoryUrl } from '@/app/components/plugins/marketplace/utils'
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
import { useFeaturedToolsCollapsed } from '@/app/components/workflow/block-selector/storage'
import { useGetLanguage } from '@/context/i18n'
import Link from '@/next/link'
import { formatNumber } from '@/utils/format'
@ -42,8 +42,6 @@ type FeaturedToolPreviewPayload = {
description: string
}
const STORAGE_KEY = 'workflow_tools_featured_collapsed'
const FeaturedTools = ({
plugins,
providerMap,
@ -57,7 +55,7 @@ const FeaturedTools = ({
const previewCardHandle = useMemo(() => createPreviewCardHandle<FeaturedToolPreviewPayload>(), [])
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
const [isCollapsed, setIsCollapsed] = useLocalStorage<boolean>(STORAGE_KEY, false)
const [isCollapsed, setIsCollapsed] = useFeaturedToolsCollapsed()
if (visibleCountPlugins !== plugins) {
setVisibleCountPlugins(plugins)

View File

@ -6,7 +6,6 @@ import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { cn } from '@langgenius/dify-ui/cn'
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { useLocalStorage } from 'foxact/use-local-storage'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
@ -15,6 +14,7 @@ import useWorkspacePluginInstallPermission from '@/app/components/plugins/instal
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { getMarketplaceCategoryUrl } from '@/app/components/plugins/marketplace/utils'
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
import { useFeaturedTriggersCollapsed } from '@/app/components/workflow/block-selector/storage'
import { useGetLanguage } from '@/context/i18n'
import Link from '@/next/link'
import { formatNumber } from '@/utils/format'
@ -41,8 +41,6 @@ type FeaturedTriggerPreviewPayload = {
description: string
}
const STORAGE_KEY = 'workflow_triggers_featured_collapsed'
const FeaturedTriggers = ({
plugins,
providerMap,
@ -56,7 +54,7 @@ const FeaturedTriggers = ({
const triggerActionPreviewCardHandle = useMemo(() => createPreviewCardHandle<TriggerPluginActionPreviewPayload>(), [])
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
const [isCollapsed, setIsCollapsed] = useLocalStorage<boolean>(STORAGE_KEY, false)
const [isCollapsed, setIsCollapsed] = useFeaturedTriggersCollapsed()
if (visibleCountPlugins !== plugins) {
setVisibleCountPlugins(plugins)

View File

@ -3,13 +3,13 @@ import type { Dispatch, SetStateAction } from 'react'
import type { ViewType } from '@/app/components/workflow/block-selector/view-type-select'
import type { OnSelectBlock } from '@/app/components/workflow/types'
import { RiMoreLine } from '@remixicon/react'
import { useLocalStorage } from 'foxact/use-local-storage'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
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 { useRAGRecommendationsCollapsed } from '@/app/components/workflow/block-selector/storage'
import Link from '@/next/link'
import { useRAGRecommendedPlugins } from '@/service/use-tools'
import { getMarketplaceUrl } from '@/utils/var'
@ -21,15 +21,13 @@ type RAGToolRecommendationsProps = {
onTagsChange: Dispatch<SetStateAction<string[]>>
}
const STORAGE_KEY = 'workflow_rag_recommendations_collapsed'
const RAGToolRecommendations = ({
viewType,
onSelect,
onTagsChange,
}: RAGToolRecommendationsProps) => {
const { t } = useTranslation()
const [isCollapsed, setIsCollapsed] = useLocalStorage<boolean>(STORAGE_KEY, false)
const [isCollapsed, setIsCollapsed] = useRAGRecommendationsCollapsed()
const {
data: ragRecommendedPlugins,

View File

@ -0,0 +1,25 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
const [
useFeaturedToolsCollapsed,
_useFeaturedToolsCollapsedValue,
_useSetFeaturedToolsCollapsed,
] = createLocalStorageState<boolean>('workflow_tools_featured_collapsed', false)
const [
useFeaturedTriggersCollapsed,
_useFeaturedTriggersCollapsedValue,
_useSetFeaturedTriggersCollapsed,
] = createLocalStorageState<boolean>('workflow_triggers_featured_collapsed', false)
const [
useRAGRecommendationsCollapsed,
_useRAGRecommendationsCollapsedValue,
_useSetRAGRecommendationsCollapsed,
] = createLocalStorageState<boolean>('workflow_rag_recommendations_collapsed', false)
export {
useFeaturedToolsCollapsed,
useFeaturedTriggersCollapsed,
useRAGRecommendationsCollapsed,
}

View File

@ -13,7 +13,6 @@ import {
RiPlayLargeLine,
} from '@remixicon/react'
import { debounce } from 'es-toolkit/compat'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import * as React from 'react'
import {
cloneElement,
@ -58,6 +57,7 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { NodeActionsDropdown } from '@/app/components/workflow/node-actions-menu'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import { useSetWorkflowNodePanelWidth } from '@/app/components/workflow/persistence/local-storage-options'
import { useLogs } from '@/app/components/workflow/run/hooks'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { useStore } from '@/app/components/workflow/store'
@ -165,7 +165,7 @@ const BasePanel: FC<BasePanelProps> = ({
const setNodePanelWidth = useStore(s => s.setNodePanelWidth)
const pendingSingleRun = useStore(s => s.pendingSingleRun)
const setPendingSingleRun = useStore(s => s.setPendingSingleRun)
const setNodePanelWidthStorage = useSetLocalStorage<string>('workflow-node-panel-width', { raw: true })
const setNodePanelWidthStorage = useSetWorkflowNodePanelWidth()
const reservedCanvasWidth = 400 // Reserve the minimum visible width for the canvas
@ -178,7 +178,7 @@ const BasePanel: FC<BasePanelProps> = ({
const newValue = clampNodePanelWidth(width, maxNodePanelWidth)
if (source === 'user')
setNodePanelWidthStorage(`${newValue}`)
setNodePanelWidthStorage(newValue)
setNodePanelWidth(newValue)
}, [maxNodePanelWidth, setNodePanelWidth, setNodePanelWidthStorage])

View File

@ -11,6 +11,7 @@ import {
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useAutoGenModel } from '@/app/components/app/configuration/config/auto-gen-model-storage'
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 useTheme from '@/hooks/use-theme'
@ -41,25 +42,13 @@ const createEmptyModel = (): Model => ({
completion_params: {} as CompletionParams,
})
const getStoredModel = (): Model | null => {
if (typeof window === 'undefined')
return null
const savedModel = window.localStorage.getItem('auto-gen-model')
if (!savedModel)
return null
return JSON.parse(savedModel) as Model
}
const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
onApply,
crossAxisOffset,
}) => {
const [open, setOpen] = useState(false)
const [view, setView] = useState<GeneratorView>(GENERATOR_VIEWS.promptEditor)
const [model, setModel] = useState<Model | null>(() => getStoredModel())
const [model, setModel] = useAutoGenModel()
const [instruction, setInstruction] = useState('')
const [schema, setSchema] = useState<SchemaRoot | null>(null)
const { theme } = useTheme()
@ -102,8 +91,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
mode: newValue.mode as ModelModeType,
}
setModel(newModel)
window.localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
}, [resolvedModel])
}, [resolvedModel, setModel])
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
const newModel = {
@ -111,8 +99,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
completion_params: newParams as CompletionParams,
}
setModel(newModel)
window.localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
}, [resolvedModel])
}, [resolvedModel, setModel])
const { mutateAsync: generateStructuredOutputRules, isPending: isGenerating } = useGenerateStructuredOutputRules()

View File

@ -5,7 +5,6 @@ import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RiDraggable } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useLocalStorage } from 'foxact/use-local-storage'
import { produce } from 'immer'
import * as React from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
@ -14,12 +13,11 @@ import { ReactSortable } from 'react-sortablejs'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import { useEdgesInteractions } from '../../../hooks'
import AddButton from '../../_base/components/add-button'
import { useInlineLabelHintDismissed } from '../storage'
import Item from './class-item'
import { getDefaultClassLabel, isDefaultClassLabel } from './class-label-utils'
const i18nPrefix = 'nodes.questionClassifiers'
const INLINE_LABEL_HINT_STORAGE_KEY = 'question-classifier-inline-label-hint-dismissed'
type Props = Readonly<{
nodeId: string
list: Topic[]
@ -43,7 +41,7 @@ const ClassList: FC<Props> = ({
const [shouldScrollToEnd, setShouldScrollToEnd] = useState(false)
const prevListLength = useRef(list.length)
const [collapsed, setCollapsed] = useState(false)
const [storedRenameHintDismissed, setIsRenameHintDismissed] = useLocalStorage<boolean>(INLINE_LABEL_HINT_STORAGE_KEY)
const [storedRenameHintDismissed, setIsRenameHintDismissed] = useInlineLabelHintDismissed()
const isRenameHintDismissed = storedRenameHintDismissed ?? false
const handleClassChange = useCallback((index: number) => {

View File

@ -0,0 +1,11 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
const [
useInlineLabelHintDismissed,
_useInlineLabelHintDismissedValue,
_useSetInlineLabelHintDismissed,
] = createLocalStorageState<boolean>('question-classifier-inline-label-hint-dismissed')
export {
useInlineLabelHintDismissed,
}

View File

@ -1,14 +1,13 @@
import type { EditorState } from 'lexical'
import type { NoteTheme } from './types'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useCallback } from 'react'
import { useNodeDataUpdate, useWorkflowHistory, WorkflowHistoryEvent } from '../hooks'
import { NOTE_SHOW_AUTHOR_STORAGE_KEY } from './constants'
import { useSetWorkflowNoteShowAuthor } from '../persistence/local-storage-options'
export const useNote = (id: string) => {
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const { saveStateToHistory } = useWorkflowHistory()
const setShowAuthorStorage = useSetLocalStorage<string>(NOTE_SHOW_AUTHOR_STORAGE_KEY, { raw: true })
const setShowAuthorStorage = useSetWorkflowNoteShowAuthor()
const handleThemeChange = useCallback((theme: NoteTheme) => {
handleNodeDataUpdateWithSyncDraft({ id, data: { theme } })

View File

@ -1,19 +1,18 @@
import type { NoteNodeType } from '../note-node/types'
import { useLocalStorage } from 'foxact/use-local-storage'
import { useCallback } from 'react'
import { useAppContext } from '@/context/app-context'
import {
CUSTOM_NOTE_NODE,
NOTE_SHOW_AUTHOR_STORAGE_KEY,
} from '../note-node/constants'
import { NoteTheme } from '../note-node/types'
import { useWorkflowNoteShowAuthorValue } from '../persistence/local-storage-options'
import { useWorkflowStore } from '../store'
import { generateNewNode } from '../utils'
export const useOperator = () => {
const workflowStore = useWorkflowStore()
const { userProfile } = useAppContext()
const [showAuthorStorage] = useLocalStorage<string>(NOTE_SHOW_AUTHOR_STORAGE_KEY, 'true', { raw: true })
const showAuthorStorage = useWorkflowNoteShowAuthorValue()
const handleAddNote = useCallback(() => {
const { newNode } = generateNewNode({

View File

@ -5,7 +5,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too
import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
import { debounce } from 'es-toolkit/compat'
import { noop } from 'es-toolkit/function'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import {
memo,
useCallback,
@ -24,6 +23,7 @@ import {
useWorkflowInteractions,
} from '../../hooks'
import { useResizePanel } from '../../nodes/_base/hooks/use-resize-panel'
import { useSetDebugPreviewPanelWidth } from '../../persistence/local-storage-options'
import { BlockEnum } from '../../types'
import ChatWrapper from './chat-wrapper'
@ -55,10 +55,10 @@ const DebugAndPreview = () => {
const nodePanelWidth = useStore(s => s.nodePanelWidth)
const panelWidth = useStore(s => s.previewPanelWidth)
const setPanelWidth = useStore(s => s.setPreviewPanelWidth)
const setPanelWidthStorage = useSetLocalStorage<string>('debug-and-preview-panel-width', { raw: true })
const setPanelWidthStorage = useSetDebugPreviewPanelWidth()
const handleResize = useCallback((width: number, source: 'user' | 'system' = 'user') => {
if (source === 'user')
setPanelWidthStorage(`${width}`)
setPanelWidthStorage(width)
setPanelWidth(width)
}, [setPanelWidth, setPanelWidthStorage])
const maxPanelWidth = useMemo(() => {

View File

@ -1,15 +1,12 @@
import { useLocalStorage, useSetLocalStorage } from 'foxact/use-local-storage'
import { useEffect, useLayoutEffect as useLayoutEffectFromReact } from 'react'
import { useStore, useWorkflowStore } from '../store'
import {
isControlMode,
isFiniteNumber,
numberStorageOptions,
rawStorageOptions,
WORKFLOW_NODE_PANEL_WIDTH_KEY,
WORKFLOW_OPERATION_MODE_KEY,
WORKFLOW_PREVIEW_PANEL_WIDTH_KEY,
WORKFLOW_VARIABLE_INSPECT_PANEL_HEIGHT_KEY,
useDebugPreviewPanelWidthValue,
useWorkflowNodePanelWidthValue,
useWorkflowOperationMode,
useWorkflowVariableInspectPanelHeightValue,
} from './local-storage-options'
const useIsoLayoutEffect = typeof document !== 'undefined'
@ -17,10 +14,10 @@ const useIsoLayoutEffect = typeof document !== 'undefined'
: useEffect
export const WorkflowLocalStorageBridge = () => {
const [storedNodePanelWidth] = useLocalStorage<number>(WORKFLOW_NODE_PANEL_WIDTH_KEY, undefined, numberStorageOptions)
const [storedPreviewPanelWidth] = useLocalStorage<number>(WORKFLOW_PREVIEW_PANEL_WIDTH_KEY, undefined, numberStorageOptions)
const [storedVariableInspectPanelHeight] = useLocalStorage<number>(WORKFLOW_VARIABLE_INSPECT_PANEL_HEIGHT_KEY, undefined, numberStorageOptions)
const [storedControlMode] = useLocalStorage<string>(WORKFLOW_OPERATION_MODE_KEY, undefined, rawStorageOptions)
const storedNodePanelWidth = useWorkflowNodePanelWidthValue()
const storedPreviewPanelWidth = useDebugPreviewPanelWidthValue()
const storedVariableInspectPanelHeight = useWorkflowVariableInspectPanelHeightValue()
const [storedControlMode, setControlModeStorage] = useWorkflowOperationMode()
const workflowStore = useWorkflowStore()
const setNodePanelWidth = useStore(state => state.setNodePanelWidth)
@ -29,8 +26,6 @@ export const WorkflowLocalStorageBridge = () => {
const setVariableInspectPanelHeight = useStore(state => state.setVariableInspectPanelHeight)
const setControlMode = useStore(state => state.setControlMode)
const setControlModeStorage = useSetLocalStorage<string>(WORKFLOW_OPERATION_MODE_KEY, rawStorageOptions)
useIsoLayoutEffect(() => {
if (!isFiniteNumber(storedNodePanelWidth))
return

View File

@ -1,12 +1,14 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
import { NOTE_SHOW_AUTHOR_STORAGE_KEY } from '../note-node/constants'
import { ControlMode } from '../types'
export const WORKFLOW_NODE_PANEL_WIDTH_KEY = 'workflow-node-panel-width'
export const WORKFLOW_PREVIEW_PANEL_WIDTH_KEY = 'debug-and-preview-panel-width'
export const WORKFLOW_VARIABLE_INSPECT_PANEL_HEIGHT_KEY = 'workflow-variable-inpsect-panel-height'
export const WORKFLOW_OPERATION_MODE_KEY = 'workflow-operation-mode'
const WORKFLOW_NODE_PANEL_WIDTH_KEY = 'workflow-node-panel-width'
const WORKFLOW_PREVIEW_PANEL_WIDTH_KEY = 'debug-and-preview-panel-width'
const WORKFLOW_VARIABLE_INSPECT_PANEL_HEIGHT_KEY = 'workflow-variable-inpsect-panel-height'
const WORKFLOW_OPERATION_MODE_KEY = 'workflow-operation-mode'
export const rawStorageOptions = { raw: true } as const
export const numberStorageOptions = {
const rawStorageOptions = { raw: true } as const
const numberStorageOptions = {
serializer: String,
deserializer: Number,
} as const
@ -18,3 +20,45 @@ export const isControlMode = (value: string | null): value is ControlMode => {
export const isFiniteNumber = (value: number | null): value is number => {
return value !== null && Number.isFinite(value)
}
const [
_useWorkflowNodePanelWidth,
useWorkflowNodePanelWidthValue,
useSetWorkflowNodePanelWidth,
] = createLocalStorageState<number>(WORKFLOW_NODE_PANEL_WIDTH_KEY, undefined, numberStorageOptions)
const [
_useDebugPreviewPanelWidth,
useDebugPreviewPanelWidthValue,
useSetDebugPreviewPanelWidth,
] = createLocalStorageState<number>(WORKFLOW_PREVIEW_PANEL_WIDTH_KEY, undefined, numberStorageOptions)
const [
_useWorkflowVariableInspectPanelHeight,
useWorkflowVariableInspectPanelHeightValue,
useSetWorkflowVariableInspectPanelHeight,
] = createLocalStorageState<number>(WORKFLOW_VARIABLE_INSPECT_PANEL_HEIGHT_KEY, undefined, numberStorageOptions)
const [
useWorkflowOperationMode,
_useWorkflowOperationModeValue,
_useSetWorkflowOperationMode,
] = createLocalStorageState<string>(WORKFLOW_OPERATION_MODE_KEY, undefined, rawStorageOptions)
const [
_useWorkflowNoteShowAuthor,
useWorkflowNoteShowAuthorValue,
useSetWorkflowNoteShowAuthor,
] = createLocalStorageState<string>(NOTE_SHOW_AUTHOR_STORAGE_KEY, 'true', rawStorageOptions)
export {
useDebugPreviewPanelWidthValue,
useSetDebugPreviewPanelWidth,
useSetWorkflowNodePanelWidth,
useSetWorkflowNoteShowAuthor,
useSetWorkflowVariableInspectPanelHeight,
useWorkflowNodePanelWidthValue,
useWorkflowNoteShowAuthorValue,
useWorkflowOperationMode,
useWorkflowVariableInspectPanelHeightValue,
}

View File

@ -1,12 +1,12 @@
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { debounce } from 'es-toolkit/compat'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import {
useCallback,
useMemo,
} from 'react'
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
import { useSetWorkflowVariableInspectPanelHeight } from '../persistence/local-storage-options'
import { useStore } from '../store'
import Panel from './panel'
@ -22,10 +22,10 @@ const VariableInspectPanel: FC = () => {
return workflowCanvasHeight - 60
}, [workflowCanvasHeight])
const setPanelHeightStorage = useSetLocalStorage<string>('workflow-variable-inpsect-panel-height', { raw: true })
const setPanelHeightStorage = useSetWorkflowVariableInspectPanelHeight()
const handleResize = useCallback((width: number, height: number) => {
setPanelHeightStorage(`${height}`)
setPanelHeightStorage(height)
setVariableInspectPanelHeight(height)
}, [setVariableInspectPanelHeight, setPanelHeightStorage])

View File

@ -1,7 +1,7 @@
'use client'
import type { GeneratedGraph } from './types'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { CompletionParams, Model } from '@/types/app'
import type { CompletionParams, ModelModeType } from '@/types/app'
import {
AlertDialog,
AlertDialogActions,
@ -16,7 +16,6 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useBoolean } from 'ahooks'
import { useLocalStorage } from 'foxact/use-local-storage'
import * as React from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -29,15 +28,14 @@ import WorkflowPreview from '@/app/components/workflow/workflow-preview'
import { useRouter } from '@/next/navigation'
import { generateWorkflow } from '@/service/debug'
import { fetchWorkflowDraft } from '@/service/workflow'
import { ModelModeType } from '@/types/app'
import { getRedirectionPath } from '@/utils/app-redirection'
import { applyToCurrentApp, applyToNewApp, WorkflowApplyHashCollisionError, WorkflowApplyOrphanError } from './apply'
import ExamplePrompts from './example-prompts'
import GenerationPhases from './generation-phases'
import { EMPTY_WORKFLOW_GENERATOR_MODEL, useWorkflowGeneratorModel } from './storage'
import { useWorkflowGeneratorStore } from './store'
import useGenGraph from './use-gen-graph'
const STORAGE_MODEL_KEY = 'workflow-gen-model'
// Hard ceiling before we abort a hung request. Generous on purpose: the
// backend runs two sequential LLM calls and may retry a transient provider
// error (bounded backoff) or an unparseable response (one extra call), so a
@ -48,17 +46,6 @@ const FE_TIMEOUT_MS = 90_000
// keeping the limit client-side turns an opaque 400 into a visible input stop.
const MAX_INSTRUCTION_LENGTH = 10_000
// Stable default used both as the SSR/empty-storage seed for the persisted
// model and as the merge base when patching a partial update. Module-level so
// the reference stays identical across renders (useLocalStorage uses it as the
// server value, which must not change identity each render).
const EMPTY_MODEL: Model = {
name: '',
provider: '',
mode: ModelModeType.chat,
completion_params: {} as CompletionParams,
}
const renderPlaceholder = (label: string) => (
<div className="flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8">
<span className="i-custom-vender-other-generator size-8 text-text-quaternary" />
@ -120,10 +107,7 @@ const WorkflowGeneratorModal: React.FC = () => {
const isRefine = intent === 'refine' && !!currentAppId
// Persisted model selection. ``useLocalStorage`` is the storage boundary
// mandated for client-only preferences — the empty model is the SSR/seed
// value so ``model`` is always a concrete ``Model`` (never null) here.
const [model, setModel] = useLocalStorage<Model>(STORAGE_MODEL_KEY, EMPTY_MODEL)
const [model, setModel] = useWorkflowGeneratorModel()
const { defaultModel } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
@ -133,7 +117,7 @@ const WorkflowGeneratorModal: React.FC = () => {
useEffect(() => {
if (defaultModel && !model.name) {
setModel(prev => ({
...(prev ?? EMPTY_MODEL),
...(prev ?? EMPTY_WORKFLOW_GENERATOR_MODEL),
name: defaultModel.model,
provider: defaultModel.provider.provider,
}))
@ -142,7 +126,7 @@ const WorkflowGeneratorModal: React.FC = () => {
const handleModelChange = useCallback((newValue: { modelId: string, provider: string, mode?: string, features?: string[] }) => {
setModel(prev => ({
...(prev ?? EMPTY_MODEL),
...(prev ?? EMPTY_WORKFLOW_GENERATOR_MODEL),
provider: newValue.provider,
name: newValue.modelId,
mode: newValue.mode as ModelModeType,
@ -151,7 +135,7 @@ const WorkflowGeneratorModal: React.FC = () => {
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
setModel(prev => ({
...(prev ?? EMPTY_MODEL),
...(prev ?? EMPTY_WORKFLOW_GENERATOR_MODEL),
completion_params: newParams as CompletionParams,
}))
}, [setModel])

View File

@ -0,0 +1,20 @@
import type { CompletionParams, Model } from '@/types/app'
import { createLocalStorageState } from 'foxact/create-local-storage-state'
import { ModelModeType } from '@/types/app'
export const EMPTY_WORKFLOW_GENERATOR_MODEL: Model = {
name: '',
provider: '',
mode: ModelModeType.chat,
completion_params: {} as CompletionParams,
}
const [
useWorkflowGeneratorModel,
_useWorkflowGeneratorModelValue,
_useSetWorkflowGeneratorModel,
] = createLocalStorageState<Model>('workflow-gen-model', EMPTY_WORKFLOW_GENERATOR_MODEL)
export {
useWorkflowGeneratorModel,
}

View File

@ -1,3 +1,2 @@
export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify'
export const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying'
export const EDUCATION_RE_VERIFY_ACTION = 'educationReVerify'

View File

@ -8,12 +8,11 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { noop } from 'es-toolkit/function'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useEducationDiscount } from '@/app/components/billing/hooks/use-education-discount'
import { Plan } from '@/app/components/billing/type'
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants'
import { useSetEducationVerifying } from '@/app/education-apply/storage'
import { useAppContext } from '@/context/app-context'
import { useDocLink } from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
@ -61,7 +60,7 @@ const EducationApplyAgeContent = () => {
const openAsyncWindow = useAsyncWindowOpen()
const queryClient = useQueryClient()
const switchWorkspaceMutation = useMutation(consoleQuery.workspaces.switch.post.mutationOptions())
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
const setEducationVerifying = useSetEducationVerifying()
const searchParams = useSearchParams()
const token = searchParams.get('token')

View File

@ -4,13 +4,18 @@ import { useDebounceFn } from 'ahooks'
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import { useLocalStorage } from 'foxact/use-local-storage'
import {
useCallback,
useEffect,
useState,
} from 'react'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import {
useEducationExpiredHasNoticed,
useEducationReverifyHasNoticed,
useEducationReverifyPrevExpireAt,
useEducationVerifying,
} from '@/app/education-apply/storage'
import { useModalContextSelector } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
@ -19,7 +24,6 @@ import { useEducationAutocomplete, useEducationVerify } from '@/service/use-educ
import {
EDUCATION_RE_VERIFY_ACTION,
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from './constants'
dayjs.extend(utc)
@ -89,9 +93,9 @@ const useEducationReverifyNotice = ({
// const [educationInfo, setEducationInfo] = useState<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null } | null>(null)
// const isLoading = !educationInfo
const { educationAccountExpireAt, allowRefreshEducationVerify, isLoadingEducationAccountInfo: isLoading } = useProviderContext()
const [prevExpireAt, setPrevExpireAt] = useLocalStorage<number>('education-reverify-prev-expire-at', 0)
const [reverifyHasNoticed, setReverifyHasNoticed] = useLocalStorage<boolean>('education-reverify-has-noticed', false)
const [expiredHasNoticed, setExpiredHasNoticed] = useLocalStorage<boolean>('education-expired-has-noticed', false)
const [prevExpireAt, setPrevExpireAt] = useEducationReverifyPrevExpireAt()
const [reverifyHasNoticed, setReverifyHasNoticed] = useEducationReverifyHasNoticed()
const [expiredHasNoticed, setExpiredHasNoticed] = useEducationExpiredHasNoticed()
useEffect(() => {
if (isLoading || !timezone)
@ -132,7 +136,7 @@ const useEducationReverifyNotice = ({
export const useEducationInit = () => {
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
const setShowEducationExpireNoticeModal = useModalContextSelector(s => s.setShowEducationExpireNoticeModal)
const [educationVerifying, setEducationVerifying] = useLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'no', { raw: true })
const [educationVerifying, setEducationVerifying] = useEducationVerifying()
const searchParams = useSearchParams()
const educationVerifyAction = searchParams.get('action')

View File

@ -0,0 +1,38 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying'
const [
useEducationVerifying,
_useEducationVerifyingValue,
useSetEducationVerifying,
] = createLocalStorageState<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'no', { raw: true })
const [
useEducationReverifyPrevExpireAt,
_useEducationReverifyPrevExpireAtValue,
useSetEducationReverifyPrevExpireAt,
] = createLocalStorageState<number>('education-reverify-prev-expire-at', 0)
const [
useEducationReverifyHasNoticed,
_useEducationReverifyHasNoticedValue,
useSetEducationReverifyHasNoticed,
] = createLocalStorageState<boolean>('education-reverify-has-noticed', false)
const [
useEducationExpiredHasNoticed,
_useEducationExpiredHasNoticedValue,
useSetEducationExpiredHasNoticed,
] = createLocalStorageState<boolean>('education-expired-has-noticed', false)
export {
useEducationExpiredHasNoticed,
useEducationReverifyHasNoticed,
useEducationReverifyPrevExpireAt,
useEducationVerifying,
useSetEducationExpiredHasNoticed,
useSetEducationReverifyHasNoticed,
useSetEducationReverifyPrevExpireAt,
useSetEducationVerifying,
}

View File

@ -12,7 +12,7 @@ import useDocumentTitle from '@/hooks/use-document-title'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { sendResetPasswordCode } from '@/service/common'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '../components/signin/countdown'
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '../components/signin/storage'
export default function CheckCode() {
const { t } = useTranslation()
@ -22,6 +22,7 @@ export default function CheckCode() {
const [email, setEmail] = useState('')
const [loading, setIsLoading] = useState(false)
const locale = useLocale()
const setCountdownLeftTime = useSetCountdownLeftTime()
const handleGetEMailVerificationCode = async () => {
try {
@ -37,7 +38,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}`)
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(res.data))
params.set('email', encodeURIComponent(email))

View File

@ -2,10 +2,9 @@ import { Button } from '@langgenius/dify-ui/button'
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
import { toast } from '@langgenius/dify-ui/toast'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage'
import { emailRegex } from '@/config'
import { useLocale } from '@/context/i18n'
import { useRouter, useSearchParams } from '@/next/navigation'
@ -23,7 +22,7 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
const [email, setEmail] = useState(emailFromLink)
const [loading, setLoading] = useState(false)
const locale = useLocale()
const setCountdownLeftTime = useSetLocalStorage<string>(COUNT_DOWN_KEY, { raw: true })
const setCountdownLeftTime = useSetCountdownLeftTime()
const handleGetEMailVerificationCode = async () => {
try {

View File

@ -180,8 +180,6 @@ export const VAR_ITEM_TEMPLATE_IN_PIPELINE = {
export const appDefaultIconBackground = '#D5F5F6'
export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList'
export const DATASET_DEFAULT = {
top_k: 4,
score_threshold: 0.8,

View File

@ -11,7 +11,6 @@ import type { InputVar } from '@/app/components/workflow/types'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
import type { ExternalDataTool } from '@/models/common'
import type { ModerationConfig, PromptVariable } from '@/models/debug'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
DEFAULT_ACCOUNT_SETTING_TAB,
@ -20,9 +19,7 @@ import {
isValidSettingsTab,
isWorkspaceSettingTab,
} from '@/app/components/header/account-setting/constants'
import {
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
import { useSetEducationVerifying } from '@/app/education-apply/storage'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import {
@ -107,7 +104,7 @@ export const ModalContextProvider = ({
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const { currentWorkspace } = useAppContext()
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
const setEducationVerifying = useSetEducationVerifying()
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
const handleCancelAccountSettingModal = () => {

View File

@ -38,8 +38,8 @@ vi.mock('@/app/components/header/account-setting', () => ({
),
}))
vi.mock('foxact/use-local-storage', () => ({
useSetLocalStorage: () => mockSetEducationVerifying,
vi.mock('@/app/education-apply/storage', () => ({
useSetEducationVerifying: () => mockSetEducationVerifying,
}))
const mockUseProviderContext = vi.fn()

View File

@ -5,7 +5,6 @@ import type { ProviderContextState } from './provider-context'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueryClient } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useLocalStorage } from 'foxact/use-local-storage'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
@ -25,6 +24,7 @@ import {
} from '@/service/use-common'
import { useEducationStatus } from '@/service/use-education'
import { ProviderContext } from './provider-context'
import { useAnthropicQuotaNotice } from './provider-storage'
type ProviderContextProviderProps = {
children: ReactNode
@ -40,8 +40,6 @@ const unlimitedMemberInviteLimit: MemberInviteLimit = {
limit: 0,
}
const ANTHROPIC_QUOTA_NOTICE_STORAGE_KEY = 'anthropic_quota_notice'
const resolveMemberInviteLimit = (data: Awaited<ReturnType<typeof fetchCurrentPlanInfo>>): MemberInviteLimit => {
if (!data)
return unlimitedMemberInviteLimit
@ -158,11 +156,7 @@ export const ProviderContextProvider = ({
// #endregion Zendesk conversation fields
const { t } = useTranslation()
const [anthropicQuotaNotice, setAnthropicQuotaNotice] = useLocalStorage<string>(
ANTHROPIC_QUOTA_NOTICE_STORAGE_KEY,
'false',
{ raw: true },
)
const [anthropicQuotaNotice, setAnthropicQuotaNotice] = useAnthropicQuotaNotice()
useEffect(() => {
if (anthropicQuotaNotice === 'true')

View File

@ -0,0 +1,13 @@
import { createLocalStorageState } from 'foxact/create-local-storage-state'
const ANTHROPIC_QUOTA_NOTICE_STORAGE_KEY = 'anthropic_quota_notice'
const [
useAnthropicQuotaNotice,
_useAnthropicQuotaNoticeValue,
_useSetAnthropicQuotaNotice,
] = createLocalStorageState<string>(ANTHROPIC_QUOTA_NOTICE_STORAGE_KEY, 'false', { raw: true })
export {
useAnthropicQuotaNotice,
}

View File

@ -197,7 +197,7 @@ export default antfu(
'error',
{
name: 'localStorage',
message: 'Do not use localStorage directly. Use foxact/use-local-storage instead.',
message: 'Do not use localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
},
],
'no-restricted-properties': [
@ -205,19 +205,19 @@ export default antfu(
{
object: 'window',
property: 'localStorage',
message: 'Do not use window.localStorage directly. Use foxact/use-local-storage instead.',
message: 'Do not use window.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
},
{
object: 'globalThis',
property: 'localStorage',
message: 'Do not use globalThis.localStorage directly. Use foxact/use-local-storage instead.',
message: 'Do not use globalThis.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
},
],
'no-restricted-syntax': [
'error',
{
selector: 'ImportDeclaration[source.value="ahooks"] ImportSpecifier[imported.name="useLocalStorageState"]',
message: 'Do not use ahooks useLocalStorageState. Use foxact/use-local-storage instead.',
message: 'Do not use ahooks useLocalStorageState. Use foxact storage hooks instead.',
},
],
},

View File

@ -4,15 +4,14 @@ import type {
} from '@/models/app'
import type { AppIconType } from '@/types/app'
import { toast } from '@langgenius/dify-ui/toast'
import { useSetLocalStorage } from 'foxact/use-local-storage'
import {
useCallback,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { DSLImportStatus } from '@/models/app'
import { useRouter } from '@/next/navigation'
import {
@ -45,7 +44,7 @@ export const useImportDSL = () => {
const invalidateAppList = useInvalidateAppList()
const [versions, setVersions] = useState<{ importedVersion: string, systemVersion: string }>()
const importIdRef = useRef<string>('')
const setNeedRefresh = useSetLocalStorage<string>(NEED_REFRESH_APP_LIST_KEY, { raw: true })
const setNeedRefresh = useSetNeedRefreshAppList()
const handleImportDSL = useCallback(async (
payload: DSLPayload,
@ -114,7 +113,7 @@ export const useImportDSL = () => {
finally {
setIsFetching(false)
}
}, [isFetching, t, setNeedRefresh, handleCheckPluginDependencies, push, setNeedRefresh, invalidateAppList])
}, [isFetching, t, handleCheckPluginDependencies, push, setNeedRefresh, invalidateAppList])
const handleImportDSLConfirm = useCallback(async (
{
@ -157,7 +156,7 @@ export const useImportDSL = () => {
finally {
setIsFetching(false)
}
}, [isFetching, t, handleCheckPluginDependencies, setNeedRefresh, push, setNeedRefresh, invalidateAppList])
}, [isFetching, t, handleCheckPluginDependencies, setNeedRefresh, push, invalidateAppList])
return {
handleImportDSL,