mirror of
https://github.com/langgenius/dify.git
synced 2026-06-11 19:14:34 +08:00
refactor: use foxact package for copied hooks (#37308)
This commit is contained in:
parent
08f1bf20ab
commit
5ed663e7fd
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
@ -264,9 +264,6 @@ catalogs:
|
||||
cli-table3:
|
||||
specifier: 0.6.5
|
||||
version: 0.6.5
|
||||
client-only:
|
||||
specifier: 0.0.1
|
||||
version: 0.0.1
|
||||
clsx:
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
@ -348,6 +345,9 @@ catalogs:
|
||||
fast-deep-equal:
|
||||
specifier: 3.1.3
|
||||
version: 3.1.3
|
||||
foxact:
|
||||
specifier: 0.3.4
|
||||
version: 0.3.4
|
||||
fuse.js:
|
||||
specifier: 7.4.2
|
||||
version: 7.4.2
|
||||
@ -1089,9 +1089,6 @@ importers:
|
||||
class-variance-authority:
|
||||
specifier: 'catalog:'
|
||||
version: 0.7.1
|
||||
client-only:
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.1
|
||||
cmdk:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
@ -1134,6 +1131,9 @@ importers:
|
||||
fast-deep-equal:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.3
|
||||
foxact:
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
fuse.js:
|
||||
specifier: 'catalog:'
|
||||
version: 7.4.2
|
||||
@ -6358,6 +6358,9 @@ packages:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
event-target-bus@1.0.0:
|
||||
resolution: {integrity: sha512-uPcWKbj/BJU3Tbw9XqhHqET4/LBOhvv3/SJWr7NksxA6TC5YqBpaZgawE9R+WpYFCBFSAE4Vun+xQS6w4ABdlA==}
|
||||
|
||||
eventsource-parser@3.1.0:
|
||||
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@ -6463,6 +6466,17 @@ packages:
|
||||
engines: {node: '>=18.3.0'}
|
||||
hasBin: true
|
||||
|
||||
foxact@0.3.4:
|
||||
resolution: {integrity: sha512-6ENWStzEp/VczVgwBdn8scZUGccko2kLGho8Qg1VyUTG6tpSW1vz2xX/dRtAUjPEFbu618DKsjj/YL7iMU8mlA==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
@ -14740,6 +14754,8 @@ snapshots:
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
event-target-bus@1.0.0: {}
|
||||
|
||||
eventsource-parser@3.1.0: {}
|
||||
|
||||
expand-template@2.0.3:
|
||||
@ -14843,6 +14859,15 @@ snapshots:
|
||||
dependencies:
|
||||
fd-package-json: 2.0.0
|
||||
|
||||
foxact@0.3.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
event-target-bus: 1.0.0
|
||||
server-only: 0.0.1
|
||||
optionalDependencies:
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
|
||||
fs-constants@1.0.0:
|
||||
optional: true
|
||||
|
||||
@ -18169,7 +18194,6 @@ time:
|
||||
chokidar@5.0.0: '2025-11-25T23:28:06.854Z'
|
||||
class-variance-authority@0.7.1: '2024-11-26T08:20:34.604Z'
|
||||
cli-table3@0.6.5: '2024-05-12T16:36:50.079Z'
|
||||
client-only@0.0.1: '2022-09-03T01:07:11.981Z'
|
||||
clsx@2.1.1: '2024-04-23T05:26:04.645Z'
|
||||
cmdk@1.1.1: '2025-03-14T19:21:16.194Z'
|
||||
code-inspector-plugin@1.5.2: '2026-06-07T02:32:04.545Z'
|
||||
@ -18197,6 +18221,7 @@ time:
|
||||
eslint@10.4.1: '2026-05-29T20:32:32.193Z'
|
||||
eventsource-parser@3.1.0: '2026-05-27T20:55:51.466Z'
|
||||
fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z'
|
||||
foxact@0.3.4: '2026-05-15T12:55:52.584Z'
|
||||
fuse.js@7.4.2: '2026-06-05T22:22:52.388Z'
|
||||
happy-dom@20.10.2: '2026-06-06T15:03:11.325Z'
|
||||
hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z'
|
||||
|
||||
@ -131,7 +131,6 @@ catalog:
|
||||
chokidar: 5.0.0
|
||||
class-variance-authority: 0.7.1
|
||||
cli-table3: 0.6.5
|
||||
client-only: 0.0.1
|
||||
clsx: 2.1.1
|
||||
cmdk: 1.1.1
|
||||
code-inspector-plugin: 1.5.2
|
||||
@ -159,6 +158,7 @@ catalog:
|
||||
eslint-plugin-storybook: 10.4.2
|
||||
eventsource-parser: 3.1.0
|
||||
fast-deep-equal: 3.1.3
|
||||
foxact: 0.3.4
|
||||
fuse.js: 7.4.2
|
||||
happy-dom: 20.10.2
|
||||
hast-util-to-jsx-runtime: 2.3.6
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
- 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 `@/hooks/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.
|
||||
- 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 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.
|
||||
- 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.
|
||||
|
||||
@ -77,7 +77,7 @@ vi.mock('@/hooks/use-async-window-open', () => ({
|
||||
useAsyncWindowOpen: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
vi.mock('foxact/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => mockSetEducationVerifying,
|
||||
}))
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import type { App } from '@/types/app'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useSetLocalStorage } from 'foxact/use-local-storage'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -18,7 +19,6 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co
|
||||
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 { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
updateAppSiteAccessToken,
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
RiFocus2Fill,
|
||||
RiFocus2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -24,7 +25,6 @@ import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
vi.mock('foxact/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => setEducationVerifyingMock,
|
||||
}))
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@ import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-moda
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
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 { useProviderContext } from '@/context/provider-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps'
|
||||
import { useInvalidateAppList } from '@/service/use-apps'
|
||||
|
||||
@ -19,6 +19,7 @@ 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'
|
||||
@ -27,7 +28,6 @@ import Loading from '@/app/components/base/loading'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { generateRule } from '@/service/debug'
|
||||
import { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
|
||||
|
||||
@ -6,6 +6,7 @@ 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'
|
||||
@ -17,7 +18,6 @@ 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 { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { importDSL } from '@/service/apps'
|
||||
|
||||
@ -10,6 +10,7 @@ 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 AppIcon from '@/app/components/base/app-icon'
|
||||
@ -20,7 +21,6 @@ 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 { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { createApp } from '@/service/apps'
|
||||
|
||||
@ -8,6 +8,7 @@ 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 Input from '@/app/components/base/input'
|
||||
@ -16,7 +17,6 @@ import { usePluginDependencies } from '@/app/components/workflow/plugin-dependen
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import {
|
||||
DSLImportMode,
|
||||
DSLImportStatus,
|
||||
|
||||
@ -16,6 +16,7 @@ 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'
|
||||
@ -26,7 +27,6 @@ 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 { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { deleteApp, switchApp } from '@/service/apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
@ -30,6 +30,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@langgenius/dify-ui/tooltip'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useSetLocalStorage } from 'foxact/use-local-storage'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
@ -42,7 +43,6 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { AppCardTags } from '@/features/tag-management/components/app-card-tags'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
|
||||
@ -4,6 +4,7 @@ import type { AppListQuery } from '@/contract/console/apps'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { keepPreviousData, useInfiniteQuery, 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 { SearchInput } from '@/app/components/base/search-input'
|
||||
@ -11,7 +12,6 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { TagFilter } from '@/features/tag-management/components/tag-filter'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import Link from '@/next/link'
|
||||
|
||||
@ -5,6 +5,7 @@ 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'
|
||||
@ -12,7 +13,6 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
import { AppSourceType, delConversation, pinConversation, renameConversation, unpinConversation, updateFeedback } from '@/service/share'
|
||||
import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share'
|
||||
|
||||
@ -5,13 +5,13 @@ 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 { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
import { AppSourceType, updateFeedback } from '@/service/share'
|
||||
import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share'
|
||||
|
||||
@ -5,7 +5,7 @@ const mockCopy = vi.fn()
|
||||
const mockReset = vi.fn()
|
||||
let mockCopied = false
|
||||
|
||||
vi.mock('@/hooks/use-clipboard', () => ({
|
||||
vi.mock('foxact/use-clipboard', () => ({
|
||||
useClipboard: () => ({
|
||||
copy: mockCopy,
|
||||
reset: mockReset,
|
||||
|
||||
@ -4,10 +4,10 @@ import {
|
||||
RiClipboardFill,
|
||||
RiClipboardLine,
|
||||
} from '@remixicon/react'
|
||||
import { useClipboard } from 'foxact/use-clipboard'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { useClipboard } from '@/hooks/use-clipboard'
|
||||
import copyStyle from './style.module.css'
|
||||
|
||||
type Props = Readonly<{
|
||||
|
||||
@ -5,7 +5,7 @@ const copy = vi.fn()
|
||||
const reset = vi.fn()
|
||||
let copied = false
|
||||
|
||||
vi.mock('@/hooks/use-clipboard', () => ({
|
||||
vi.mock('foxact/use-clipboard', () => ({
|
||||
useClipboard: () => ({
|
||||
copy,
|
||||
reset,
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useClipboard } from 'foxact/use-clipboard'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useClipboard } from '@/hooks/use-clipboard'
|
||||
|
||||
type Props = Readonly<{
|
||||
content: string
|
||||
|
||||
@ -6,7 +6,7 @@ const mockCopy = vi.fn()
|
||||
let mockCopied = false
|
||||
const mockReset = vi.fn()
|
||||
|
||||
vi.mock('@/hooks/use-clipboard', () => ({
|
||||
vi.mock('foxact/use-clipboard', () => ({
|
||||
useClipboard: () => ({
|
||||
copy: mockCopy,
|
||||
copied: mockCopied,
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
import type { InputProps } from '../input'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useClipboard } from 'foxact/use-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useClipboard } from '@/hooks/use-clipboard'
|
||||
import ActionButton from '../action-button'
|
||||
|
||||
type InputWithCopyProps = {
|
||||
|
||||
@ -36,7 +36,7 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
vi.mock('foxact/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => setEducationVerifyingMock,
|
||||
}))
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ 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'
|
||||
@ -18,7 +19,6 @@ import VerifyStateModal from '@/app/education-apply/verify-state-modal'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { useEducationVerify } from '@/service/use-education'
|
||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'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 { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
|
||||
export function EducationVerifyActionRecorder() {
|
||||
|
||||
@ -5,6 +5,7 @@ import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useSetLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
@ -18,7 +19,6 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { env } from '@/env'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'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 { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import s from './index.module.css'
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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 { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { NOTICE_I18N } from '@/i18n-config/language'
|
||||
|
||||
const MaintenanceNotice = () => {
|
||||
|
||||
@ -6,6 +6,7 @@ import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
@ -13,7 +14,6 @@ import Loading from '@/app/components/base/loading'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import Link from '@/next/link'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
@ -6,6 +6,7 @@ import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
@ -13,7 +14,6 @@ import Loading from '@/app/components/base/loading'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import Link from '@/next/link'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
@ -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 { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import Link from '@/next/link'
|
||||
import { useRAGRecommendedPlugins } from '@/service/use-tools'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
@ -13,6 +13,7 @@ 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,
|
||||
@ -68,7 +69,6 @@ import {
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
@ -5,13 +5,13 @@ 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'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useEdgesInteractions } from '../../../hooks'
|
||||
import AddButton from '../../_base/components/add-button'
|
||||
import Item from './class-item'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { EditorState } from 'lexical'
|
||||
import type { NoteTheme } from './types'
|
||||
import { useSetLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useCallback } from 'react'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useNodeDataUpdate, useWorkflowHistory, WorkflowHistoryEvent } from '../hooks'
|
||||
import { NOTE_SHOW_AUTHOR_STORAGE_KEY } from './constants'
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import {
|
||||
CUSTOM_NOTE_NODE,
|
||||
NOTE_SHOW_AUTHOR_STORAGE_KEY,
|
||||
|
||||
@ -5,6 +5,7 @@ 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,
|
||||
@ -19,7 +20,6 @@ import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows
|
||||
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 { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import {
|
||||
useWorkflowInteractions,
|
||||
} from '../../hooks'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useLocalStorage, useSetLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useEffect, useLayoutEffect as useLayoutEffectFromReact } from 'react'
|
||||
import { useLocalStorage, useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import {
|
||||
isControlMode,
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
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 { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
|
||||
import { useStore } from '../store'
|
||||
import Panel from './panel'
|
||||
|
||||
@ -16,6 +16,7 @@ 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'
|
||||
@ -26,7 +27,6 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import WorkflowPreview from '@/app/components/workflow/workflow-preview'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { generateWorkflow } from '@/service/debug'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
|
||||
@ -8,6 +8,7 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { 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'
|
||||
@ -19,7 +20,6 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
import { WorkspaceProvider } from '@/context/workspace-context-provider'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import {
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
|
||||
@ -4,6 +4,7 @@ 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,
|
||||
@ -13,7 +14,6 @@ import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/con
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education'
|
||||
import {
|
||||
|
||||
@ -2,12 +2,12 @@ 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 { emailRegex } from '@/config'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { sendEMailLoginCode } from '@/service/common'
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ 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 {
|
||||
|
||||
@ -22,7 +23,6 @@ import {
|
||||
} from '@/app/education-apply/constants'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import {
|
||||
useAccountSettingModal,
|
||||
usePricingModal,
|
||||
|
||||
@ -32,7 +32,7 @@ vi.mock('@/app/components/header/account-setting', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
vi.mock('foxact/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => mockSetEducationVerifying,
|
||||
}))
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import type { ReactNode } from 'react'
|
||||
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'
|
||||
@ -15,7 +16,6 @@ import {
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { fetchCurrentPlanInfo } from '@/service/billing'
|
||||
import {
|
||||
useModelListByType,
|
||||
|
||||
@ -186,14 +186,13 @@ export default antfu(
|
||||
...GLOB_TESTS,
|
||||
'vitest.setup.ts',
|
||||
'instrumentation-client.ts',
|
||||
'hooks/use-local-storage/index.ts',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
name: 'localStorage',
|
||||
message: 'Do not use localStorage directly. Use @/hooks/use-local-storage instead.',
|
||||
message: 'Do not use localStorage directly. Use foxact/use-local-storage instead.',
|
||||
},
|
||||
],
|
||||
'no-restricted-properties': [
|
||||
@ -201,19 +200,19 @@ export default antfu(
|
||||
{
|
||||
object: 'window',
|
||||
property: 'localStorage',
|
||||
message: 'Do not use window.localStorage directly. Use @/hooks/use-local-storage instead.',
|
||||
message: 'Do not use window.localStorage directly. Use foxact/use-local-storage instead.',
|
||||
},
|
||||
{
|
||||
object: 'globalThis',
|
||||
property: 'localStorage',
|
||||
message: 'Do not use globalThis.localStorage directly. Use @/hooks/use-local-storage instead.',
|
||||
message: 'Do not use globalThis.localStorage directly. Use foxact/use-local-storage instead.',
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'ImportDeclaration[source.value="ahooks"] ImportSpecifier[imported.name="useLocalStorageState"]',
|
||||
message: 'Do not use ahooks useLocalStorageState. Use @/hooks/use-local-storage instead.',
|
||||
message: 'Do not use ahooks useLocalStorageState. Use foxact/use-local-storage instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
type Noop = {
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
(...args: any[]): any
|
||||
}
|
||||
|
||||
/** @see https://foxact.skk.moe/noop */
|
||||
export const noop: Noop = () => { /* noop */ }
|
||||
@ -1,74 +0,0 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { writeTextToClipboard } from '@/utils/clipboard'
|
||||
import { noop } from './noop'
|
||||
import { useStableHandler } from './use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired'
|
||||
import { useCallback } from './use-typescript-happy-callback'
|
||||
import 'client-only'
|
||||
|
||||
type UseClipboardOption = {
|
||||
timeout?: number
|
||||
usePromptAsFallback?: boolean
|
||||
promptFallbackText?: string
|
||||
onCopyError?: (error: Error) => void
|
||||
}
|
||||
|
||||
/** @see https://foxact.skk.moe/use-clipboard */
|
||||
export function useClipboard({
|
||||
timeout = 1000,
|
||||
usePromptAsFallback = false,
|
||||
promptFallbackText = 'Failed to copy to clipboard automatically, please manually copy the text below.',
|
||||
onCopyError,
|
||||
}: UseClipboardOption = {}) {
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const copyTimeoutRef = useRef<number | null>(null)
|
||||
|
||||
const stablizedOnCopyError = useStableHandler<[e: Error], void>(onCopyError || noop)
|
||||
|
||||
const handleCopyResult = useCallback((isCopied: boolean) => {
|
||||
if (copyTimeoutRef.current) {
|
||||
clearTimeout(copyTimeoutRef.current)
|
||||
}
|
||||
if (isCopied) {
|
||||
copyTimeoutRef.current = window.setTimeout(() => setCopied(false), timeout)
|
||||
}
|
||||
setCopied(isCopied)
|
||||
}, [timeout])
|
||||
|
||||
const handleCopyError = useCallback((e: Error) => {
|
||||
setError(e)
|
||||
stablizedOnCopyError(e)
|
||||
}, [stablizedOnCopyError])
|
||||
|
||||
const copy = useCallback(async (valueToCopy: string) => {
|
||||
try {
|
||||
await writeTextToClipboard(valueToCopy)
|
||||
handleCopyResult(true)
|
||||
}
|
||||
catch (e) {
|
||||
if (usePromptAsFallback) {
|
||||
try {
|
||||
// eslint-disable-next-line no-alert -- prompt as fallback in case of copy error
|
||||
window.prompt(promptFallbackText, valueToCopy)
|
||||
handleCopyResult(true)
|
||||
}
|
||||
catch (e2) {
|
||||
handleCopyError(e2 as Error)
|
||||
}
|
||||
}
|
||||
else {
|
||||
handleCopyError(e as Error)
|
||||
}
|
||||
}
|
||||
}, [handleCopyResult, promptFallbackText, handleCopyError, usePromptAsFallback])
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setCopied(false)
|
||||
setError(null)
|
||||
if (copyTimeoutRef.current) {
|
||||
clearTimeout(copyTimeoutRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { copy, reset, error, copied }
|
||||
}
|
||||
@ -4,6 +4,7 @@ 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,
|
||||
@ -13,7 +14,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import {
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { useLocalStorage } from '../index'
|
||||
|
||||
describe('useLocalStorage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
window.localStorage.clear()
|
||||
})
|
||||
|
||||
it('should return server value and persist it when storage is empty', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('shape', 'circle'))
|
||||
|
||||
expect(result.current[0]).toBe('circle')
|
||||
expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('circle'))
|
||||
})
|
||||
|
||||
it('should prefer stored value over server value', () => {
|
||||
window.localStorage.setItem('shape', JSON.stringify('square'))
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('shape', 'circle'))
|
||||
|
||||
expect(result.current[0]).toBe('square')
|
||||
expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('square'))
|
||||
})
|
||||
|
||||
it('should update storage and subscribers from setter calls', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('shape', 'circle'))
|
||||
|
||||
act(() => {
|
||||
result.current[1]('triangle')
|
||||
})
|
||||
|
||||
expect(result.current[0]).toBe('triangle')
|
||||
expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('triangle'))
|
||||
})
|
||||
|
||||
it('should support updater functions and null removal', () => {
|
||||
const { result } = renderHook(() => useLocalStorage<number>('count', 1))
|
||||
|
||||
act(() => {
|
||||
result.current[1](current => (current ?? 0) + 1)
|
||||
})
|
||||
|
||||
expect(result.current[0]).toBe(2)
|
||||
expect(window.localStorage.getItem('count')).toBe(JSON.stringify(2))
|
||||
|
||||
act(() => {
|
||||
result.current[1](null)
|
||||
})
|
||||
|
||||
expect(result.current[0]).toBe(1)
|
||||
expect(window.localStorage.getItem('count')).toBeNull()
|
||||
})
|
||||
|
||||
it('should update from cross-tab storage events', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('shape', 'circle'))
|
||||
|
||||
window.localStorage.setItem('shape', JSON.stringify('square'))
|
||||
act(() => {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: 'shape' }))
|
||||
})
|
||||
|
||||
expect(result.current[0]).toBe('square')
|
||||
})
|
||||
|
||||
it('should support raw string values', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('raw-shape', 'circle', { raw: true }))
|
||||
|
||||
act(() => {
|
||||
result.current[1]('square')
|
||||
})
|
||||
|
||||
expect(result.current[0]).toBe('square')
|
||||
expect(window.localStorage.getItem('raw-shape')).toBe('square')
|
||||
})
|
||||
|
||||
it('should render with server value during server rendering', () => {
|
||||
const Component = () => {
|
||||
const [value] = useLocalStorage('shape', 'circle')
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
expect(renderToString(<Component />)).toContain('circle')
|
||||
})
|
||||
|
||||
it('should throw a recoverable no-SSR error during server rendering without server value', () => {
|
||||
const Component = () => {
|
||||
const [value] = useLocalStorage<string>('shape')
|
||||
return <div>{value}</div>
|
||||
}
|
||||
|
||||
expect(() => renderToString(<Component />)).toThrow('[foxact/use-local-storage] cannot be used on the server without a serverValue')
|
||||
})
|
||||
})
|
||||
@ -1,260 +0,0 @@
|
||||
import { useCallback, useEffect, useLayoutEffect as useLayoutEffectFromReact, useMemo, useSyncExternalStore } from 'react'
|
||||
import { noop } from '../noop'
|
||||
import 'client-only'
|
||||
|
||||
type NotUndefined<T> = T extends undefined ? never : T
|
||||
type StateHookTuple<T> = readonly [T, React.Dispatch<React.SetStateAction<T | null>>]
|
||||
|
||||
type Serializer<T> = (value: T) => string
|
||||
type Deserializer<T> = (value: string) => T
|
||||
|
||||
export type UseLocalStorageRawOption = {
|
||||
raw: true
|
||||
}
|
||||
|
||||
export type UseLocalStorageParserOption<T> = {
|
||||
raw?: false
|
||||
serializer: Serializer<T>
|
||||
deserializer: Deserializer<T>
|
||||
}
|
||||
|
||||
const FOXACT_LOCAL_STORAGE_EVENT_KEY = 'foxact-use-local-storage'
|
||||
const HOOK_NAME = 'foxact/use-local-storage'
|
||||
|
||||
const useLayoutEffect = typeof window === 'undefined'
|
||||
? useEffect
|
||||
: useLayoutEffectFromReact
|
||||
|
||||
type ErrorConstructorWithStackTraceLimit = ErrorConstructor & {
|
||||
stackTraceLimit?: number
|
||||
}
|
||||
|
||||
const errorConstructor = Error as ErrorConstructorWithStackTraceLimit
|
||||
const stackTraceLimitProperty = Object.getOwnPropertyDescriptor(errorConstructor, 'stackTraceLimit')
|
||||
const hasWritableStackTraceLimit = stackTraceLimitProperty?.writable && typeof stackTraceLimitProperty.value === 'number'
|
||||
|
||||
function createStacklessError<T = Error>(errorFactory: () => T): T {
|
||||
const originalStackTraceLimit = errorConstructor.stackTraceLimit
|
||||
|
||||
if (hasWritableStackTraceLimit)
|
||||
errorConstructor.stackTraceLimit = 0
|
||||
|
||||
const error = errorFactory()
|
||||
|
||||
if (hasWritableStackTraceLimit)
|
||||
errorConstructor.stackTraceLimit = originalStackTraceLimit
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
function noSSRError(errorMessage?: string, nextjsDigest = 'BAILOUT_TO_CLIENT_SIDE_RENDERING') {
|
||||
const error = createStacklessError(() => new Error(errorMessage)) as Error & {
|
||||
digest?: string
|
||||
recoverableError?: string
|
||||
}
|
||||
|
||||
error.digest = nextjsDigest
|
||||
error.recoverableError = 'NO_SSR'
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
function getServerSnapshotWithoutServerValue(): never {
|
||||
throw noSSRError(`[${HOOK_NAME}] cannot be used on the server without a serverValue`)
|
||||
}
|
||||
|
||||
function rawSerializer<T>(value: T): string {
|
||||
return value as string
|
||||
}
|
||||
|
||||
function rawDeserializer<T>(value: string): T {
|
||||
return value as T
|
||||
}
|
||||
|
||||
function isStorageSetter<T>(
|
||||
value: React.SetStateAction<T | null>,
|
||||
): value is (previousState: T | null) => T | null {
|
||||
return typeof value === 'function'
|
||||
}
|
||||
|
||||
const dispatchStorageEvent = typeof window === 'undefined'
|
||||
? noop
|
||||
: (key: string) => {
|
||||
window.dispatchEvent(new CustomEvent<string>(FOXACT_LOCAL_STORAGE_EVENT_KEY, { detail: key }))
|
||||
}
|
||||
|
||||
const setStorageItem = typeof window === 'undefined'
|
||||
? noop
|
||||
: (key: string, value: string) => {
|
||||
try {
|
||||
window.localStorage.setItem(key, value)
|
||||
}
|
||||
catch {
|
||||
console.warn(`[${HOOK_NAME}] Failed to set value to localStorage, it might be blocked`)
|
||||
}
|
||||
finally {
|
||||
dispatchStorageEvent(key)
|
||||
}
|
||||
}
|
||||
|
||||
const removeStorageItem = typeof window === 'undefined'
|
||||
? noop
|
||||
: (key: string) => {
|
||||
try {
|
||||
window.localStorage.removeItem(key)
|
||||
}
|
||||
catch {
|
||||
console.warn(`[${HOOK_NAME}] Failed to remove value from localStorage, it might be blocked`)
|
||||
}
|
||||
finally {
|
||||
dispatchStorageEvent(key)
|
||||
}
|
||||
}
|
||||
|
||||
function getStorageItem(key: string) {
|
||||
if (typeof window === 'undefined')
|
||||
return null
|
||||
|
||||
try {
|
||||
return window.localStorage.getItem(key)
|
||||
}
|
||||
catch {
|
||||
console.warn(`[${HOOK_NAME}] Failed to get value from localStorage, it might be blocked`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getStorageOptions<T>(
|
||||
options: UseLocalStorageRawOption | UseLocalStorageParserOption<T>,
|
||||
) {
|
||||
return options.raw
|
||||
? {
|
||||
serializer: rawSerializer<T>,
|
||||
deserializer: rawDeserializer<T>,
|
||||
}
|
||||
: {
|
||||
serializer: options.serializer,
|
||||
deserializer: options.deserializer,
|
||||
}
|
||||
}
|
||||
|
||||
const defaultStorageOptions = {
|
||||
raw: false,
|
||||
serializer: JSON.stringify,
|
||||
deserializer: JSON.parse,
|
||||
} satisfies UseLocalStorageParserOption<unknown>
|
||||
|
||||
/** @see https://foxact.skk.moe/use-local-storage */
|
||||
export const useSetLocalStorage = <T>(
|
||||
key: string,
|
||||
options: UseLocalStorageRawOption | UseLocalStorageParserOption<T> = defaultStorageOptions as UseLocalStorageParserOption<T>,
|
||||
) => {
|
||||
const { serializer, deserializer } = getStorageOptions(options)
|
||||
|
||||
return useCallback((value: React.SetStateAction<T | null>) => {
|
||||
try {
|
||||
let nextState: T | null
|
||||
if (isStorageSetter(value)) {
|
||||
const currentRaw = getStorageItem(key)
|
||||
const currentState = currentRaw === null ? null : deserializer(currentRaw)
|
||||
nextState = value(currentState)
|
||||
}
|
||||
else {
|
||||
nextState = value
|
||||
}
|
||||
|
||||
if (nextState === null)
|
||||
removeStorageItem(key)
|
||||
else
|
||||
setStorageItem(key, serializer(nextState))
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}, [key, serializer, deserializer])
|
||||
}
|
||||
|
||||
function useLocalStorageValue<T>(
|
||||
key: string,
|
||||
serverValue: NotUndefined<T>,
|
||||
options?: UseLocalStorageRawOption | UseLocalStorageParserOption<T>,
|
||||
): T
|
||||
function useLocalStorageValue<T>(
|
||||
key: string,
|
||||
serverValue?: undefined,
|
||||
options?: UseLocalStorageRawOption | UseLocalStorageParserOption<T>,
|
||||
): T | null
|
||||
function useLocalStorageValue<T>(
|
||||
key: string,
|
||||
serverValue?: NotUndefined<T>,
|
||||
options: UseLocalStorageRawOption | UseLocalStorageParserOption<T> = defaultStorageOptions as UseLocalStorageParserOption<T>,
|
||||
): T | null {
|
||||
const subscribeToSpecificKeyOfLocalStorage = useCallback((callback: () => void) => {
|
||||
if (typeof window === 'undefined')
|
||||
return noop
|
||||
|
||||
const handleStorageEvent = (event: StorageEvent) => {
|
||||
if (!('key' in event) || event.key === key)
|
||||
callback()
|
||||
}
|
||||
const handleCustomStorageEvent: EventListener = (event) => {
|
||||
if (event instanceof CustomEvent && event.detail === key)
|
||||
callback()
|
||||
}
|
||||
|
||||
window.addEventListener('storage', handleStorageEvent)
|
||||
window.addEventListener(FOXACT_LOCAL_STORAGE_EVENT_KEY, handleCustomStorageEvent)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageEvent)
|
||||
window.removeEventListener(FOXACT_LOCAL_STORAGE_EVENT_KEY, handleCustomStorageEvent)
|
||||
}
|
||||
}, [key])
|
||||
|
||||
const { serializer, deserializer } = getStorageOptions(options)
|
||||
const getClientSnapshot = () => getStorageItem(key)
|
||||
const getServerSnapshot = serverValue === undefined
|
||||
? getServerSnapshotWithoutServerValue
|
||||
: () => serializer(serverValue)
|
||||
|
||||
const store = useSyncExternalStore(
|
||||
subscribeToSpecificKeyOfLocalStorage,
|
||||
getClientSnapshot,
|
||||
getServerSnapshot,
|
||||
)
|
||||
|
||||
const deserialized = useMemo(() => (store === null ? null : deserializer(store)), [store, deserializer])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (getStorageItem(key) === null && serverValue !== undefined)
|
||||
setStorageItem(key, serializer(serverValue))
|
||||
}, [key, serializer, serverValue])
|
||||
|
||||
return deserialized === null
|
||||
? (serverValue === undefined ? null : serverValue)
|
||||
: deserialized
|
||||
}
|
||||
|
||||
function useLocalStorage<T>(
|
||||
key: string,
|
||||
serverValue: NotUndefined<T>,
|
||||
options?: UseLocalStorageRawOption | UseLocalStorageParserOption<T>,
|
||||
): StateHookTuple<T>
|
||||
function useLocalStorage<T>(
|
||||
key: string,
|
||||
serverValue?: undefined,
|
||||
options?: UseLocalStorageRawOption | UseLocalStorageParserOption<T>,
|
||||
): StateHookTuple<T | null>
|
||||
/** @see https://foxact.skk.moe/use-local-storage */
|
||||
function useLocalStorage<T>(
|
||||
key: string,
|
||||
serverValue?: NotUndefined<T>,
|
||||
options: UseLocalStorageRawOption | UseLocalStorageParserOption<T> = defaultStorageOptions as UseLocalStorageParserOption<T>,
|
||||
): StateHookTuple<T> | StateHookTuple<T | null> {
|
||||
const value = useLocalStorageValue<T>(key, serverValue!, options)
|
||||
const setState = useSetLocalStorage<T>(key, options)
|
||||
|
||||
return [value, setState] as const
|
||||
}
|
||||
|
||||
export { useLocalStorage }
|
||||
@ -1,44 +0,0 @@
|
||||
import * as reactExports from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'
|
||||
|
||||
// useIsomorphicInsertionEffect
|
||||
const useInsertionEffect
|
||||
= typeof window === 'undefined'
|
||||
// useInsertionEffect is only available in React 18+
|
||||
|
||||
? useEffect
|
||||
: reactExports.useInsertionEffect || useLayoutEffect
|
||||
|
||||
/**
|
||||
* @see https://foxact.skk.moe/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired
|
||||
* Similar to useCallback, with a few subtle differences:
|
||||
* - The returned function is a stable reference, and will always be the same between renders
|
||||
* - No dependency lists required
|
||||
* - Properties or state accessed within the callback will always be "current"
|
||||
*/
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
export function useStableHandler<Args extends any[], Result>(
|
||||
callback: (...args: Args) => Result,
|
||||
): typeof callback {
|
||||
// Keep track of the latest callback:
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
const latestRef = useRef<typeof callback>(shouldNotBeInvokedBeforeMount as any)
|
||||
useInsertionEffect(() => {
|
||||
latestRef.current = callback
|
||||
}, [callback])
|
||||
|
||||
return useCallback<typeof callback>((...args) => {
|
||||
const fn = latestRef.current
|
||||
return fn(...args)
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Render methods should be pure, especially when concurrency is used,
|
||||
* so we will throw this error if the callback is called while rendering.
|
||||
*/
|
||||
function shouldNotBeInvokedBeforeMount() {
|
||||
throw new Error(
|
||||
'foxact: the stablized handler cannot be invoked before the component has mounted.',
|
||||
)
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { useCallback as useCallbackFromReact } from 'react'
|
||||
|
||||
/** @see https://foxact.skk.moe/use-typescript-happy-callback */
|
||||
const useTypeScriptHappyCallback: <Args extends unknown[], R>(
|
||||
fn: (...args: Args) => R,
|
||||
deps: React.DependencyList,
|
||||
) => (...args: Args) => R = useCallbackFromReact
|
||||
|
||||
/** @see https://foxact.skk.moe/use-typescript-happy-callback */
|
||||
export const useCallback = useTypeScriptHappyCallback
|
||||
@ -80,7 +80,6 @@
|
||||
"abcjs": "catalog:",
|
||||
"ahooks": "catalog:",
|
||||
"class-variance-authority": "catalog:",
|
||||
"client-only": "catalog:",
|
||||
"cmdk": "catalog:",
|
||||
"copy-to-clipboard": "catalog:",
|
||||
"cron-parser": "catalog:",
|
||||
@ -95,6 +94,7 @@
|
||||
"emoji-mart": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"fast-deep-equal": "catalog:",
|
||||
"foxact": "catalog:",
|
||||
"fuse.js": "catalog:",
|
||||
"hast-util-to-jsx-runtime": "catalog:",
|
||||
"html-entities": "catalog:",
|
||||
|
||||
@ -76,7 +76,7 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
// mock custom clipboard hook - wraps writeTextToClipboard with fallback
|
||||
vi.mock('@/hooks/use-clipboard', () => ({
|
||||
vi.mock('foxact/use-clipboard', () => ({
|
||||
useClipboard: () => ({
|
||||
copy: vi.fn(),
|
||||
copied: false,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user