diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 1c5434924f..f2a10573f2 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -18,6 +18,7 @@ import { useStore } from '@/app/components/app/store' import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline' import Loading from '@/app/components/base/loading' import ExtraInfo from '@/app/components/datasets/extra-info' +import { STORAGE_KEYS } from '@/config/storage-keys' import { useAppContext } from '@/context/app-context' import DatasetDetailContext from '@/context/dataset-detail' import { useEventEmitterContextContext } from '@/context/event-emitter' @@ -25,6 +26,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useDocumentTitle from '@/hooks/use-document-title' import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' import { cn } from '@/utils/classnames' +import { storage } from '@/utils/storage' export type IAppDetailLayoutProps = { children: React.ReactNode @@ -40,7 +42,7 @@ const DatasetDetailLayout: FC = (props) => { const pathname = usePathname() const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-pipeline') const isPipelineCanvas = pathname.endsWith('/pipeline') - const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' + const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false) ?? false const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) const { eventEmitter } = useEventEmitterContextContext() diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index afc6bd0f13..48a0cabe78 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -5,9 +5,11 @@ import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useShallow } from 'zustand/react/shallow' import { useStore as useAppStore } from '@/app/components/app/store' +import { STORAGE_KEYS } from '@/config/storage-keys' import { useEventEmitterContextContext } from '@/context/event-emitter' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { cn } from '@/utils/classnames' +import { storage } from '@/utils/storage' import Divider from '../base/divider' import { getKeyboardKeyCodeBySystem } from '../workflow/utils' import AppInfo from './app-info' @@ -53,7 +55,7 @@ const AppDetailNav = ({ const pathname = usePathname() const inWorkflowCanvas = pathname.endsWith('/workflow') const isPipelineCanvas = pathname.endsWith('/pipeline') - const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' + const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false) ?? false const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) const { eventEmitter } = useEventEmitterContextContext() @@ -64,7 +66,7 @@ const AppDetailNav = ({ useEffect(() => { if (appSidebarExpand) { - localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand) + storage.set(STORAGE_KEYS.APP.DETAIL_COLLAPSE, appSidebarExpand) setAppSidebarExpand(appSidebarExpand) } }, [appSidebarExpand, setAppSidebarExpand]) diff --git a/web/app/components/header/header-wrapper.tsx b/web/app/components/header/header-wrapper.tsx index 1b81c1152c..96616392cf 100644 --- a/web/app/components/header/header-wrapper.tsx +++ b/web/app/components/header/header-wrapper.tsx @@ -2,8 +2,10 @@ import { usePathname } from 'next/navigation' import * as React from 'react' import { useState } from 'react' +import { STORAGE_KEYS } from '@/config/storage-keys' import { useEventEmitterContextContext } from '@/context/event-emitter' import { cn } from '@/utils/classnames' +import { storage } from '@/utils/storage' import s from './index.module.css' type HeaderWrapperProps = { @@ -18,7 +20,7 @@ const HeaderWrapper = ({ // Check if the current path is a workflow canvas & fullscreen const inWorkflowCanvas = pathname.endsWith('/workflow') const isPipelineCanvas = pathname.endsWith('/pipeline') - const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' + const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false) ?? false const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) const { eventEmitter } = useEventEmitterContextContext() diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index 7a58581a99..05e4824cc9 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -5,7 +5,9 @@ import { useCallback, } from 'react' import { useReactFlow, useStoreApi } from 'reactflow' +import { STORAGE_KEYS } from '@/config/storage-keys' import { useEventEmitterContextContext } from '@/context/event-emitter' +import { storage } from '@/utils/storage' import { CUSTOM_NODE, NODE_LAYOUT_HORIZONTAL_PADDING, @@ -342,7 +344,7 @@ export const useWorkflowCanvasMaximize = () => { return setMaximizeCanvas(!maximizeCanvas) - localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas)) + storage.set(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, !maximizeCanvas) eventEmitter?.emit({ type: 'workflow-canvas-maximize', payload: !maximizeCanvas, 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 c834f29ab3..5c9b26abe2 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 @@ -59,12 +59,14 @@ import { hasRetryNode, isSupportCustomRunForm, } from '@/app/components/workflow/utils' +import { STORAGE_KEYS } from '@/config/storage-keys' import { useModalContext } from '@/context/modal-context' import { useAllBuiltInTools } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { FlowType } from '@/types/common' import { canFindTool } from '@/utils' import { cn } from '@/utils/classnames' +import { storage } from '@/utils/storage' import { useResizePanel } from '../../hooks/use-resize-panel' import BeforeRunForm from '../before-run-form' import PanelWrap from '../before-run-form/panel-wrap' @@ -137,7 +139,7 @@ const BasePanel: FC = ({ const newValue = Math.max(400, Math.min(width, maxNodePanelWidth)) if (source === 'user') - localStorage.setItem('workflow-node-panel-width', `${newValue}`) + storage.set(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, newValue) setNodePanelWidth(newValue) }, [maxNodePanelWidth, setNodePanelWidth]) 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 bc5116bc65..8a0f443fee 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -18,7 +18,9 @@ import Tooltip from '@/app/components/base/tooltip' import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' import { useStore } from '@/app/components/workflow/store' +import { STORAGE_KEYS } from '@/config/storage-keys' import { cn } from '@/utils/classnames' +import { storage } from '@/utils/storage' import { useWorkflowInteractions, } from '../../hooks' @@ -56,7 +58,7 @@ const DebugAndPreview = () => { const setPanelWidth = useStore(s => s.setPreviewPanelWidth) const handleResize = useCallback((width: number, source: 'user' | 'system' = 'user') => { if (source === 'user') - localStorage.setItem('debug-and-preview-panel-width', `${width}`) + storage.set(STORAGE_KEYS.WORKFLOW.PREVIEW_PANEL_WIDTH, width) setPanelWidth(width) }, [setPanelWidth]) const maxPanelWidth = useMemo(() => { diff --git a/web/app/components/workflow/store/workflow/layout-slice.ts b/web/app/components/workflow/store/workflow/layout-slice.ts index 91c80ba84a..3380c6cf27 100644 --- a/web/app/components/workflow/store/workflow/layout-slice.ts +++ b/web/app/components/workflow/store/workflow/layout-slice.ts @@ -1,11 +1,12 @@ import type { StateCreator } from 'zustand' +import { STORAGE_KEYS } from '@/config/storage-keys' +import { storage } from '@/utils/storage' export type LayoutSliceShape = { workflowCanvasWidth?: number workflowCanvasHeight?: number setWorkflowCanvasWidth: (width: number) => void setWorkflowCanvasHeight: (height: number) => void - // rightPanelWidth - otherPanelWidth = nodePanelWidth rightPanelWidth?: number setRightPanelWidth: (width: number) => void nodePanelWidth: number @@ -14,11 +15,11 @@ export type LayoutSliceShape = { setPreviewPanelWidth: (width: number) => void otherPanelWidth: number setOtherPanelWidth: (width: number) => void - bottomPanelWidth: number // min-width = 400px; default-width = auto || 480px; + bottomPanelWidth: number setBottomPanelWidth: (width: number) => void bottomPanelHeight: number setBottomPanelHeight: (height: number) => void - variableInspectPanelHeight: number // min-height = 120px; default-height = 320px; + variableInspectPanelHeight: number setVariableInspectPanelHeight: (height: number) => void maximizeCanvas: boolean setMaximizeCanvas: (maximize: boolean) => void @@ -31,9 +32,9 @@ export const createLayoutSlice: StateCreator = set => ({ setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })), rightPanelWidth: undefined, setRightPanelWidth: width => set(() => ({ rightPanelWidth: width })), - nodePanelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 400, + nodePanelWidth: storage.getNumber(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, 400)!, setNodePanelWidth: width => set(() => ({ nodePanelWidth: width })), - previewPanelWidth: localStorage.getItem('debug-and-preview-panel-width') ? Number.parseFloat(localStorage.getItem('debug-and-preview-panel-width')!) : 400, + previewPanelWidth: storage.getNumber(STORAGE_KEYS.WORKFLOW.PREVIEW_PANEL_WIDTH, 400)!, setPreviewPanelWidth: width => set(() => ({ previewPanelWidth: width })), otherPanelWidth: 400, setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })), @@ -41,8 +42,8 @@ export const createLayoutSlice: StateCreator = set => ({ setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })), bottomPanelHeight: 324, setBottomPanelHeight: height => set(() => ({ bottomPanelHeight: height })), - variableInspectPanelHeight: localStorage.getItem('workflow-variable-inpsect-panel-height') ? Number.parseFloat(localStorage.getItem('workflow-variable-inpsect-panel-height')!) : 320, + variableInspectPanelHeight: storage.getNumber(STORAGE_KEYS.WORKFLOW.VARIABLE_INSPECT_PANEL_HEIGHT, 320)!, setVariableInspectPanelHeight: height => set(() => ({ variableInspectPanelHeight: height })), - maximizeCanvas: localStorage.getItem('workflow-canvas-maximize') === 'true', + maximizeCanvas: storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)!, setMaximizeCanvas: maximize => set(() => ({ maximizeCanvas: maximize })), }) diff --git a/web/app/components/workflow/store/workflow/panel-slice.ts b/web/app/components/workflow/store/workflow/panel-slice.ts index 4848beeac5..83db26e490 100644 --- a/web/app/components/workflow/store/workflow/panel-slice.ts +++ b/web/app/components/workflow/store/workflow/panel-slice.ts @@ -1,4 +1,6 @@ import type { StateCreator } from 'zustand' +import { STORAGE_KEYS } from '@/config/storage-keys' +import { storage } from '@/utils/storage' export type PanelSliceShape = { panelWidth: number @@ -27,7 +29,7 @@ export type PanelSliceShape = { } export const createPanelSlice: StateCreator = set => ({ - panelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420, + panelWidth: storage.getNumber(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, 420) ?? 420, showFeaturesPanel: false, setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })), showWorkflowVersionHistoryPanel: false, diff --git a/web/app/components/workflow/store/workflow/persist-config.ts b/web/app/components/workflow/store/workflow/persist-config.ts new file mode 100644 index 0000000000..b560f2b832 --- /dev/null +++ b/web/app/components/workflow/store/workflow/persist-config.ts @@ -0,0 +1,8 @@ +import type { StateStorage } from 'zustand/middleware' +import { storage } from '@/utils/storage' + +export const createZustandStorage = (): StateStorage => ({ + getItem: (name: string) => storage.get(name), + setItem: storage.set, + removeItem: storage.remove, +}) diff --git a/web/app/components/workflow/store/workflow/workflow-slice.ts b/web/app/components/workflow/store/workflow/workflow-slice.ts index df24058975..62b6310af3 100644 --- a/web/app/components/workflow/store/workflow/workflow-slice.ts +++ b/web/app/components/workflow/store/workflow/workflow-slice.ts @@ -5,6 +5,8 @@ import type { WorkflowRunningData, } from '@/app/components/workflow/types' import type { FileUploadConfigResponse } from '@/models/common' +import { STORAGE_KEYS } from '@/config/storage-keys' +import { storage } from '@/utils/storage' type PreviewRunningData = WorkflowRunningData & { resultTabActive?: boolean @@ -63,10 +65,10 @@ export const createWorkflowSlice: StateCreator = set => ({ setSelection: selection => set(() => ({ selection })), bundleNodeSize: null, setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })), - controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : 'hand', + controlMode: storage.get<'pointer' | 'hand'>(STORAGE_KEYS.WORKFLOW.OPERATION_MODE) === 'pointer' ? 'pointer' : 'hand', setControlMode: (controlMode) => { set(() => ({ controlMode })) - localStorage.setItem('workflow-operation-mode', controlMode) + storage.set(STORAGE_KEYS.WORKFLOW.OPERATION_MODE, controlMode) }, mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, setMousePosition: mousePosition => set(() => ({ mousePosition })), diff --git a/web/config/index.ts b/web/config/index.ts index 08ce14b264..1483f684be 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -1,5 +1,6 @@ import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' import { InputVarType } from '@/app/components/workflow/types' +import { STORAGE_KEYS } from '@/config/storage-keys' import { PromptRole } from '@/models/debug' import { PipelineInputVarType } from '@/models/pipeline' import { AgentStrategy } from '@/types/app' @@ -179,7 +180,7 @@ export const CSRF_COOKIE_NAME = () => { return isSecure ? '__Host-csrf_token' : 'csrf_token' } export const CSRF_HEADER_NAME = 'X-CSRF-Token' -export const ACCESS_TOKEN_LOCAL_STORAGE_NAME = 'access_token' +export const ACCESS_TOKEN_LOCAL_STORAGE_NAME = STORAGE_KEYS.AUTH.ACCESS_TOKEN export const PASSPORT_LOCAL_STORAGE_NAME = (appCode: string) => `passport-${appCode}` export const PASSPORT_HEADER_NAME = 'X-App-Passport' @@ -229,7 +230,7 @@ export const VAR_ITEM_TEMPLATE_IN_PIPELINE = { export const appDefaultIconBackground = '#D5F5F6' -export const NEED_REFRESH_APP_LIST_KEY = 'needRefreshAppList' +export const NEED_REFRESH_APP_LIST_KEY = STORAGE_KEYS.APP.NEED_REFRESH_LIST export const DATASET_DEFAULT = { top_k: 4, diff --git a/web/config/storage-keys.ts b/web/config/storage-keys.ts new file mode 100644 index 0000000000..c203cb4057 --- /dev/null +++ b/web/config/storage-keys.ts @@ -0,0 +1,35 @@ +export const STORAGE_KEYS = { + WORKFLOW: { + NODE_PANEL_WIDTH: 'workflow-node-panel-width', + PREVIEW_PANEL_WIDTH: 'debug-and-preview-panel-width', + VARIABLE_INSPECT_PANEL_HEIGHT: 'workflow-variable-inspect-panel-height', + CANVAS_MAXIMIZE: 'workflow-canvas-maximize', + OPERATION_MODE: 'workflow-operation-mode', + }, + APP: { + SIDEBAR_COLLAPSE: 'webappSidebarCollapse', + NEED_REFRESH_LIST: 'needRefreshAppList', + DETAIL_COLLAPSE: 'app-detail-collapse-or-expand', + }, + CONVERSATION: { + ID_INFO: 'conversationIdInfo', + }, + AUTH: { + ACCESS_TOKEN: 'access_token', + REFRESH_LOCK: 'is_other_tab_refreshing', + LAST_REFRESH_TIME: 'last_refresh_time', + }, + EDUCATION: { + VERIFYING: 'educationVerifying', + REVERIFY_PREV_EXPIRE_AT: 'education-reverify-prev-expire-at', + REVERIFY_HAS_NOTICED: 'education-reverify-has-noticed', + EXPIRED_HAS_NOTICED: 'education-expired-has-noticed', + }, + CONFIG: { + AUTO_GEN_MODEL: 'auto-gen-model', + DEBUG_MODELS: 'app-debug-with-single-or-multiple-models', + SETUP_STATUS: 'setup_status', + }, +} as const + +export type StorageKeys = typeof STORAGE_KEYS diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index b043a2d951..019090b508 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -56,6 +56,9 @@ "no-console": { "count": 16 }, + "no-restricted-properties": { + "count": 5 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 4 }, @@ -74,6 +77,9 @@ } }, "app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx": { + "no-restricted-globals": { + "count": 1 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 2 }, @@ -81,6 +87,11 @@ "count": 1 } }, + "app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx": { "react-hooks/preserve-manual-memoization": { "count": 1 @@ -103,6 +114,9 @@ } }, "app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 1 } @@ -117,12 +131,25 @@ "count": 1 } }, + "app/(shareLayout)/webapp-reset-password/page.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, + "app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx": { "ts/no-explicit-any": { "count": 2 } }, "app/account/(commonLayout)/account-page/email-change-modal.tsx": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 5 } @@ -132,6 +159,11 @@ "count": 1 } }, + "app/account/(commonLayout)/avatar.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/account/(commonLayout)/delete-account/components/feed-back.tsx": { "react-hooks/preserve-manual-memoization": { "count": 1 @@ -142,17 +174,33 @@ "count": 1 } }, + "app/account/(commonLayout)/delete-account/index.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/account/oauth/authorize/layout.tsx": { "ts/no-explicit-any": { "count": 1 } }, "app/account/oauth/authorize/page.tsx": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 1 } }, + "app/components/app-initializer.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/components/app-sidebar/app-info.tsx": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 1 } @@ -353,6 +401,9 @@ } }, "app/components/app/configuration/config/automatic/get-automatic-res.tsx": { + "no-restricted-globals": { + "count": 6 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 4 }, @@ -381,6 +432,9 @@ } }, "app/components/app/configuration/config/code-generator/get-code-generator-res.tsx": { + "no-restricted-globals": { + "count": 6 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 4 }, @@ -485,6 +539,9 @@ } }, "app/components/app/configuration/debug/hooks.tsx": { + "no-restricted-globals": { + "count": 2 + }, "react-hooks/refs": { "count": 7 }, @@ -541,12 +598,23 @@ "count": 1 } }, + "app/components/app/create-app-dialog/app-list/index.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/components/app/create-app-modal/index.spec.tsx": { + "no-restricted-properties": { + "count": 1 + }, "ts/no-explicit-any": { "count": 7 } }, "app/components/app/create-app-modal/index.tsx": { + "no-restricted-globals": { + "count": 1 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 }, @@ -555,6 +623,9 @@ } }, "app/components/app/create-from-dsl-modal/index.tsx": { + "no-restricted-globals": { + "count": 2 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 2 } @@ -620,11 +691,17 @@ } }, "app/components/app/switch-app-modal/index.spec.tsx": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 1 } }, "app/components/app/switch-app-modal/index.tsx": { + "no-restricted-globals": { + "count": 1 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 } @@ -663,6 +740,9 @@ } }, "app/components/apps/app-card.tsx": { + "no-restricted-globals": { + "count": 1 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 }, @@ -674,11 +754,17 @@ } }, "app/components/apps/list.spec.tsx": { + "no-restricted-globals": { + "count": 3 + }, "ts/no-explicit-any": { "count": 9 } }, "app/components/apps/list.tsx": { + "no-restricted-globals": { + "count": 2 + }, "unused-imports/no-unused-vars": { "count": 1 } @@ -806,7 +892,15 @@ "count": 2 } }, + "app/components/base/chat/chat-with-history/hooks.spec.tsx": { + "no-restricted-globals": { + "count": 4 + } + }, "app/components/base/chat/chat-with-history/hooks.tsx": { + "no-restricted-globals": { + "count": 2 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 4 }, @@ -907,6 +1001,11 @@ "count": 7 } }, + "app/components/base/chat/embedded-chatbot/hooks.spec.tsx": { + "no-restricted-globals": { + "count": 3 + } + }, "app/components/base/chat/embedded-chatbot/hooks.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 6 @@ -1671,7 +1770,15 @@ "count": 4 } }, + "app/components/billing/plan/index.spec.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/components/billing/plan/index.tsx": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 2 } @@ -1701,6 +1808,11 @@ "count": 1 } }, + "app/components/browser-initializer.tsx": { + "no-restricted-properties": { + "count": 2 + } + }, "app/components/custom/custom-web-app-brand/index.spec.tsx": { "ts/no-explicit-any": { "count": 7 @@ -2112,6 +2224,9 @@ } }, "app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts": { + "no-restricted-globals": { + "count": 2 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 } @@ -2136,6 +2251,11 @@ "count": 1 } }, + "app/components/datasets/metadata/metadata-document/info-group.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/components/datasets/settings/form/index.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 2 @@ -2244,6 +2364,11 @@ "count": 9 } }, + "app/components/header/account-dropdown/index.tsx": { + "no-restricted-globals": { + "count": 4 + } + }, "app/components/header/account-setting/data-source-page-new/card.tsx": { "react-hooks/immutability": { "count": 1 @@ -2459,6 +2584,11 @@ "count": 1 } }, + "app/components/header/maintenance-notice.tsx": { + "no-restricted-globals": { + "count": 2 + } + }, "app/components/header/nav/nav-selector/index.tsx": { "react-hooks/use-memo": { "count": 1 @@ -2999,6 +3129,11 @@ "count": 2 } }, + "app/components/signin/countdown.tsx": { + "no-restricted-globals": { + "count": 4 + } + }, "app/components/tools/edit-custom-collection-modal/get-schema.tsx": { "ts/no-explicit-any": { "count": 1 @@ -3198,6 +3333,9 @@ } }, "app/components/workflow/block-selector/featured-tools.tsx": { + "no-restricted-properties": { + "count": 3 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 2 }, @@ -3206,6 +3344,9 @@ } }, "app/components/workflow/block-selector/featured-triggers.tsx": { + "no-restricted-properties": { + "count": 3 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 2 }, @@ -3224,6 +3365,9 @@ } }, "app/components/workflow/block-selector/rag-tool-recommendations/index.tsx": { + "no-restricted-properties": { + "count": 3 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 } @@ -3540,6 +3684,11 @@ "count": 2 } }, + "app/components/workflow/nodes/_base/components/workflow-panel/index.spec.tsx": { + "no-restricted-globals": { + "count": 18 + } + }, "app/components/workflow/nodes/_base/components/workflow-panel/index.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 3 @@ -3938,6 +4087,9 @@ } }, "app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx": { + "no-restricted-globals": { + "count": 6 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 2 } @@ -4359,6 +4511,11 @@ "count": 7 } }, + "app/components/workflow/panel/debug-and-preview/index.spec.tsx": { + "no-restricted-globals": { + "count": 15 + } + }, "app/components/workflow/panel/env-panel/variable-modal.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 4 @@ -4627,7 +4784,15 @@ "count": 1 } }, + "app/education-apply/education-apply-page.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/education-apply/hooks.ts": { + "no-restricted-globals": { + "count": 2 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 5 } @@ -4637,6 +4802,11 @@ "count": 1 } }, + "app/education-apply/user-info.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/education-apply/verify-state-modal.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 @@ -4653,15 +4823,33 @@ } }, "app/install/installForm.spec.tsx": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 7 } }, + "app/install/installForm.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/reset-password/layout.tsx": { "ts/no-explicit-any": { "count": 1 } }, + "app/reset-password/page.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, + "app/signin/components/mail-and-code-auth.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "app/signin/components/mail-and-password-auth.tsx": { "ts/no-explicit-any": { "count": 1 @@ -4682,6 +4870,11 @@ "count": 1 } }, + "app/signin/utils/post-login-redirect.ts": { + "no-restricted-globals": { + "count": 3 + } + }, "app/signup/layout.tsx": { "ts/no-explicit-any": { "count": 1 @@ -4698,21 +4891,36 @@ } }, "context/hooks/use-trigger-events-limit-modal.ts": { + "no-restricted-globals": { + "count": 2 + }, "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 3 } }, "context/modal-context.test.tsx": { + "no-restricted-globals": { + "count": 4 + }, + "no-restricted-properties": { + "count": 1 + }, "ts/no-explicit-any": { "count": 3 } }, "context/modal-context.tsx": { + "no-restricted-globals": { + "count": 2 + }, "ts/no-explicit-any": { "count": 5 } }, "context/provider-context.tsx": { + "no-restricted-globals": { + "count": 2 + }, "ts/no-explicit-any": { "count": 1 } @@ -4730,6 +4938,11 @@ "count": 1 } }, + "hooks/use-import-dsl.ts": { + "no-restricted-globals": { + "count": 2 + } + }, "hooks/use-metadata.ts": { "ts/no-explicit-any": { "count": 1 @@ -4914,6 +5127,11 @@ "count": 2 } }, + "service/refresh-token.ts": { + "no-restricted-properties": { + "count": 7 + } + }, "service/share.ts": { "ts/no-explicit-any": { "count": 4 @@ -4978,6 +5196,11 @@ "count": 2 } }, + "service/webapp-auth.ts": { + "no-restricted-globals": { + "count": 6 + } + }, "service/workflow-payload.ts": { "ts/no-explicit-any": { "count": 10 @@ -5089,6 +5312,16 @@ "count": 4 } }, + "utils/setup-status.spec.ts": { + "no-restricted-globals": { + "count": 11 + } + }, + "utils/setup-status.ts": { + "no-restricted-globals": { + "count": 3 + } + }, "utils/tool-call.spec.ts": { "ts/no-explicit-any": { "count": 1 diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 05c7502612..70da1d166f 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -42,6 +42,40 @@ export default antfu( { rules: { 'node/prefer-global/process': 'off', + 'no-restricted-globals': [ + 'error', + { + name: 'localStorage', + message: 'Use @/utils/storage instead. Direct localStorage access causes SSR issues.', + }, + { + name: 'sessionStorage', + message: 'Use @/utils/storage instead. Direct sessionStorage access causes SSR issues.', + }, + ], + 'no-restricted-properties': [ + 'error', + { + object: 'window', + property: 'localStorage', + message: 'Use @/utils/storage instead.', + }, + { + object: 'window', + property: 'sessionStorage', + message: 'Use @/utils/storage instead.', + }, + { + object: 'globalThis', + property: 'localStorage', + message: 'Use @/utils/storage instead.', + }, + { + object: 'globalThis', + property: 'sessionStorage', + message: 'Use @/utils/storage instead.', + }, + ], }, }, { diff --git a/web/utils/storage.ts b/web/utils/storage.ts new file mode 100644 index 0000000000..aef1ce69aa --- /dev/null +++ b/web/utils/storage.ts @@ -0,0 +1,103 @@ +/* eslint-disable no-restricted-globals */ +import { isClient } from './client' + +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } + +let _isAvailable: boolean | null = null + +function isLocalStorageAvailable(): boolean { + if (_isAvailable !== null) + return _isAvailable + + if (!isClient) { + _isAvailable = false + return false + } + + try { + const testKey = '__storage_test__' + localStorage.setItem(testKey, 'test') + localStorage.removeItem(testKey) + _isAvailable = true + return true + } + catch { + _isAvailable = false + return false + } +} + +function get(key: string, defaultValue?: T): T | null { + if (!isLocalStorageAvailable()) + return defaultValue ?? null + + try { + const item = localStorage.getItem(key) + if (item === null) + return defaultValue ?? null + + try { + return JSON.parse(item) as T + } + catch { + return item as T + } + } + catch { + return defaultValue ?? null + } +} + +function set(key: string, value: T): void { + if (!isLocalStorageAvailable()) + return + + try { + const stringValue = typeof value === 'string' ? value : JSON.stringify(value) + localStorage.setItem(key, stringValue) + } + catch { + // Silent fail - localStorage may be full or disabled + } +} + +function remove(key: string): void { + if (!isLocalStorageAvailable()) + return + + try { + localStorage.removeItem(key) + } + catch { + // Silent fail + } +} + +function getNumber(key: string, defaultValue?: number): number | null { + const value = get(key) + if (value === null) + return defaultValue ?? null + + const parsed = typeof value === 'number' ? value : Number.parseFloat(value as string) + return Number.isNaN(parsed) ? (defaultValue ?? null) : parsed +} + +function getBoolean(key: string, defaultValue?: boolean): boolean | null { + const value = get(key) + if (value === null) + return defaultValue ?? null + + if (typeof value === 'boolean') + return value + + return value === 'true' +} + +export const storage = { + get, + set, + remove, + getNumber, + getBoolean, + isAvailable: isLocalStorageAvailable, +}