diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 07c7c8c2a40..7e4521f51cb 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 } diff --git a/web/AGENTS.md b/web/AGENTS.md index 6221b2376ae..2281cfab4bf 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -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 diff --git a/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx index 0e51c650b21..c1f3a1c62d4 100644 --- a/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx +++ b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx @@ -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) => 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', () => { diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index 42131506208..1b010964ce2 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -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 ─────────────────────────────────────────────── diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index fd7a35c86da..74772c2b806 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -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 = ({ appId, isInPanel, className }) => { ? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' })) : null - const setNeedRefresh = useSetLocalStorage(NEED_REFRESH_APP_LIST_KEY, { raw: true }) + const setNeedRefresh = useSetNeedRefreshAppList() const updateAppDetail = useCallback(async () => { try { diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx index d905f8d2135..d21729ab59d 100644 --- a/web/app/(shareLayout)/webapp-reset-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx @@ -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)) diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx index 2d2fde18085..f5f66604283 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx @@ -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)) diff --git a/web/app/account/(commonLayout)/delete-account/index.tsx b/web/app/account/(commonLayout)/delete-account/index.tsx index e4af24e8bfc..0baf6e798e6 100644 --- a/web/app/account/(commonLayout)/delete-account/index.tsx +++ b/web/app/account/(commonLayout)/delete-account/index.tsx @@ -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 diff --git a/web/app/components/__tests__/education-verify-action-recorder.spec.tsx b/web/app/components/__tests__/education-verify-action-recorder.spec.tsx index d93dc90c2ac..fa885814458 100644 --- a/web/app/components/__tests__/education-verify-action-recorder.spec.tsx +++ b/web/app/components/__tests__/education-verify-action-recorder.spec.tsx @@ -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) diff --git a/web/app/components/app-sidebar/__tests__/index.spec.tsx b/web/app/components/app-sidebar/__tests__/index.spec.tsx index cf3537a1464..859d4f5de3f 100644 --- a/web/app/components/app-sidebar/__tests__/index.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/index.spec.tsx @@ -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) => 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() 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() 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() 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() await user.click(screen.getByTestId('toggle-button')) - expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('expand') - }) - }) - - describe('Sidebar persistence', () => { - it('should persist expand state to localStorage', () => { - render() - 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') }) }) diff --git a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts index e5f060bced1..898b8c55a74 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts +++ b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts @@ -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() diff --git a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts index 463a23cddf5..89688a4882f 100644 --- a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts +++ b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts @@ -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(NEED_REFRESH_APP_LIST_KEY, { raw: true }) + const setNeedRefresh = useSetNeedRefreshAppList() const emitAppMetaUpdate = useCallback(() => { if (!appDetail?.id) diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index 3e4fb51ee21..1e4783390e4 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -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(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 ( - {iconType !== 'app' && extraInfo && extraInfo(appSidebarExpand)} + {iconType !== 'app' && extraInfo && extraInfo(detailSidebarMode)} ) } diff --git a/web/app/components/app/__tests__/store.spec.ts b/web/app/components/app/__tests__/store.spec.ts index 204d659fdd8..6245d4b2d3d 100644 --- a/web/app/components/app/__tests__/store.spec.ts +++ b/web/app/components/app/__tests__/store.spec.ts @@ -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['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, diff --git a/web/app/components/app/configuration/config/auto-gen-model-storage.ts b/web/app/components/app/configuration/config/auto-gen-model-storage.ts new file mode 100644 index 00000000000..137e8bc556a --- /dev/null +++ b/web/app/components/app/configuration/config/auto-gen-model-storage.ts @@ -0,0 +1,12 @@ +import type { Model } from '@/types/app' +import { createLocalStorageState } from 'foxact/create-local-storage-state' + +const [ + useAutoGenModel, + _useAutoGenModelValue, + _useSetAutoGenModel, +] = createLocalStorageState('auto-gen-model') + +export { + useAutoGenModel, +} diff --git a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx index 7041a11ee67..763913f7a64 100644 --- a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx +++ b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx @@ -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 = ({ 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(localModel || { + const [storedModel, setStoredModel] = useAutoGenModel() + const [model, setModel] = React.useState(storedModel || { name: '', provider: '', mode: mode as unknown as ModelModeType, @@ -182,11 +181,8 @@ const GetAutomaticRes: FC = ({ 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 = ({ })) } } - }, [defaultModel]) + }, [defaultModel, storedModel]) const renderLoading = (
@@ -213,8 +209,8 @@ const GetAutomaticRes: FC = ({ 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 = ({ 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()) diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index dd7fd42dc8d..82f99204880 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -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 = ( }, ) => { const { t } = useTranslation() - const [storedModel, setStoredModel] = useLocalStorage(AUTO_GEN_MODEL_STORAGE_KEY) + const [storedModel, setStoredModel] = useAutoGenModel() const [model, setModel] = React.useState(storedModel || { name: '', provider: '', diff --git a/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx b/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx index bf1ea95cc36..c9f10b71574 100644 --- a/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx +++ b/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx @@ -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', diff --git a/web/app/components/app/configuration/hooks/use-configuration.ts b/web/app/components/app/configuration/hooks/use-configuration.ts index 17853d8c6af..53f3364fa9f 100644 --- a/web/app/components/app/configuration/hooks/use-configuration.ts +++ b/web/app/components/app/configuration/hooks/use-configuration.ts @@ -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) => { diff --git a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx index bfcab9d4184..72e3ff2c19a 100644 --- a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx @@ -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 }) => ( -
{app.app.name} -
+ ), })) vi.mock('@/app/components/explore/create-app-modal', () => ({ diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index 470de67aa29..04e759575b9 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -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(NEED_REFRESH_APP_LIST_KEY, { raw: true }) + const setNeedRefresh = useSetNeedRefreshAppList() const [keywords, setKeywords] = useState('') const [searchKeywords, setSearchKeywords] = useState('') diff --git a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx index df3b74255d5..47c8fb5c46c 100644 --- a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx @@ -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' diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 359df8fa79b..ce3f11f24c3 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -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(NEED_REFRESH_APP_LIST_KEY, { raw: true }) + const setNeedRefresh = useSetNeedRefreshAppList() const onCreate = useCallback(async () => { if (!canCreateApp) diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx index f1b3468b098..45bb635edcb 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx @@ -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' diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 87d4a21531d..c44e6f3769c 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -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() const { handleCheckPluginDependencies } = usePluginDependencies() - const setNeedRefresh = useSetLocalStorage(NEED_REFRESH_APP_LIST_KEY, { raw: true }) + const setNeedRefresh = useSetNeedRefreshAppList() const readFile = useCallback((file: File) => { const reader = new FileReader() diff --git a/web/app/components/app/store.ts b/web/app/components/app/store.ts index 020f2b4e6fe..059bd160430 100644 --- a/web/app/components/app/store.ts +++ b/web/app/components/app/store.ts @@ -4,7 +4,6 @@ import { create } from 'zustand' type State = { appDetail?: App & Partial - appSidebarExpand: string currentLogItem?: IChatItem currentLogModalActiveTab: string showPromptLogModal: boolean @@ -15,7 +14,6 @@ type State = { type Action = { setAppDetail: (appDetail?: App & Partial) => 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(set => ({ appDetail: undefined, setAppDetail: appDetail => set(() => ({ appDetail })), - appSidebarExpand: '', - setAppSidebarExpand: appSidebarExpand => set(() => ({ appSidebarExpand })), currentLogItem: undefined, currentLogModalActiveTab: 'DETAIL', setCurrentLogItem: currentLogItem => set(() => ({ currentLogItem })), diff --git a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx index 5a541087bd6..8d3684c0d03 100644 --- a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx @@ -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' diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index fc3013adfb4..40eb7252746 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -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(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) - const setNeedRefresh = useSetLocalStorage(NEED_REFRESH_APP_LIST_KEY, { raw: true }) + const setNeedRefresh = useSetNeedRefreshAppList() const goStart = async () => { try { diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index fda64f8ccb8..f5ac48c3a46 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -238,14 +238,6 @@ vi.mock('@/service/use-apps', () => ({ }), })) -vi.mock('@/config', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - NEED_REFRESH_APP_LIST_KEY: 'needRefreshAppList', - } -}) - vi.mock('@/hooks/use-pay', () => ({ CheckModal: () => null, })) diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index da150571340..7e2f173ee3f 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -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([]) const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation() const { mutateAsync: mutateToggleAppStar, isPending: isTogglingStar } = useToggleAppStarMutation() - const setNeedRefresh = useSetLocalStorage(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([]) const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation() const { mutateAsync: mutateToggleAppStar, isPending: isTogglingStar } = useToggleAppStarMutation() - const setNeedRefresh = useSetLocalStorage(NEED_REFRESH_APP_LIST_KEY, { raw: true }) + const setNeedRefresh = useSetNeedRefreshAppList() const resourceMaintainer = getAppResourceMaintainer(app) const maintainerPermissionOptions = useMemo(() => ({ currentUserId, diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 3c7e0c8c72c..7af2adc1a05 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -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() - const [needsRefreshAppList, setNeedsRefreshAppList] = useLocalStorage(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) => { diff --git a/web/app/components/apps/storage.ts b/web/app/components/apps/storage.ts new file mode 100644 index 00000000000..bb54f6155dc --- /dev/null +++ b/web/app/components/apps/storage.ts @@ -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(NEED_REFRESH_APP_LIST_KEY, '0', { raw: true }) + +export { + useNeedRefreshAppList, + useSetNeedRefreshAppList, +} diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index c2936a71ee5..98840509e94 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -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( - 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>>(CONVERSATION_ID_INFO, {}) + const [conversationIdInfo, setConversationIdInfo] = useConversationIdInfo() const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || '', [appId, conversationIdInfo, userId]) const handleConversationIdInfoChange = useCallback((changeConversationId: string) => { if (appId) { diff --git a/web/app/components/base/chat/chat/log/__tests__/index.spec.tsx b/web/app/components/base/chat/chat/log/__tests__/index.spec.tsx index b59f439f16d..2235c9db1d9 100644 --- a/web/app/components/base/chat/chat/log/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat/log/__tests__/index.spec.tsx @@ -28,7 +28,6 @@ describe('Log', () => { beforeEach(() => { vi.mocked(useAppStore).mockImplementation(selector => selector({ // State properties - appSidebarExpand: 'expand', currentLogModalActiveTab: 'question', showPromptLogModal: false, showAgentLogModal: false, diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index b9c2df19b3d..34abfad3489 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -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>>(CONVERSATION_ID_INFO, {}) + const [conversationIdInfo, setConversationIdInfo] = useConversationIdInfo() const removeConversationIdInfo = useCallback((appId: string) => { setConversationIdInfo((prev) => { const newInfo = { ...prev } diff --git a/web/app/components/base/chat/storage.ts b/web/app/components/base/chat/storage.ts new file mode 100644 index 00000000000..d31b5778615 --- /dev/null +++ b/web/app/components/base/chat/storage.ts @@ -0,0 +1,19 @@ +import { createLocalStorageState } from 'foxact/create-local-storage-state' +import { CONVERSATION_ID_INFO } from './constants' + +const [ + useConversationIdInfo, + _useConversationIdInfoValue, + _useSetConversationIdInfo, +] = createLocalStorageState>>(CONVERSATION_ID_INFO, {}) + +const [ + useWebAppSidebarCollapseState, + _useWebAppSidebarCollapseStateValue, + _useSetWebAppSidebarCollapseState, +] = createLocalStorageState('webappSidebarCollapse', undefined, { raw: true }) + +export { + useConversationIdInfo, + useWebAppSidebarCollapseState, +} diff --git a/web/app/components/billing/plan/__tests__/index.spec.tsx b/web/app/components/billing/plan/__tests__/index.spec.tsx index 103cd5e8a10..f062acba15e 100644 --- a/web/app/components/billing/plan/__tests__/index.spec.tsx +++ b/web/app/components/billing/plan/__tests__/index.spec.tsx @@ -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', () => ({ diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index 337c88e9597..775f6997f19 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -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 = ({ const canManageBilling = hasPermission(workspacePermissionKeys, BillingPermission.Manage) const { mutateAsync, isPending } = useEducationVerify() const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) - const setEducationVerifying = useSetLocalStorage(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true }) + const setEducationVerifying = useSetEducationVerifying() const unmountedRef = useUnmountedRef() const handleVerify = () => { if (isPending) diff --git a/web/app/components/education-verify-action-recorder.tsx b/web/app/components/education-verify-action-recorder.tsx index ece0569c7e2..03f1d942d35 100644 --- a/web/app/components/education-verify-action-recorder.tsx +++ b/web/app/components/education-verify-action-recorder.tsx @@ -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(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true }) + const setEducationVerifying = useSetEducationVerifying() useEffect(() => { if (searchParams.get('action') === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) diff --git a/web/app/components/explore/app-list/__tests__/index.spec.tsx b/web/app/components/explore/app-list/__tests__/index.spec.tsx index f884e4d2994..0bc33b5a751 100644 --- a/web/app/components/explore/app-list/__tests__/index.spec.tsx +++ b/web/app/components/explore/app-list/__tests__/index.spec.tsx @@ -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 = { diff --git a/web/app/components/explore/learn-dify/atoms.ts b/web/app/components/explore/learn-dify/atoms.ts deleted file mode 100644 index b5400a34ad3..00000000000 --- a/web/app/components/explore/learn-dify/atoms.ts +++ /dev/null @@ -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( - 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) -} diff --git a/web/app/components/explore/learn-dify/index.tsx b/web/app/components/explore/learn-dify/index.tsx index 9fc940735f8..d057b21a215 100644 --- a/web/app/components/explore/learn-dify/index.tsx +++ b/web/app/components/explore/learn-dify/index.tsx @@ -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 setHidden(true)} /> diff --git a/web/app/components/explore/learn-dify/storage.ts b/web/app/components/explore/learn-dify/storage.ts new file mode 100644 index 00000000000..479a4b287a3 --- /dev/null +++ b/web/app/components/explore/learn-dify/storage.ts @@ -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(LEARN_DIFY_HIDDEN_STORAGE_KEY, false) + +export { + useLearnDifyHiddenValue, + useSetLearnDifyHidden, +} diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 5d9524d9fe5..dbadfc1f64c 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -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(EDUCATION_REVERIFY_PREV_EXPIRE_AT_KEY) - const clearEducationReverifyHasNoticed = useSetLocalStorage(EDUCATION_REVERIFY_HAS_NOTICED_KEY) - const clearEducationExpiredHasNoticed = useSetLocalStorage(EDUCATION_EXPIRED_HAS_NOTICED_KEY) + const clearEducationReverifyPrevExpireAt = useSetEducationReverifyPrevExpireAt() + const clearEducationReverifyHasNoticed = useSetEducationReverifyHasNoticed() + const clearEducationExpiredHasNoticed = useSetEducationExpiredHasNoticed() const { mutateAsync: logout } = useLogout() diff --git a/web/app/components/header/header-wrapper.tsx b/web/app/components/header/header-wrapper.tsx index 52c91c92061..efff8432174 100644 --- a/web/app/components/header/header-wrapper.tsx +++ b/web/app/components/header/header-wrapper.tsx @@ -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('workflow-canvas-maximize', false) + const storedHideHeader = useWorkflowCanvasMaximizeValue() const [eventHideHeader, setEventHideHeader] = useState(null) const hideHeader = eventHideHeader ?? storedHideHeader const { eventEmitter } = useEventEmitterContextContext() diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index d177152251c..5c496952a40 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -97,7 +97,7 @@ export function Header() { return (
-
+
{renderLogo()}
/
diff --git a/web/app/components/header/maintenance-notice.tsx b/web/app/components/header/maintenance-notice.tsx index d3a6481f1a5..918c3ce78f4 100644 --- a/web/app/components/header/maintenance-notice.tsx +++ b/web/app/components/header/maintenance-notice.tsx @@ -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('hide-maintenance-notice', '0', { raw: true }) + const [hiddenNoticeValue, setHiddenNoticeValue] = useHideMaintenanceNotice() const hiddenNotice = hiddenNoticeValue === '1' const [closedInSession, setClosedInSession] = useState(false) const showNotice = !hiddenNotice && !closedInSession diff --git a/web/app/components/header/storage.ts b/web/app/components/header/storage.ts new file mode 100644 index 00000000000..0c3b099808d --- /dev/null +++ b/web/app/components/header/storage.ts @@ -0,0 +1,18 @@ +import { createLocalStorageState } from 'foxact/create-local-storage-state' + +const [ + useHideMaintenanceNotice, + _useHideMaintenanceNoticeValue, + _useSetHideMaintenanceNotice, +] = createLocalStorageState('hide-maintenance-notice', '0', { raw: true }) + +const [ + _useWorkflowCanvasMaximize, + useWorkflowCanvasMaximizeValue, + _useSetWorkflowCanvasMaximize, +] = createLocalStorageState('workflow-canvas-maximize', false) + +export { + useHideMaintenanceNotice, + useWorkflowCanvasMaximizeValue, +} diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index 42217d59431..cf68d4cac8a 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -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') diff --git a/web/app/components/main-nav/components/help-menu.tsx b/web/app/components/main-nav/components/help-menu.tsx index 9bf62c6a777..2f74454fd2e 100644 --- a/web/app/components/main-nav/components/help-menu.tsx +++ b/web/app/components/main-nav/components/help-menu.tsx @@ -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 = ({ /> 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', )} > diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx index dbbf00f04ba..6e11df51839 100644 --- a/web/app/components/main-nav/index.tsx +++ b/web/app/components/main-nav/index.tsx @@ -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 = 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(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 diff --git a/web/app/components/main-nav/storage.ts b/web/app/components/main-nav/storage.ts new file mode 100644 index 00000000000..2fb03d23595 --- /dev/null +++ b/web/app/components/main-nav/storage.ts @@ -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(DETAIL_SIDEBAR_STORAGE_KEY, 'expand', { raw: true }) + +export { + useDetailSidebarMode, + useSetDetailSidebarMode, +} diff --git a/web/app/components/signin/__tests__/countdown.spec.tsx b/web/app/components/signin/__tests__/countdown.spec.tsx index 4ac0b437bb6..abd89686f17 100644 --- a/web/app/components/signin/__tests__/countdown.spec.tsx +++ b/web/app/components/signin/__tests__/countdown.spec.tsx @@ -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(() => { diff --git a/web/app/components/signin/countdown.tsx b/web/app/components/signin/countdown.tsx index 627a1eea36d..54c22262d7e 100644 --- a/web/app/components/signin/countdown.tsx +++ b/web/app/components/signin/countdown.tsx @@ -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 (

diff --git a/web/app/components/signin/storage.ts b/web/app/components/signin/storage.ts new file mode 100644 index 00000000000..ae5f8306b42 --- /dev/null +++ b/web/app/components/signin/storage.ts @@ -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(COUNT_DOWN_KEY, undefined, { raw: true }) + +export { + useCountdownLeftTimeValue, + useSetCountdownLeftTime, +} diff --git a/web/app/components/snippets/__tests__/index.spec.tsx b/web/app/components/snippets/__tests__/index.spec.tsx index 9d524b38c9a..977454c1661 100644 --- a/web/app/components/snippets/__tests__/index.spec.tsx +++ b/web/app/components/snippets/__tests__/index.spec.tsx @@ -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, diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 12a70f17e7c..bd50d1abc64 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -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(), []) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins) - const [isCollapsed, setIsCollapsed] = useLocalStorage(STORAGE_KEY, false) + const [isCollapsed, setIsCollapsed] = useFeaturedToolsCollapsed() if (visibleCountPlugins !== plugins) { setVisibleCountPlugins(plugins) diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 917f46074fd..99617c76d27 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -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(), []) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins) - const [isCollapsed, setIsCollapsed] = useLocalStorage(STORAGE_KEY, false) + const [isCollapsed, setIsCollapsed] = useFeaturedTriggersCollapsed() if (visibleCountPlugins !== plugins) { setVisibleCountPlugins(plugins) diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index f7def41acd5..62ef354c35f 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -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> } -const STORAGE_KEY = 'workflow_rag_recommendations_collapsed' - const RAGToolRecommendations = ({ viewType, onSelect, onTagsChange, }: RAGToolRecommendationsProps) => { const { t } = useTranslation() - const [isCollapsed, setIsCollapsed] = useLocalStorage(STORAGE_KEY, false) + const [isCollapsed, setIsCollapsed] = useRAGRecommendationsCollapsed() const { data: ragRecommendedPlugins, diff --git a/web/app/components/workflow/block-selector/storage.ts b/web/app/components/workflow/block-selector/storage.ts new file mode 100644 index 00000000000..bddac31846f --- /dev/null +++ b/web/app/components/workflow/block-selector/storage.ts @@ -0,0 +1,25 @@ +import { createLocalStorageState } from 'foxact/create-local-storage-state' + +const [ + useFeaturedToolsCollapsed, + _useFeaturedToolsCollapsedValue, + _useSetFeaturedToolsCollapsed, +] = createLocalStorageState('workflow_tools_featured_collapsed', false) + +const [ + useFeaturedTriggersCollapsed, + _useFeaturedTriggersCollapsedValue, + _useSetFeaturedTriggersCollapsed, +] = createLocalStorageState('workflow_triggers_featured_collapsed', false) + +const [ + useRAGRecommendationsCollapsed, + _useRAGRecommendationsCollapsedValue, + _useSetRAGRecommendationsCollapsed, +] = createLocalStorageState('workflow_rag_recommendations_collapsed', false) + +export { + useFeaturedToolsCollapsed, + useFeaturedTriggersCollapsed, + useRAGRecommendationsCollapsed, +} diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 7a420b258e7..f30c428890e 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -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 = ({ const setNodePanelWidth = useStore(s => s.setNodePanelWidth) const pendingSingleRun = useStore(s => s.pendingSingleRun) const setPendingSingleRun = useStore(s => s.setPendingSingleRun) - const setNodePanelWidthStorage = useSetLocalStorage('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 = ({ const newValue = clampNodePanelWidth(width, maxNodePanelWidth) if (source === 'user') - setNodePanelWidthStorage(`${newValue}`) + setNodePanelWidthStorage(newValue) setNodePanelWidth(newValue) }, [maxNodePanelWidth, setNodePanelWidth, setNodePanelWidthStorage]) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx index 2f2c3469794..84177e9ade8 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx @@ -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 = ({ onApply, crossAxisOffset, }) => { const [open, setOpen] = useState(false) const [view, setView] = useState(GENERATOR_VIEWS.promptEditor) - const [model, setModel] = useState(() => getStoredModel()) + const [model, setModel] = useAutoGenModel() const [instruction, setInstruction] = useState('') const [schema, setSchema] = useState(null) const { theme } = useTheme() @@ -102,8 +91,7 @@ const JsonSchemaGenerator: FC = ({ 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 = ({ 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() diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index 672536aabed..d81392ef19c 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -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 = ({ const [shouldScrollToEnd, setShouldScrollToEnd] = useState(false) const prevListLength = useRef(list.length) const [collapsed, setCollapsed] = useState(false) - const [storedRenameHintDismissed, setIsRenameHintDismissed] = useLocalStorage(INLINE_LABEL_HINT_STORAGE_KEY) + const [storedRenameHintDismissed, setIsRenameHintDismissed] = useInlineLabelHintDismissed() const isRenameHintDismissed = storedRenameHintDismissed ?? false const handleClassChange = useCallback((index: number) => { diff --git a/web/app/components/workflow/nodes/question-classifier/storage.ts b/web/app/components/workflow/nodes/question-classifier/storage.ts new file mode 100644 index 00000000000..e13a5436c7e --- /dev/null +++ b/web/app/components/workflow/nodes/question-classifier/storage.ts @@ -0,0 +1,11 @@ +import { createLocalStorageState } from 'foxact/create-local-storage-state' + +const [ + useInlineLabelHintDismissed, + _useInlineLabelHintDismissedValue, + _useSetInlineLabelHintDismissed, +] = createLocalStorageState('question-classifier-inline-label-hint-dismissed') + +export { + useInlineLabelHintDismissed, +} diff --git a/web/app/components/workflow/note-node/hooks.ts b/web/app/components/workflow/note-node/hooks.ts index 8a9e3573eba..0760697179d 100644 --- a/web/app/components/workflow/note-node/hooks.ts +++ b/web/app/components/workflow/note-node/hooks.ts @@ -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(NOTE_SHOW_AUTHOR_STORAGE_KEY, { raw: true }) + const setShowAuthorStorage = useSetWorkflowNoteShowAuthor() const handleThemeChange = useCallback((theme: NoteTheme) => { handleNodeDataUpdateWithSyncDraft({ id, data: { theme } }) diff --git a/web/app/components/workflow/operator/hooks.ts b/web/app/components/workflow/operator/hooks.ts index 4330920d501..8c7fd0f15e3 100644 --- a/web/app/components/workflow/operator/hooks.ts +++ b/web/app/components/workflow/operator/hooks.ts @@ -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(NOTE_SHOW_AUTHOR_STORAGE_KEY, 'true', { raw: true }) + const showAuthorStorage = useWorkflowNoteShowAuthorValue() const handleAddNote = useCallback(() => { const { newNode } = generateNewNode({ diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index 3f0576d8703..ff99f10505c 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -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('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(() => { diff --git a/web/app/components/workflow/persistence/local-storage-bridge.tsx b/web/app/components/workflow/persistence/local-storage-bridge.tsx index 437917162e1..31031e5348b 100644 --- a/web/app/components/workflow/persistence/local-storage-bridge.tsx +++ b/web/app/components/workflow/persistence/local-storage-bridge.tsx @@ -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(WORKFLOW_NODE_PANEL_WIDTH_KEY, undefined, numberStorageOptions) - const [storedPreviewPanelWidth] = useLocalStorage(WORKFLOW_PREVIEW_PANEL_WIDTH_KEY, undefined, numberStorageOptions) - const [storedVariableInspectPanelHeight] = useLocalStorage(WORKFLOW_VARIABLE_INSPECT_PANEL_HEIGHT_KEY, undefined, numberStorageOptions) - const [storedControlMode] = useLocalStorage(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(WORKFLOW_OPERATION_MODE_KEY, rawStorageOptions) - useIsoLayoutEffect(() => { if (!isFiniteNumber(storedNodePanelWidth)) return diff --git a/web/app/components/workflow/persistence/local-storage-options.ts b/web/app/components/workflow/persistence/local-storage-options.ts index af953f8287e..1cf90d806f0 100644 --- a/web/app/components/workflow/persistence/local-storage-options.ts +++ b/web/app/components/workflow/persistence/local-storage-options.ts @@ -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(WORKFLOW_NODE_PANEL_WIDTH_KEY, undefined, numberStorageOptions) + +const [ + _useDebugPreviewPanelWidth, + useDebugPreviewPanelWidthValue, + useSetDebugPreviewPanelWidth, +] = createLocalStorageState(WORKFLOW_PREVIEW_PANEL_WIDTH_KEY, undefined, numberStorageOptions) + +const [ + _useWorkflowVariableInspectPanelHeight, + useWorkflowVariableInspectPanelHeightValue, + useSetWorkflowVariableInspectPanelHeight, +] = createLocalStorageState(WORKFLOW_VARIABLE_INSPECT_PANEL_HEIGHT_KEY, undefined, numberStorageOptions) + +const [ + useWorkflowOperationMode, + _useWorkflowOperationModeValue, + _useSetWorkflowOperationMode, +] = createLocalStorageState(WORKFLOW_OPERATION_MODE_KEY, undefined, rawStorageOptions) + +const [ + _useWorkflowNoteShowAuthor, + useWorkflowNoteShowAuthorValue, + useSetWorkflowNoteShowAuthor, +] = createLocalStorageState(NOTE_SHOW_AUTHOR_STORAGE_KEY, 'true', rawStorageOptions) + +export { + useDebugPreviewPanelWidthValue, + useSetDebugPreviewPanelWidth, + useSetWorkflowNodePanelWidth, + useSetWorkflowNoteShowAuthor, + useSetWorkflowVariableInspectPanelHeight, + useWorkflowNodePanelWidthValue, + useWorkflowNoteShowAuthorValue, + useWorkflowOperationMode, + useWorkflowVariableInspectPanelHeightValue, +} diff --git a/web/app/components/workflow/variable-inspect/index.tsx b/web/app/components/workflow/variable-inspect/index.tsx index 924ad7d07c0..8d5b500bca6 100644 --- a/web/app/components/workflow/variable-inspect/index.tsx +++ b/web/app/components/workflow/variable-inspect/index.tsx @@ -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('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]) diff --git a/web/app/components/workflow/workflow-generator/index.tsx b/web/app/components/workflow/workflow-generator/index.tsx index 102b6de08b7..fade26f0482 100644 --- a/web/app/components/workflow/workflow-generator/index.tsx +++ b/web/app/components/workflow/workflow-generator/index.tsx @@ -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) => (

@@ -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(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]) diff --git a/web/app/components/workflow/workflow-generator/storage.ts b/web/app/components/workflow/workflow-generator/storage.ts new file mode 100644 index 00000000000..500fb8d43fd --- /dev/null +++ b/web/app/components/workflow/workflow-generator/storage.ts @@ -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('workflow-gen-model', EMPTY_WORKFLOW_GENERATOR_MODEL) + +export { + useWorkflowGeneratorModel, +} diff --git a/web/app/education-apply/constants.ts b/web/app/education-apply/constants.ts index cfe80556c6e..cb0c339af3a 100644 --- a/web/app/education-apply/constants.ts +++ b/web/app/education-apply/constants.ts @@ -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' diff --git a/web/app/education-apply/education-apply-page.tsx b/web/app/education-apply/education-apply-page.tsx index 61f232b5739..61ae39b3ac7 100644 --- a/web/app/education-apply/education-apply-page.tsx +++ b/web/app/education-apply/education-apply-page.tsx @@ -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(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true }) + const setEducationVerifying = useSetEducationVerifying() const searchParams = useSearchParams() const token = searchParams.get('token') diff --git a/web/app/education-apply/hooks.ts b/web/app/education-apply/hooks.ts index ae216feebc2..d4a1e6fa274 100644 --- a/web/app/education-apply/hooks.ts +++ b/web/app/education-apply/hooks.ts @@ -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('education-reverify-prev-expire-at', 0) - const [reverifyHasNoticed, setReverifyHasNoticed] = useLocalStorage('education-reverify-has-noticed', false) - const [expiredHasNoticed, setExpiredHasNoticed] = useLocalStorage('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(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'no', { raw: true }) + const [educationVerifying, setEducationVerifying] = useEducationVerifying() const searchParams = useSearchParams() const educationVerifyAction = searchParams.get('action') diff --git a/web/app/education-apply/storage.ts b/web/app/education-apply/storage.ts new file mode 100644 index 00000000000..209045b087e --- /dev/null +++ b/web/app/education-apply/storage.ts @@ -0,0 +1,38 @@ +import { createLocalStorageState } from 'foxact/create-local-storage-state' + +const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying' + +const [ + useEducationVerifying, + _useEducationVerifyingValue, + useSetEducationVerifying, +] = createLocalStorageState(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'no', { raw: true }) + +const [ + useEducationReverifyPrevExpireAt, + _useEducationReverifyPrevExpireAtValue, + useSetEducationReverifyPrevExpireAt, +] = createLocalStorageState('education-reverify-prev-expire-at', 0) + +const [ + useEducationReverifyHasNoticed, + _useEducationReverifyHasNoticedValue, + useSetEducationReverifyHasNoticed, +] = createLocalStorageState('education-reverify-has-noticed', false) + +const [ + useEducationExpiredHasNoticed, + _useEducationExpiredHasNoticedValue, + useSetEducationExpiredHasNoticed, +] = createLocalStorageState('education-expired-has-noticed', false) + +export { + useEducationExpiredHasNoticed, + useEducationReverifyHasNoticed, + useEducationReverifyPrevExpireAt, + useEducationVerifying, + useSetEducationExpiredHasNoticed, + useSetEducationReverifyHasNoticed, + useSetEducationReverifyPrevExpireAt, + useSetEducationVerifying, +} diff --git a/web/app/reset-password/page.tsx b/web/app/reset-password/page.tsx index e15d87034c8..25ccfecb7b7 100644 --- a/web/app/reset-password/page.tsx +++ b/web/app/reset-password/page.tsx @@ -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)) diff --git a/web/app/signin/components/mail-and-code-auth.tsx b/web/app/signin/components/mail-and-code-auth.tsx index 2c013b868bc..98a24c44ccc 100644 --- a/web/app/signin/components/mail-and-code-auth.tsx +++ b/web/app/signin/components/mail-and-code-auth.tsx @@ -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(COUNT_DOWN_KEY, { raw: true }) + const setCountdownLeftTime = useSetCountdownLeftTime() const handleGetEMailVerificationCode = async () => { try { diff --git a/web/config/index.ts b/web/config/index.ts index dcd38229400..5b325a267e3 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -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, diff --git a/web/context/modal-context-provider.tsx b/web/context/modal-context-provider.tsx index 7f22cddfa1b..2c2d9ad185d 100644 --- a/web/context/modal-context-provider.tsx +++ b/web/context/modal-context-provider.tsx @@ -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 | null>(null) const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState | null>(null) const { currentWorkspace } = useAppContext() - const setEducationVerifying = useSetLocalStorage(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true }) + const setEducationVerifying = useSetEducationVerifying() const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false) const handleCancelAccountSettingModal = () => { diff --git a/web/context/modal-context.test.tsx b/web/context/modal-context.test.tsx index 7feaedd4fa3..cd54fcd41a9 100644 --- a/web/context/modal-context.test.tsx +++ b/web/context/modal-context.test.tsx @@ -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() diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index 5d52abedf8f..fb9706cc521 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -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>): MemberInviteLimit => { if (!data) return unlimitedMemberInviteLimit @@ -158,11 +156,7 @@ export const ProviderContextProvider = ({ // #endregion Zendesk conversation fields const { t } = useTranslation() - const [anthropicQuotaNotice, setAnthropicQuotaNotice] = useLocalStorage( - ANTHROPIC_QUOTA_NOTICE_STORAGE_KEY, - 'false', - { raw: true }, - ) + const [anthropicQuotaNotice, setAnthropicQuotaNotice] = useAnthropicQuotaNotice() useEffect(() => { if (anthropicQuotaNotice === 'true') diff --git a/web/context/provider-storage.ts b/web/context/provider-storage.ts new file mode 100644 index 00000000000..7bd951d959a --- /dev/null +++ b/web/context/provider-storage.ts @@ -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(ANTHROPIC_QUOTA_NOTICE_STORAGE_KEY, 'false', { raw: true }) + +export { + useAnthropicQuotaNotice, +} diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 3a9af0fafcd..1b4afefef6e 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -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.', }, ], }, diff --git a/web/hooks/use-import-dsl.ts b/web/hooks/use-import-dsl.ts index 387fb2c37df..97a1f9e93f2 100644 --- a/web/hooks/use-import-dsl.ts +++ b/web/hooks/use-import-dsl.ts @@ -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('') - const setNeedRefresh = useSetLocalStorage(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,