mirror of
https://github.com/langgenius/dify.git
synced 2026-04-28 20:17:29 +08:00
Merge remote-tracking branch 'myori/main' into feat/collaboration2
This commit is contained in:
commit
682c93f262
10
.github/workflows/style.yml
vendored
10
.github/workflows/style.yml
vendored
@ -106,8 +106,9 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: |
|
run: |
|
||||||
pnpm run lint:report
|
pnpm run lint:ci
|
||||||
continue-on-error: true
|
# pnpm run lint:report
|
||||||
|
# continue-on-error: true
|
||||||
|
|
||||||
# - name: Annotate Code
|
# - name: Annotate Code
|
||||||
# if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'pull_request'
|
# if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'pull_request'
|
||||||
@ -126,11 +127,6 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: pnpm run knip
|
run: pnpm run knip
|
||||||
|
|
||||||
- name: Web build check
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
working-directory: ./web
|
|
||||||
run: pnpm run build
|
|
||||||
|
|
||||||
superlinter:
|
superlinter:
|
||||||
name: SuperLinter
|
name: SuperLinter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
45
.github/workflows/web-tests.yml
vendored
45
.github/workflows/web-tests.yml
vendored
@ -366,3 +366,48 @@ jobs:
|
|||||||
path: web/coverage
|
path: web/coverage
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
web-build:
|
||||||
|
name: Web Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Check changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v47
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
web/**
|
||||||
|
.github/workflows/web-tests.yml
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
package_json_file: web/package.json
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Setup NodeJS
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Web dependencies
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Web build check
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
run: pnpm run build
|
||||||
|
|||||||
56
web/__mocks__/zustand.ts
Normal file
56
web/__mocks__/zustand.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type * as ZustandExportedTypes from 'zustand'
|
||||||
|
import { act } from '@testing-library/react'
|
||||||
|
|
||||||
|
export * from 'zustand'
|
||||||
|
|
||||||
|
const { create: actualCreate, createStore: actualCreateStore }
|
||||||
|
// eslint-disable-next-line antfu/no-top-level-await
|
||||||
|
= await vi.importActual<typeof ZustandExportedTypes>('zustand')
|
||||||
|
|
||||||
|
export const storeResetFns = new Set<() => void>()
|
||||||
|
|
||||||
|
const createUncurried = <T>(
|
||||||
|
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||||
|
) => {
|
||||||
|
const store = actualCreate(stateCreator)
|
||||||
|
const initialState = store.getInitialState()
|
||||||
|
storeResetFns.add(() => {
|
||||||
|
store.setState(initialState, true)
|
||||||
|
})
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export const create = (<T>(
|
||||||
|
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||||
|
) => {
|
||||||
|
return typeof stateCreator === 'function'
|
||||||
|
? createUncurried(stateCreator)
|
||||||
|
: createUncurried
|
||||||
|
}) as typeof ZustandExportedTypes.create
|
||||||
|
|
||||||
|
const createStoreUncurried = <T>(
|
||||||
|
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||||
|
) => {
|
||||||
|
const store = actualCreateStore(stateCreator)
|
||||||
|
const initialState = store.getInitialState()
|
||||||
|
storeResetFns.add(() => {
|
||||||
|
store.setState(initialState, true)
|
||||||
|
})
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createStore = (<T>(
|
||||||
|
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||||
|
) => {
|
||||||
|
return typeof stateCreator === 'function'
|
||||||
|
? createStoreUncurried(stateCreator)
|
||||||
|
: createStoreUncurried
|
||||||
|
}) as typeof ZustandExportedTypes.createStore
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
act(() => {
|
||||||
|
storeResetFns.forEach((resetFn) => {
|
||||||
|
resetFn()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -3,9 +3,7 @@ import type { App } from '@/types/app'
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import useAccessControlStore from '@/context/access-control-store'
|
import useAccessControlStore from '@/context/access-control-store'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
|
||||||
import Toast from '../../base/toast'
|
import Toast from '../../base/toast'
|
||||||
import AccessControlDialog from './access-control-dialog'
|
import AccessControlDialog from './access-control-dialog'
|
||||||
import AccessControlItem from './access-control-item'
|
import AccessControlItem from './access-control-item'
|
||||||
@ -105,22 +103,6 @@ const memberSubject: Subject = {
|
|||||||
accountData: baseMember,
|
accountData: baseMember,
|
||||||
} as Subject
|
} as Subject
|
||||||
|
|
||||||
const resetAccessControlStore = () => {
|
|
||||||
useAccessControlStore.setState({
|
|
||||||
appId: '',
|
|
||||||
specificGroups: [],
|
|
||||||
specificMembers: [],
|
|
||||||
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
|
||||||
selectedGroupsForBreadcrumb: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetGlobalStore = () => {
|
|
||||||
useGlobalPublicStore.setState({
|
|
||||||
systemFeatures: defaultSystemFeatures,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
class MockIntersectionObserver {
|
class MockIntersectionObserver {
|
||||||
observe = vi.fn(() => undefined)
|
observe = vi.fn(() => undefined)
|
||||||
@ -132,9 +114,6 @@ beforeAll(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
|
||||||
resetAccessControlStore()
|
|
||||||
resetGlobalStore()
|
|
||||||
mockMutateAsync.mockResolvedValue(undefined)
|
mockMutateAsync.mockResolvedValue(undefined)
|
||||||
mockUseUpdateAccessMode.mockReturnValue({
|
mockUseUpdateAccessMode.mockReturnValue({
|
||||||
isPending: false,
|
isPending: false,
|
||||||
|
|||||||
@ -189,6 +189,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{isFetchingNextPage && <Loading />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -21,15 +21,15 @@ export const AppCardSkeleton = React.memo(({ count = 6 }: AppCardSkeletonProps)
|
|||||||
>
|
>
|
||||||
<SkeletonContainer className="h-full">
|
<SkeletonContainer className="h-full">
|
||||||
<SkeletonRow>
|
<SkeletonRow>
|
||||||
<SkeletonRectangle className="h-10 w-10 rounded-lg" />
|
<SkeletonRectangle className="h-10 w-10 animate-pulse rounded-lg" />
|
||||||
<div className="flex flex-1 flex-col gap-1">
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
<SkeletonRectangle className="h-4 w-2/3" />
|
<SkeletonRectangle className="h-4 w-2/3 animate-pulse" />
|
||||||
<SkeletonRectangle className="h-3 w-1/3" />
|
<SkeletonRectangle className="h-3 w-1/3 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</SkeletonRow>
|
</SkeletonRow>
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
<SkeletonRectangle className="h-3 w-full" />
|
<SkeletonRectangle className="h-3 w-full animate-pulse" />
|
||||||
<SkeletonRectangle className="h-3 w-4/5" />
|
<SkeletonRectangle className="h-3 w-4/5 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</SkeletonContainer>
|
</SkeletonContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -281,6 +281,9 @@ const List = () => {
|
|||||||
// No apps - show empty state
|
// No apps - show empty state
|
||||||
return <Empty />
|
return <Empty />
|
||||||
})()}
|
})()}
|
||||||
|
{isFetchingNextPage && (
|
||||||
|
<AppCardSkeleton count={3} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCurrentWorkspaceEditor && (
|
{isCurrentWorkspaceEditor && (
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import * as React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { cn } from '@/utils/classnames'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
type ILoadingProps = {
|
type ILoadingProps = {
|
||||||
type?: 'area' | 'app'
|
type?: 'area' | 'app'
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
const Loading = (
|
|
||||||
{ type = 'area' }: ILoadingProps = { type: 'area' },
|
const Loading = (props?: ILoadingProps) => {
|
||||||
) => {
|
const { type = 'area', className } = props || {}
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex w-full items-center justify-center ${type === 'app' ? 'h-full' : ''}`}
|
className={cn(
|
||||||
|
'flex w-full items-center justify-center',
|
||||||
|
type === 'app' && 'h-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label={t('loading', { ns: 'appApi' })}
|
aria-label={t('loading', { ns: 'appApi' })}
|
||||||
@ -37,4 +41,5 @@ const Loading = (
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Loading
|
export default Loading
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||||
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||||
import DatasetCard from './dataset-card'
|
import DatasetCard from './dataset-card'
|
||||||
@ -25,6 +26,7 @@ const Datasets = ({
|
|||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
isFetchingNextPage,
|
||||||
} = useDatasetList({
|
} = useDatasetList({
|
||||||
initialPage: 1,
|
initialPage: 1,
|
||||||
tag_ids: tags,
|
tag_ids: tags,
|
||||||
@ -60,6 +62,7 @@ const Datasets = ({
|
|||||||
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
|
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
|
||||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} />),
|
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} />),
|
||||||
))}
|
))}
|
||||||
|
{isFetchingNextPage && <Loading />}
|
||||||
<div ref={anchorRef} className="h-0" />
|
<div ref={anchorRef} className="h-0" />
|
||||||
</nav>
|
</nav>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ const AppNav = () => {
|
|||||||
data: appsData,
|
data: appsData,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
refetch,
|
refetch,
|
||||||
} = useInfiniteAppList({
|
} = useInfiniteAppList({
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -111,6 +112,7 @@ const AppNav = () => {
|
|||||||
createText={t('menus.newApp', { ns: 'common' })}
|
createText={t('menus.newApp', { ns: 'common' })}
|
||||||
onCreate={openModal}
|
onCreate={openModal}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
|
isLoadingMore={isFetchingNextPage}
|
||||||
/>
|
/>
|
||||||
<CreateAppModal
|
<CreateAppModal
|
||||||
show={showNewAppDialog}
|
show={showNewAppDialog}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const DatasetNav = () => {
|
|||||||
data: datasetList,
|
data: datasetList,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
} = useDatasetList({
|
} = useDatasetList({
|
||||||
initialPage: 1,
|
initialPage: 1,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
@ -93,6 +94,7 @@ const DatasetNav = () => {
|
|||||||
createText={t('menus.newDataset', { ns: 'common' })}
|
createText={t('menus.newDataset', { ns: 'common' })}
|
||||||
onCreate={() => router.push(createRoute)}
|
onCreate={() => router.push(createRoute)}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
|
isLoadingMore={isFetchingNextPage}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ const Nav = ({
|
|||||||
createText,
|
createText,
|
||||||
onCreate,
|
onCreate,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
|
isLoadingMore,
|
||||||
isApp,
|
isApp,
|
||||||
}: INavProps) => {
|
}: INavProps) => {
|
||||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||||
@ -81,6 +82,7 @@ const Nav = ({
|
|||||||
createText={createText}
|
createText={createText}
|
||||||
onCreate={onCreate}
|
onCreate={onCreate}
|
||||||
onLoadMore={onLoadMore}
|
onLoadMore={onLoadMore}
|
||||||
|
isLoadingMore={isLoadingMore}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
|||||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
|
||||||
@ -34,9 +35,10 @@ export type INavSelectorProps = {
|
|||||||
isApp?: boolean
|
isApp?: boolean
|
||||||
onCreate: (state: string) => void
|
onCreate: (state: string) => void
|
||||||
onLoadMore?: () => void
|
onLoadMore?: () => void
|
||||||
|
isLoadingMore?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore }: INavSelectorProps) => {
|
const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore, isLoadingMore }: INavSelectorProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||||
@ -106,6 +108,11 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="flex justify-center py-2">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isApp && isCurrentWorkspaceEditor && (
|
{!isApp && isCurrentWorkspaceEditor && (
|
||||||
<MenuItem as="div" className="w-full p-1">
|
<MenuItem as="div" className="w-full p-1">
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const ListWrapper = ({
|
|||||||
marketplaceCollections,
|
marketplaceCollections,
|
||||||
marketplaceCollectionPluginsMap,
|
marketplaceCollectionPluginsMap,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isFetchingNextPage,
|
||||||
page,
|
page,
|
||||||
} = useMarketplaceData()
|
} = useMarketplaceData()
|
||||||
|
|
||||||
@ -53,6 +54,11 @@ const ListWrapper = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
isFetchingNextPage && (
|
||||||
|
<Loading className="my-3" />
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function useMarketplaceData() {
|
|||||||
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
|
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
|
||||||
|
|
||||||
const pluginsQuery = useMarketplacePlugins(queryParams)
|
const pluginsQuery = useMarketplacePlugins(queryParams)
|
||||||
const { hasNextPage, fetchNextPage, isFetching } = pluginsQuery
|
const { hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = pluginsQuery
|
||||||
|
|
||||||
const handlePageChange = useCallback(() => {
|
const handlePageChange = useCallback(() => {
|
||||||
if (hasNextPage && !isFetching)
|
if (hasNextPage && !isFetching)
|
||||||
@ -50,5 +50,6 @@ export function useMarketplaceData() {
|
|||||||
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
|
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
|
||||||
page: pluginsQuery.data?.pages.length || 1,
|
page: pluginsQuery.data?.pages.length || 1,
|
||||||
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
|
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
|
||||||
|
isFetchingNextPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -144,17 +144,6 @@ describe('constant.ts - Type Definitions', () => {
|
|||||||
|
|
||||||
// ==================== store.ts Tests ====================
|
// ==================== store.ts Tests ====================
|
||||||
describe('store.ts - Zustand Store', () => {
|
describe('store.ts - Zustand Store', () => {
|
||||||
beforeEach(() => {
|
|
||||||
// Reset store to initial state
|
|
||||||
const { setState } = useStore
|
|
||||||
setState({
|
|
||||||
tagList: [],
|
|
||||||
categoryList: [],
|
|
||||||
showTagManagementModal: false,
|
|
||||||
showCategoryManagementModal: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Initial State', () => {
|
describe('Initial State', () => {
|
||||||
it('should have empty tagList initially', () => {
|
it('should have empty tagList initially', () => {
|
||||||
const { result } = renderHook(() => useStore(state => state.tagList))
|
const { result } = renderHook(() => useStore(state => state.tagList))
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import { useDebounceFn } from 'ahooks'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
import { renderI18nObject } from '@/i18n-config'
|
import { renderI18nObject } from '@/i18n-config'
|
||||||
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||||
import Loading from '../../base/loading'
|
|
||||||
import { PluginSource } from '../types'
|
import { PluginSource } from '../types'
|
||||||
import { usePluginPageContext } from './context'
|
import { usePluginPageContext } from './context'
|
||||||
import Empty from './empty'
|
import Empty from './empty'
|
||||||
@ -107,12 +107,17 @@ const PluginsPanel = () => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<List pluginList={filteredList || []} />
|
<List pluginList={filteredList || []} />
|
||||||
</div>
|
</div>
|
||||||
{!isLastPage && !isFetching && (
|
{!isLastPage && (
|
||||||
<Button onClick={loadNextPage}>
|
<div className="flex justify-center py-4">
|
||||||
{t('common.loadMore', { ns: 'workflow' })}
|
{isFetching
|
||||||
</Button>
|
? <Loading className="size-8" />
|
||||||
|
: (
|
||||||
|
<Button onClick={loadNextPage}>
|
||||||
|
{t('common.loadMore', { ns: 'workflow' })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isFetching && <div className="system-md-semibold text-text-secondary">{t('detail.loading', { ns: 'appLog' })}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
|
|||||||
@ -134,13 +134,6 @@ describe('BUILTIN_TOOLS_ARRAY', () => {
|
|||||||
// Store Tests
|
// Store Tests
|
||||||
// ================================
|
// ================================
|
||||||
describe('useReadmePanelStore', () => {
|
describe('useReadmePanelStore', () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks()
|
|
||||||
// Reset store state before each test
|
|
||||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
|
||||||
setCurrentPluginDetail()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Initial State', () => {
|
describe('Initial State', () => {
|
||||||
it('should have undefined currentPluginDetail initially', () => {
|
it('should have undefined currentPluginDetail initially', () => {
|
||||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||||
@ -228,13 +221,6 @@ describe('useReadmePanelStore', () => {
|
|||||||
// ReadmeEntrance Component Tests
|
// ReadmeEntrance Component Tests
|
||||||
// ================================
|
// ================================
|
||||||
describe('ReadmeEntrance', () => {
|
describe('ReadmeEntrance', () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks()
|
|
||||||
// Reset store state
|
|
||||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
|
||||||
setCurrentPluginDetail()
|
|
||||||
})
|
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// Rendering Tests
|
// Rendering Tests
|
||||||
// ================================
|
// ================================
|
||||||
@ -417,11 +403,6 @@ describe('ReadmeEntrance', () => {
|
|||||||
// ================================
|
// ================================
|
||||||
describe('ReadmePanel', () => {
|
describe('ReadmePanel', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
|
||||||
// Reset store state
|
|
||||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
|
||||||
setCurrentPluginDetail()
|
|
||||||
// Reset mock
|
|
||||||
mockUsePluginReadme.mockReturnValue({
|
mockUsePluginReadme.mockReturnValue({
|
||||||
data: null,
|
data: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|||||||
5107
web/eslint-suppressions.json
Normal file
5107
web/eslint-suppressions.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,30 +9,15 @@ import difyI18n from './eslint-rules/index.js'
|
|||||||
export default antfu(
|
export default antfu(
|
||||||
{
|
{
|
||||||
react: {
|
react: {
|
||||||
|
reactCompiler: true,
|
||||||
overrides: {
|
overrides: {
|
||||||
'react/no-context-provider': 'off',
|
'react/no-context-provider': 'off',
|
||||||
'react/no-forward-ref': 'off',
|
'react/no-forward-ref': 'off',
|
||||||
'react/no-use-context': 'off',
|
'react/no-use-context': 'off',
|
||||||
'react/prefer-namespace-import': 'error',
|
|
||||||
|
|
||||||
// React Compiler rules
|
|
||||||
// Set to warn for gradual adoption
|
|
||||||
'react-hooks/config': 'warn',
|
|
||||||
'react-hooks/error-boundaries': 'warn',
|
|
||||||
'react-hooks/component-hook-factories': 'warn',
|
|
||||||
'react-hooks/gating': 'warn',
|
|
||||||
'react-hooks/globals': 'warn',
|
|
||||||
'react-hooks/immutability': 'warn',
|
|
||||||
'react-hooks/preserve-manual-memoization': 'warn',
|
|
||||||
'react-hooks/purity': 'warn',
|
|
||||||
'react-hooks/refs': 'warn',
|
|
||||||
// prefer react-hooks-extra/no-direct-set-state-in-use-effect
|
// prefer react-hooks-extra/no-direct-set-state-in-use-effect
|
||||||
'react-hooks/set-state-in-effect': 'off',
|
'react-hooks/set-state-in-effect': 'off',
|
||||||
'react-hooks/set-state-in-render': 'warn',
|
'react-hooks-extra/no-direct-set-state-in-use-effect': 'error',
|
||||||
'react-hooks/static-components': 'warn',
|
|
||||||
'react-hooks/unsupported-syntax': 'warn',
|
|
||||||
'react-hooks/use-memo': 'warn',
|
|
||||||
'react-hooks/incompatible-library': 'warn',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nextjs: true,
|
nextjs: true,
|
||||||
@ -40,7 +25,7 @@ export default antfu(
|
|||||||
typescript: {
|
typescript: {
|
||||||
overrides: {
|
overrides: {
|
||||||
'ts/consistent-type-definitions': ['error', 'type'],
|
'ts/consistent-type-definitions': ['error', 'type'],
|
||||||
'ts/no-explicit-any': 'warn',
|
'ts/no-explicit-any': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
@ -54,6 +39,11 @@ export default antfu(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'node/prefer-global/process': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
settings: {
|
settings: {
|
||||||
@ -62,32 +52,6 @@ export default antfu(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// downgrade some rules from error to warn for gradual adoption
|
|
||||||
// we should fix these in following pull requests
|
|
||||||
{
|
|
||||||
// @keep-sorted
|
|
||||||
rules: {
|
|
||||||
'next/inline-script-id': 'warn',
|
|
||||||
'no-console': 'warn',
|
|
||||||
'no-irregular-whitespace': 'warn',
|
|
||||||
'node/prefer-global/buffer': 'warn',
|
|
||||||
'node/prefer-global/process': 'warn',
|
|
||||||
'react/no-create-ref': 'warn',
|
|
||||||
'react/no-missing-key': 'warn',
|
|
||||||
'react/no-nested-component-definitions': 'warn',
|
|
||||||
'regexp/no-dupe-disjunctions': 'warn',
|
|
||||||
'regexp/no-super-linear-backtracking': 'warn',
|
|
||||||
'regexp/no-unused-capturing-group': 'warn',
|
|
||||||
'regexp/no-useless-assertions': 'warn',
|
|
||||||
'regexp/no-useless-quantifier': 'warn',
|
|
||||||
'style/multiline-ternary': 'warn',
|
|
||||||
'test/no-identical-title': 'warn',
|
|
||||||
'test/prefer-hooks-in-order': 'warn',
|
|
||||||
'ts/no-empty-object-type': 'warn',
|
|
||||||
'unicorn/prefer-number-properties': 'warn',
|
|
||||||
'unused-imports/no-unused-vars': 'warn',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
storybook.configs['flat/recommended'],
|
storybook.configs['flat/recommended'],
|
||||||
...pluginQuery.configs['flat/recommended'],
|
...pluginQuery.configs['flat/recommended'],
|
||||||
// sonar
|
// sonar
|
||||||
@ -178,19 +142,19 @@ export default antfu(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// dify i18n namespace migration
|
// dify i18n namespace migration
|
||||||
{
|
// {
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
// files: ['**/*.ts', '**/*.tsx'],
|
||||||
ignores: ['eslint-rules/**', 'i18n/**', 'i18n-config/**'],
|
// ignores: ['eslint-rules/**', 'i18n/**', 'i18n-config/**'],
|
||||||
plugins: {
|
// plugins: {
|
||||||
'dify-i18n': difyI18n,
|
// 'dify-i18n': difyI18n,
|
||||||
},
|
// },
|
||||||
rules: {
|
// rules: {
|
||||||
// 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
|
// // 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
|
||||||
'dify-i18n/no-as-any-in-t': 'error',
|
// 'dify-i18n/no-as-any-in-t': 'error',
|
||||||
// 'dify-i18n/no-legacy-namespace-prefix': 'error',
|
// // 'dify-i18n/no-legacy-namespace-prefix': 'error',
|
||||||
// 'dify-i18n/require-ns-option': 'error',
|
// // 'dify-i18n/require-ns-option': 'error',
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
// i18n JSON validation rules
|
// i18n JSON validation rules
|
||||||
{
|
{
|
||||||
files: ['i18n/**/*.json'],
|
files: ['i18n/**/*.json'],
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
"build:docker": "next build && node scripts/optimize-standalone.js",
|
"build:docker": "next build && node scripts/optimize-standalone.js",
|
||||||
"start": "node ./scripts/copy-and-start.mjs",
|
"start": "node ./scripts/copy-and-start.mjs",
|
||||||
"lint": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
|
"lint": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
|
||||||
|
"lint:ci": "pnpm lint --concurrency 3",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"lint:quiet": "pnpm lint --quiet",
|
"lint:quiet": "pnpm lint --quiet",
|
||||||
"lint:complexity": "pnpm lint --rule 'complexity: [error, {max: 15}]' --quiet",
|
"lint:complexity": "pnpm lint --rule 'complexity: [error, {max: 15}]' --quiet",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"target": "es2015",
|
"target": "es2022",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
|
|||||||
@ -85,6 +85,10 @@ afterEach(() => {
|
|||||||
// mock next/image to avoid width/height requirements for data URLs
|
// mock next/image to avoid width/height requirements for data URLs
|
||||||
vi.mock('next/image')
|
vi.mock('next/image')
|
||||||
|
|
||||||
|
// mock zustand - auto-resets all stores after each test
|
||||||
|
// Based on official Zustand testing guide: https://zustand.docs.pmnd.rs/guides/testing
|
||||||
|
vi.mock('zustand')
|
||||||
|
|
||||||
// mock react-i18next
|
// mock react-i18next
|
||||||
vi.mock('react-i18next', async () => {
|
vi.mock('react-i18next', async () => {
|
||||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user