Compare commits

...

6 Commits

Author SHA1 Message Date
yyh
1cf3e599df fix: remove panelWidth prop in panel slice, use default value 420 in layout slice(single source) 2026-01-18 16:33:59 +08:00
yyh
a85946d3e7 refactor(web): remove redundant nullish coalescing from storage calls
Function overloads in storage utility already guarantee non-null returns
when defaultValue is provided, making ?? fallbacks unnecessary.
2026-01-18 16:21:29 +08:00
yyh
9e4d3c75ae fix 2026-01-18 16:15:30 +08:00
yyh
70bea85624 refactor(web): improve storage utility type safety with function overloads
Add function overloads to getNumber and getBoolean so they return
non-nullable types when a defaultValue is provided. This eliminates
the need for non-null assertions at call sites.

- Remove unused persist-config.ts (zustand adapter not needed)
- Remove ! assertions from layout-slice.ts
2026-01-18 16:10:04 +08:00
yyh
b75b7d6c61 fix: unused 2026-01-18 16:05:10 +08:00
yyh
e819b804ba refactor(web): add SSR-safe localStorage utility and ESLint rules
Introduce centralized storage utilities to address SSR issues with direct
localStorage access in zustand slices and components. This adds ESLint
rules to prevent future regressions while preserving existing usages
via bulk suppressions.

- Add config/storage-keys.ts for centralized storage key definitions
- Add utils/storage.ts with SSR-safe get/set/remove operations
- Add workflow/store/persist-config.ts for zustand storage adapter
- Add no-restricted-globals and no-restricted-properties ESLint rules
- Migrate workflow slices and related components to use new utilities
2026-01-18 16:01:04 +08:00
16 changed files with 449 additions and 24 deletions

View File

@@ -18,6 +18,7 @@ import { useStore } from '@/app/components/app/store'
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
import Loading from '@/app/components/base/loading'
import ExtraInfo from '@/app/components/datasets/extra-info'
import { STORAGE_KEYS } from '@/config/storage-keys'
import { useAppContext } from '@/context/app-context'
import DatasetDetailContext from '@/context/dataset-detail'
import { useEventEmitterContextContext } from '@/context/event-emitter'
@@ -25,6 +26,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useDocumentTitle from '@/hooks/use-document-title'
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
import { cn } from '@/utils/classnames'
import { storage } from '@/utils/storage'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@@ -40,7 +42,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const pathname = usePathname()
const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-pipeline')
const isPipelineCanvas = pathname.endsWith('/pipeline')
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
const { eventEmitter } = useEventEmitterContextContext()

View File

@@ -5,9 +5,11 @@ import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useStore as useAppStore } from '@/app/components/app/store'
import { STORAGE_KEYS } from '@/config/storage-keys'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { cn } from '@/utils/classnames'
import { storage } from '@/utils/storage'
import Divider from '../base/divider'
import { getKeyboardKeyCodeBySystem } from '../workflow/utils'
import AppInfo from './app-info'
@@ -53,7 +55,7 @@ const AppDetailNav = ({
const pathname = usePathname()
const inWorkflowCanvas = pathname.endsWith('/workflow')
const isPipelineCanvas = pathname.endsWith('/pipeline')
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
const { eventEmitter } = useEventEmitterContextContext()
@@ -64,7 +66,7 @@ const AppDetailNav = ({
useEffect(() => {
if (appSidebarExpand) {
localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand)
storage.set(STORAGE_KEYS.APP.DETAIL_COLLAPSE, appSidebarExpand)
setAppSidebarExpand(appSidebarExpand)
}
}, [appSidebarExpand, setAppSidebarExpand])

View File

@@ -2,8 +2,10 @@
import { usePathname } from 'next/navigation'
import * as React from 'react'
import { useState } from 'react'
import { STORAGE_KEYS } from '@/config/storage-keys'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { cn } from '@/utils/classnames'
import { storage } from '@/utils/storage'
import s from './index.module.css'
type HeaderWrapperProps = {
@@ -18,7 +20,7 @@ const HeaderWrapper = ({
// Check if the current path is a workflow canvas & fullscreen
const inWorkflowCanvas = pathname.endsWith('/workflow')
const isPipelineCanvas = pathname.endsWith('/pipeline')
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
const workflowCanvasMaximize = storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false)
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
const { eventEmitter } = useEventEmitterContextContext()

View File

@@ -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,

View File

@@ -26,12 +26,12 @@ const Wrap = ({
isExpand,
children,
}: Props) => {
const panelWidth = useStore(state => state.panelWidth)
const nodePanelWidth = useStore(state => state.nodePanelWidth)
const wrapStyle = (() => {
if (isExpand) {
return {
...style,
width: panelWidth - 1,
width: nodePanelWidth - 1,
}
}
return style

View File

@@ -59,12 +59,14 @@ import {
hasRetryNode,
isSupportCustomRunForm,
} from '@/app/components/workflow/utils'
import { STORAGE_KEYS } from '@/config/storage-keys'
import { useModalContext } from '@/context/modal-context'
import { useAllBuiltInTools } from '@/service/use-tools'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import { FlowType } from '@/types/common'
import { canFindTool } from '@/utils'
import { cn } from '@/utils/classnames'
import { storage } from '@/utils/storage'
import { useResizePanel } from '../../hooks/use-resize-panel'
import BeforeRunForm from '../before-run-form'
import PanelWrap from '../before-run-form/panel-wrap'
@@ -137,7 +139,7 @@ const BasePanel: FC<BasePanelProps> = ({
const newValue = Math.max(400, Math.min(width, maxNodePanelWidth))
if (source === 'user')
localStorage.setItem('workflow-node-panel-width', `${newValue}`)
storage.set(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, newValue)
setNodePanelWidth(newValue)
}, [maxNodePanelWidth, setNodePanelWidth])

View File

@@ -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(() => {

View File

@@ -1,11 +1,12 @@
import type { StateCreator } from 'zustand'
import { STORAGE_KEYS } from '@/config/storage-keys'
import { storage } from '@/utils/storage'
export type LayoutSliceShape = {
workflowCanvasWidth?: number
workflowCanvasHeight?: number
setWorkflowCanvasWidth: (width: number) => void
setWorkflowCanvasHeight: (height: number) => void
// rightPanelWidth - otherPanelWidth = nodePanelWidth
rightPanelWidth?: number
setRightPanelWidth: (width: number) => void
nodePanelWidth: number
@@ -14,11 +15,11 @@ export type LayoutSliceShape = {
setPreviewPanelWidth: (width: number) => void
otherPanelWidth: number
setOtherPanelWidth: (width: number) => void
bottomPanelWidth: number // min-width = 400px; default-width = auto || 480px;
bottomPanelWidth: number
setBottomPanelWidth: (width: number) => void
bottomPanelHeight: number
setBottomPanelHeight: (height: number) => void
variableInspectPanelHeight: number // min-height = 120px; default-height = 320px;
variableInspectPanelHeight: number
setVariableInspectPanelHeight: (height: number) => void
maximizeCanvas: boolean
setMaximizeCanvas: (maximize: boolean) => void
@@ -31,9 +32,9 @@ export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({
setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })),
rightPanelWidth: undefined,
setRightPanelWidth: width => set(() => ({ rightPanelWidth: width })),
nodePanelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 400,
nodePanelWidth: storage.getNumber(STORAGE_KEYS.WORKFLOW.NODE_PANEL_WIDTH, 420),
setNodePanelWidth: width => set(() => ({ nodePanelWidth: width })),
previewPanelWidth: localStorage.getItem('debug-and-preview-panel-width') ? Number.parseFloat(localStorage.getItem('debug-and-preview-panel-width')!) : 400,
previewPanelWidth: storage.getNumber(STORAGE_KEYS.WORKFLOW.PREVIEW_PANEL_WIDTH, 400),
setPreviewPanelWidth: width => set(() => ({ previewPanelWidth: width })),
otherPanelWidth: 400,
setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })),
@@ -41,8 +42,8 @@ export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({
setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })),
bottomPanelHeight: 324,
setBottomPanelHeight: height => set(() => ({ bottomPanelHeight: height })),
variableInspectPanelHeight: localStorage.getItem('workflow-variable-inpsect-panel-height') ? Number.parseFloat(localStorage.getItem('workflow-variable-inpsect-panel-height')!) : 320,
variableInspectPanelHeight: storage.getNumber(STORAGE_KEYS.WORKFLOW.VARIABLE_INSPECT_PANEL_HEIGHT, 320),
setVariableInspectPanelHeight: height => set(() => ({ variableInspectPanelHeight: height })),
maximizeCanvas: localStorage.getItem('workflow-canvas-maximize') === 'true',
maximizeCanvas: storage.getBoolean(STORAGE_KEYS.WORKFLOW.CANVAS_MAXIMIZE, false),
setMaximizeCanvas: maximize => set(() => ({ maximizeCanvas: maximize })),
})

View File

@@ -1,7 +1,6 @@
import type { StateCreator } from 'zustand'
export type PanelSliceShape = {
panelWidth: number
showFeaturesPanel: boolean
setShowFeaturesPanel: (showFeaturesPanel: boolean) => void
showWorkflowVersionHistoryPanel: boolean
@@ -27,7 +26,6 @@ export type PanelSliceShape = {
}
export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({
panelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420,
showFeaturesPanel: false,
setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })),
showWorkflowVersionHistoryPanel: false,

View File

@@ -5,6 +5,8 @@ import type {
WorkflowRunningData,
} from '@/app/components/workflow/types'
import type { FileUploadConfigResponse } from '@/models/common'
import { STORAGE_KEYS } from '@/config/storage-keys'
import { storage } from '@/utils/storage'
type PreviewRunningData = WorkflowRunningData & {
resultTabActive?: boolean
@@ -63,10 +65,10 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
setSelection: selection => set(() => ({ selection })),
bundleNodeSize: null,
setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })),
controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : 'hand',
controlMode: storage.get<'pointer' | 'hand'>(STORAGE_KEYS.WORKFLOW.OPERATION_MODE) === 'pointer' ? 'pointer' : 'hand',
setControlMode: (controlMode) => {
set(() => ({ controlMode }))
localStorage.setItem('workflow-operation-mode', controlMode)
storage.set(STORAGE_KEYS.WORKFLOW.OPERATION_MODE, controlMode)
},
mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 },
setMousePosition: mousePosition => set(() => ({ mousePosition })),

View File

@@ -4,7 +4,9 @@ import {
useCallback,
useMemo,
} from 'react'
import { STORAGE_KEYS } from '@/config/storage-keys'
import { cn } from '@/utils/classnames'
import { storage } from '@/utils/storage'
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
import { useStore } from '../store'
import Panel from './panel'
@@ -21,8 +23,8 @@ const VariableInspectPanel: FC = () => {
return workflowCanvasHeight - 60
}, [workflowCanvasHeight])
const handleResize = useCallback((width: number, height: number) => {
localStorage.setItem('workflow-variable-inpsect-panel-height', `${height}`)
const handleResize = useCallback((_width: number, height: number) => {
storage.set(STORAGE_KEYS.WORKFLOW.VARIABLE_INSPECT_PANEL_HEIGHT, height)
setVariableInspectPanelHeight(height)
}, [setVariableInspectPanelHeight])

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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.',
},
],
},
},
{

107
web/utils/storage.ts Normal file
View File

@@ -0,0 +1,107 @@
/* 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<T extends JsonValue>(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<T extends JsonValue>(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): number | null
function getNumber(key: string, defaultValue: number): number
function getNumber(key: string, defaultValue?: number): number | null {
const value = get<string | number>(key)
if (value === null)
return defaultValue ?? null
const parsed = typeof value === 'number' ? value : Number.parseFloat(value as string)
return Number.isNaN(parsed) ? (defaultValue ?? null) : parsed
}
function getBoolean(key: string): boolean | null
function getBoolean(key: string, defaultValue: boolean): boolean
function getBoolean(key: string, defaultValue?: boolean): boolean | null {
const value = get<string | boolean>(key)
if (value === null)
return defaultValue ?? null
if (typeof value === 'boolean')
return value
return value === 'true'
}
export const storage = {
get,
set,
remove,
getNumber,
getBoolean,
isAvailable: isLocalStorageAvailable,
}