mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Merge branch 'main' into feat/summary-index
This commit is contained in:
@ -65,15 +65,17 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
<div className="text-xs text-text-secondary">
|
||||
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer text-xs font-medium text-text-accent hover:underline"
|
||||
<a
|
||||
href={triggerDocUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block cursor-pointer text-xs font-medium text-text-accent hover:underline"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
window.open(triggerDocUrl, '_blank')
|
||||
}}
|
||||
>
|
||||
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
), [t, triggerDocUrl])
|
||||
|
||||
|
||||
@ -3,9 +3,7 @@ import type { App } from '@/types/app'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import useAccessControlStore from '@/context/access-control-store'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import Toast from '../../base/toast'
|
||||
import AccessControlDialog from './access-control-dialog'
|
||||
import AccessControlItem from './access-control-item'
|
||||
@ -105,22 +103,6 @@ const memberSubject: Subject = {
|
||||
accountData: baseMember,
|
||||
} as Subject
|
||||
|
||||
const resetAccessControlStore = () => {
|
||||
useAccessControlStore.setState({
|
||||
appId: '',
|
||||
specificGroups: [],
|
||||
specificMembers: [],
|
||||
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
selectedGroupsForBreadcrumb: [],
|
||||
})
|
||||
}
|
||||
|
||||
const resetGlobalStore = () => {
|
||||
useGlobalPublicStore.setState({
|
||||
systemFeatures: defaultSystemFeatures,
|
||||
})
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
class MockIntersectionObserver {
|
||||
observe = vi.fn(() => undefined)
|
||||
@ -132,9 +114,6 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetAccessControlStore()
|
||||
resetGlobalStore()
|
||||
mockMutateAsync.mockResolvedValue(undefined)
|
||||
mockUseUpdateAccessMode.mockReturnValue({
|
||||
isPending: false,
|
||||
|
||||
@ -189,6 +189,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
{isFetchingNextPage && <Loading />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -21,15 +21,15 @@ export const AppCardSkeleton = React.memo(({ count = 6 }: AppCardSkeletonProps)
|
||||
>
|
||||
<SkeletonContainer className="h-full">
|
||||
<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">
|
||||
<SkeletonRectangle className="h-4 w-2/3" />
|
||||
<SkeletonRectangle className="h-3 w-1/3" />
|
||||
<SkeletonRectangle className="h-4 w-2/3 animate-pulse" />
|
||||
<SkeletonRectangle className="h-3 w-1/3 animate-pulse" />
|
||||
</div>
|
||||
</SkeletonRow>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<SkeletonRectangle className="h-3 w-full" />
|
||||
<SkeletonRectangle className="h-3 w-4/5" />
|
||||
<SkeletonRectangle className="h-3 w-full animate-pulse" />
|
||||
<SkeletonRectangle className="h-3 w-4/5 animate-pulse" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
</div>
|
||||
|
||||
@ -248,6 +248,9 @@ const List = () => {
|
||||
// No apps - show empty state
|
||||
return <Empty />
|
||||
})()}
|
||||
{isFetchingNextPage && (
|
||||
<AppCardSkeleton count={3} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCurrentWorkspaceEditor && (
|
||||
|
||||
@ -1,21 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { cn } from '@/utils/classnames'
|
||||
import './style.css'
|
||||
|
||||
type ILoadingProps = {
|
||||
type?: 'area' | 'app'
|
||||
className?: string
|
||||
}
|
||||
const Loading = (
|
||||
{ type = 'area' }: ILoadingProps = { type: 'area' },
|
||||
) => {
|
||||
|
||||
const Loading = (props?: ILoadingProps) => {
|
||||
const { type = 'area', className } = props || {}
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<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"
|
||||
aria-live="polite"
|
||||
aria-label={t('loading', { ns: 'appApi' })}
|
||||
@ -37,4 +41,5 @@ const Loading = (
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
||||
|
||||
@ -20,6 +20,7 @@ const SearchInput: FC<SearchInputProps> = ({
|
||||
white,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [focus, setFocus] = useState<boolean>(false)
|
||||
const isComposing = useRef<boolean>(false)
|
||||
const [compositionValue, setCompositionValue] = useState<string>('')
|
||||
@ -36,6 +37,7 @@ const SearchInput: FC<SearchInputProps> = ({
|
||||
<RiSearchLine className="h-4 w-4 text-components-input-text-placeholder" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
name="query"
|
||||
className={cn(
|
||||
@ -65,14 +67,17 @@ const SearchInput: FC<SearchInputProps> = ({
|
||||
autoComplete="off"
|
||||
/>
|
||||
{value && (
|
||||
<div
|
||||
className="group/clear flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className="group/clear flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0"
|
||||
onClick={() => {
|
||||
onChange('')
|
||||
inputRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
<RiCloseCircleFill className="h-4 w-4 text-text-quaternary group-hover/clear:text-text-tertiary" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -447,6 +447,10 @@ const Completed: FC<ICompletedProps> = ({
|
||||
setFullScreen(!fullScreen)
|
||||
}, [fullScreen])
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setIsCollapsed(prev => !prev)
|
||||
}, [])
|
||||
|
||||
const viewNewlyAddedChunk = useCallback(async () => {
|
||||
const totalPages = segmentListData?.total_pages || 0
|
||||
const total = segmentListData?.total || 0
|
||||
@ -583,15 +587,16 @@ const Completed: FC<ICompletedProps> = ({
|
||||
return selectedStatus ? 1 : 0
|
||||
}, [selectedStatus])
|
||||
|
||||
const contextValue = useMemo<SegmentListContextValue>(() => ({
|
||||
isCollapsed,
|
||||
fullScreen,
|
||||
toggleFullScreen,
|
||||
currSegment,
|
||||
currChildChunk,
|
||||
}), [isCollapsed, fullScreen, toggleFullScreen, currSegment, currChildChunk])
|
||||
|
||||
return (
|
||||
<SegmentListContext.Provider value={{
|
||||
isCollapsed,
|
||||
fullScreen,
|
||||
toggleFullScreen,
|
||||
currSegment,
|
||||
currChildChunk,
|
||||
}}
|
||||
>
|
||||
<SegmentListContext.Provider value={contextValue}>
|
||||
{/* Menu Bar */}
|
||||
{!isFullDocMode && (
|
||||
<div className={s.docSearchWrapper}>
|
||||
@ -623,7 +628,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
onClear={() => handleInputChange('')}
|
||||
/>
|
||||
<Divider type="vertical" className="mx-3 h-3.5" />
|
||||
<DisplayToggle isCollapsed={isCollapsed} toggleCollapsed={() => setIsCollapsed(!isCollapsed)} />
|
||||
<DisplayToggle isCollapsed={isCollapsed} toggleCollapsed={toggleCollapsed} />
|
||||
</div>
|
||||
)}
|
||||
{/* Segment list */}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { FC } from 'react'
|
||||
import type { SegmentListContextValue } from '..'
|
||||
import * as React from 'react'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -14,13 +15,15 @@ type ChunkContentProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const selectIsCollapsed = (s: SegmentListContextValue) => s.isCollapsed
|
||||
|
||||
const ChunkContent: FC<ChunkContentProps> = ({
|
||||
detail,
|
||||
isFullDocMode,
|
||||
className,
|
||||
}) => {
|
||||
const { answer, content, sign_content } = detail
|
||||
const isCollapsed = useSegmentListContext(s => s.isCollapsed)
|
||||
const isCollapsed = useSegmentListContext(selectIsCollapsed)
|
||||
|
||||
if (answer) {
|
||||
return (
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import DatasetCard from './dataset-card'
|
||||
@ -25,6 +26,7 @@ const Datasets = ({
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
} = useDatasetList({
|
||||
initialPage: 1,
|
||||
tag_ids: tags,
|
||||
@ -60,6 +62,7 @@ const Datasets = ({
|
||||
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
|
||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} />),
|
||||
))}
|
||||
{isFetchingNextPage && <Loading />}
|
||||
<div ref={anchorRef} className="h-0" />
|
||||
</nav>
|
||||
</>
|
||||
|
||||
@ -33,6 +33,7 @@ const AppNav = () => {
|
||||
data: appsData,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
refetch,
|
||||
} = useInfiniteAppList({
|
||||
page: 1,
|
||||
@ -111,6 +112,7 @@ const AppNav = () => {
|
||||
createText={t('menus.newApp', { ns: 'common' })}
|
||||
onCreate={openModal}
|
||||
onLoadMore={handleLoadMore}
|
||||
isLoadingMore={isFetchingNextPage}
|
||||
/>
|
||||
<CreateAppModal
|
||||
show={showNewAppDialog}
|
||||
|
||||
@ -23,6 +23,7 @@ const DatasetNav = () => {
|
||||
data: datasetList,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useDatasetList({
|
||||
initialPage: 1,
|
||||
limit: 30,
|
||||
@ -93,6 +94,7 @@ const DatasetNav = () => {
|
||||
createText={t('menus.newDataset', { ns: 'common' })}
|
||||
onCreate={() => router.push(createRoute)}
|
||||
onLoadMore={handleLoadMore}
|
||||
isLoadingMore={isFetchingNextPage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ const Nav = ({
|
||||
createText,
|
||||
onCreate,
|
||||
onLoadMore,
|
||||
isLoadingMore,
|
||||
isApp,
|
||||
}: INavProps) => {
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
@ -81,6 +82,7 @@ const Nav = ({
|
||||
createText={createText}
|
||||
onCreate={onCreate}
|
||||
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 AppIcon from '@/app/components/base/app-icon'
|
||||
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 { cn } from '@/utils/classnames'
|
||||
|
||||
@ -34,9 +35,10 @@ export type INavSelectorProps = {
|
||||
isApp?: boolean
|
||||
onCreate: (state: string) => 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 router = useRouter()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
@ -106,6 +108,11 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
{isLoadingMore && (
|
||||
<div className="flex justify-center py-2">
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isApp && isCurrentWorkspaceEditor && (
|
||||
<MenuItem as="div" className="w-full p-1">
|
||||
|
||||
@ -19,6 +19,7 @@ const ListWrapper = ({
|
||||
marketplaceCollections,
|
||||
marketplaceCollectionPluginsMap,
|
||||
isLoading,
|
||||
isFetchingNextPage,
|
||||
page,
|
||||
} = useMarketplaceData()
|
||||
|
||||
@ -53,6 +54,11 @@ const ListWrapper = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isFetchingNextPage && (
|
||||
<Loading className="my-3" />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ export function useMarketplaceData() {
|
||||
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
|
||||
|
||||
const pluginsQuery = useMarketplacePlugins(queryParams)
|
||||
const { hasNextPage, fetchNextPage, isFetching } = pluginsQuery
|
||||
const { hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = pluginsQuery
|
||||
|
||||
const handlePageChange = useCallback(() => {
|
||||
if (hasNextPage && !isFetching)
|
||||
@ -50,5 +50,6 @@ export function useMarketplaceData() {
|
||||
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
|
||||
page: pluginsQuery.data?.pages.length || 1,
|
||||
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
|
||||
isFetchingNextPage,
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,17 +144,6 @@ describe('constant.ts - Type Definitions', () => {
|
||||
|
||||
// ==================== store.ts Tests ====================
|
||||
describe('store.ts - Zustand Store', () => {
|
||||
beforeEach(() => {
|
||||
// Reset store to initial state
|
||||
const { setState } = useStore
|
||||
setState({
|
||||
tagList: [],
|
||||
categoryList: [],
|
||||
showTagManagementModal: false,
|
||||
showCategoryManagementModal: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have empty tagList initially', () => {
|
||||
const { result } = renderHook(() => useStore(state => state.tagList))
|
||||
|
||||
@ -5,11 +5,11 @@ import { useDebounceFn } from 'ahooks'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import Loading from '../../base/loading'
|
||||
import { PluginSource } from '../types'
|
||||
import { usePluginPageContext } from './context'
|
||||
import Empty from './empty'
|
||||
@ -107,12 +107,17 @@ const PluginsPanel = () => {
|
||||
<div className="w-full">
|
||||
<List pluginList={filteredList || []} />
|
||||
</div>
|
||||
{!isLastPage && !isFetching && (
|
||||
<Button onClick={loadNextPage}>
|
||||
{t('common.loadMore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
{!isLastPage && (
|
||||
<div className="flex justify-center py-4">
|
||||
{isFetching
|
||||
? <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>
|
||||
)
|
||||
: (
|
||||
|
||||
@ -134,13 +134,6 @@ describe('BUILTIN_TOOLS_ARRAY', () => {
|
||||
// Store Tests
|
||||
// ================================
|
||||
describe('useReadmePanelStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state before each test
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
})
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have undefined currentPluginDetail initially', () => {
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
@ -228,13 +221,6 @@ describe('useReadmePanelStore', () => {
|
||||
// ReadmeEntrance Component Tests
|
||||
// ================================
|
||||
describe('ReadmeEntrance', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
@ -417,11 +403,6 @@ describe('ReadmeEntrance', () => {
|
||||
// ================================
|
||||
describe('ReadmePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
// Reset mock
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
|
||||
@ -5,6 +5,7 @@ import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -147,7 +148,7 @@ const CodeEditor: FC<Props> = ({
|
||||
onMount={onEditorMounted}
|
||||
placeholder={t('common.jinjaEditorPlaceholder', { ns: 'workflow' })!}
|
||||
/>
|
||||
{isShowVarPicker && (
|
||||
{isShowVarPicker && createPortal(
|
||||
<div
|
||||
ref={popupRef}
|
||||
className="w-[228px] space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg"
|
||||
@ -164,7 +165,8 @@ const CodeEditor: FC<Props> = ({
|
||||
onChange={handleSelectVar}
|
||||
isSupportFileVar={false}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user