diff --git a/web/app/(commonLayout)/plugins/page.tsx b/web/app/(commonLayout)/plugins/page.tsx index 6220c20aa0..f28944ebbd 100644 --- a/web/app/(commonLayout)/plugins/page.tsx +++ b/web/app/(commonLayout)/plugins/page.tsx @@ -1,5 +1,5 @@ import PluginPage from '@/app/components/plugins/plugin-page' -import PluginsPanel from '@/app/components/plugins/plugins-panel' +import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel' import Marketplace from '@/app/components/plugins/marketplace' const PluginList = async () => { diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx deleted file mode 100644 index 86eab52006..0000000000 --- a/web/app/components/plugins/marketplace/context.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client' - -import type { ReactNode } from 'react' -import { useState } from 'react' -import { - createContext, - useContextSelector, -} from 'use-context-selector' - -export type MarketplaceContextValue = { - scrollIntersected: boolean - setScrollIntersected: (scrollIntersected: boolean) => void -} - -export const MarketplaceContext = createContext({ - scrollIntersected: false, - setScrollIntersected: () => {}, -}) - -type MarketplaceContextProviderProps = { - children: ReactNode -} - -export function useMarketplaceContext(selector: (value: MarketplaceContextValue) => any) { - return useContextSelector(MarketplaceContext, selector) -} - -export const MarketplaceContextProvider = ({ - children, -}: MarketplaceContextProviderProps) => { - const [scrollIntersected, setScrollIntersected] = useState(false) - - return ( - - {children} - - ) -} diff --git a/web/app/components/plugins/marketplace/header.tsx b/web/app/components/plugins/marketplace/description/index.tsx similarity index 72% rename from web/app/components/plugins/marketplace/header.tsx rename to web/app/components/plugins/marketplace/description/index.tsx index e8f0920287..754a4b12a9 100644 --- a/web/app/components/plugins/marketplace/header.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -1,14 +1,10 @@ -import SearchBox from './search-box' -import PluginTypeSwitch from './plugin-type-switch' -import IntersectionLine from './intersection-line' - -const Header = () => { +const Description = () => { return ( <>

Empower your AI development

-

+

Discover models @@ -27,13 +23,8 @@ const Header = () => { in Dify Marketplace

- -
- -
- ) } -export default Header +export default Description diff --git a/web/app/components/plugins/marketplace/description/wrapper.tsx b/web/app/components/plugins/marketplace/description/wrapper.tsx new file mode 100644 index 0000000000..91dd3a2ba6 --- /dev/null +++ b/web/app/components/plugins/marketplace/description/wrapper.tsx @@ -0,0 +1,39 @@ +'use client' + +import type { ReactNode } from 'react' +import { useCallback } from 'react' +import IntersectionLine from '../intersection-line' +import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' + +type DescriptionWrapperProps = { + children: ReactNode +} +const DescriptionWrapper = ({ + children, +}: DescriptionWrapperProps) => { + const containerRef = usePluginPageContext(v => v.containerRef) + const scrollDisabled = usePluginPageContext(v => v.scrollDisabled) + const setScrollDisabled = usePluginPageContext(v => v.setScrollDisabled) + + const handleScrollIntersectionChange = useCallback((isIntersecting: boolean) => { + if (!isIntersecting && !scrollDisabled) { + setScrollDisabled(true) + setTimeout(() => { + if (containerRef && containerRef.current) + containerRef.current.scrollTop = 0 + }, 100) + } + }, [containerRef, scrollDisabled, setScrollDisabled]) + + return !scrollDisabled && ( + <> + {children} + + + ) +} + +export default DescriptionWrapper diff --git a/web/app/components/plugins/marketplace/header/index.tsx b/web/app/components/plugins/marketplace/header/index.tsx new file mode 100644 index 0000000000..3caeea6c7f --- /dev/null +++ b/web/app/components/plugins/marketplace/header/index.tsx @@ -0,0 +1,20 @@ +import Description from '../description' +import DescriptionWrapper from '../description/wrapper' +import SearchBoxWrapper from '../search-box/wrapper' +import PluginTypeSwitch from '../plugin-type-switch' + +const Header = () => { + return ( + <> + + + +
+ +
+ + + ) +} + +export default Header diff --git a/web/app/components/plugins/marketplace/header-wrapper.tsx b/web/app/components/plugins/marketplace/header/wrapper.tsx similarity index 59% rename from web/app/components/plugins/marketplace/header-wrapper.tsx rename to web/app/components/plugins/marketplace/header/wrapper.tsx index 6e18309a5f..a0c45bbc1a 100644 --- a/web/app/components/plugins/marketplace/header-wrapper.tsx +++ b/web/app/components/plugins/marketplace/header/wrapper.tsx @@ -1,6 +1,7 @@ 'use client' import type { ReactNode } from 'react' +import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import cn from '@/utils/classnames' type HeaderWrapperProps = { @@ -9,10 +10,13 @@ type HeaderWrapperProps = { const HeaderWrapper = ({ children, }: HeaderWrapperProps) => { + const scrollDisabled = usePluginPageContext(v => v.scrollDisabled) + return (
{children} diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index cbf1c870ce..5d737db448 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -1,20 +1,17 @@ -import { MarketplaceContextProvider } from './context' -import HeaderWrapper from './header-wrapper' import Header from './header' -import ListWrapper from './list-wrapper' +import HeaderWrapper from './header/wrapper' import List from './list' +import ListWrapper from './list/wrapper' const Marketplace = () => { return ( -
- - -
- - - - - +
+ +
+ + + +
) } diff --git a/web/app/components/plugins/marketplace/intersection-line/hooks.ts b/web/app/components/plugins/marketplace/intersection-line/hooks.ts index ed79daaa5a..a31d196179 100644 --- a/web/app/components/plugins/marketplace/intersection-line/hooks.ts +++ b/web/app/components/plugins/marketplace/intersection-line/hooks.ts @@ -1,30 +1,21 @@ import { useEffect } from 'react' -import { useContextSelector } from 'use-context-selector' -import { PluginPageContext } from '../../plugin-page/context' -import { MarketplaceContext } from '../context' export const useScrollIntersection = ( + containerRef: React.RefObject, anchorRef: React.RefObject, + callback: (isIntersecting: boolean) => void, ) => { - const containerRef = useContextSelector(PluginPageContext, v => v.containerRef) - const scrollIntersected = useContextSelector(MarketplaceContext, v => v.scrollIntersected) - const setScrollIntersected = useContextSelector(MarketplaceContext, v => v.setScrollIntersected) - useEffect(() => { let observer: IntersectionObserver | undefined - if (containerRef.current && anchorRef.current) { + if (containerRef?.current && anchorRef.current) { observer = new IntersectionObserver((entries) => { - console.log(entries, 'entries') - if (entries[0].isIntersecting && !scrollIntersected) - setScrollIntersected(true) - - if (!entries[0].isIntersecting && scrollIntersected) - setScrollIntersected(false) + const isIntersecting = entries[0].isIntersecting + callback(isIntersecting) }, { root: containerRef.current, }) observer.observe(anchorRef.current) } return () => observer?.disconnect() - }, [containerRef, anchorRef, scrollIntersected, setScrollIntersected]) + }, [containerRef, anchorRef, callback]) } diff --git a/web/app/components/plugins/marketplace/intersection-line/index.tsx b/web/app/components/plugins/marketplace/intersection-line/index.tsx index 647247c995..3805ec6f13 100644 --- a/web/app/components/plugins/marketplace/intersection-line/index.tsx +++ b/web/app/components/plugins/marketplace/intersection-line/index.tsx @@ -3,10 +3,21 @@ import { useRef } from 'react' import { useScrollIntersection } from './hooks' -const IntersectionLine = () => { +type IntersectionLineProps = { + containerRef: React.RefObject + intersectedCallback: (isIntersecting: boolean) => void +} +const IntersectionLine = ({ + containerRef, + intersectedCallback, +}: IntersectionLineProps) => { const ref = useRef(null) - useScrollIntersection(ref) + useScrollIntersection( + containerRef, + ref, + intersectedCallback, + ) return (
diff --git a/web/app/components/plugins/marketplace/list-wrapper.tsx b/web/app/components/plugins/marketplace/list-wrapper.tsx deleted file mode 100644 index 6dd58bdcf5..0000000000 --- a/web/app/components/plugins/marketplace/list-wrapper.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client' - -import type { ReactNode } from 'react' -import cn from '@/utils/classnames' - -type ListWrapperProps = { - children: ReactNode -} -const ListWrapper = ({ - children, -}: ListWrapperProps) => { - return ( -
- {children} -
- ) -} - -export default ListWrapper diff --git a/web/app/components/plugins/marketplace/list.tsx b/web/app/components/plugins/marketplace/list/index.tsx similarity index 100% rename from web/app/components/plugins/marketplace/list.tsx rename to web/app/components/plugins/marketplace/list/index.tsx diff --git a/web/app/components/plugins/marketplace/list/wrapper.tsx b/web/app/components/plugins/marketplace/list/wrapper.tsx new file mode 100644 index 0000000000..cf040c8d6b --- /dev/null +++ b/web/app/components/plugins/marketplace/list/wrapper.tsx @@ -0,0 +1,39 @@ +'use client' + +import type { ReactNode } from 'react' +import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' +import cn from '@/utils/classnames' + +type ListWrapperProps = { + children: ReactNode +} +const ListWrapper = ({ + children, +}: ListWrapperProps) => { + const scrollDisabled = usePluginPageContext(v => v.scrollDisabled) + const setScrollDisabled = usePluginPageContext(v => v.setScrollDisabled) + + return ( + <> + { + scrollDisabled && ( +
+ ) + } +
{ + if ((e.target as HTMLElement).scrollTop <= 0) + setScrollDisabled(false) + }} + > + {children} +
+ + ) +} + +export default ListWrapper diff --git a/web/app/components/plugins/marketplace/search-box/index.tsx b/web/app/components/plugins/marketplace/search-box/index.tsx index 923b16096c..612130bc96 100644 --- a/web/app/components/plugins/marketplace/search-box/index.tsx +++ b/web/app/components/plugins/marketplace/search-box/index.tsx @@ -5,20 +5,20 @@ import { useState, } from 'react' import { RiCloseLine } from '@remixicon/react' -import { useMarketplaceContext } from '../context' import TagsFilter from './tags-filter' import ActionButton from '@/app/components/base/action-button' import cn from '@/utils/classnames' type SearchBoxProps = { onChange?: (searchText: string, tags: string[]) => void + widthShouldChange?: boolean } const SearchBox = ({ onChange, + widthShouldChange, }: SearchBoxProps) => { const [searchText, setSearchText] = useState('') const [selectedTags, setSelectedTags] = useState([]) - const scrollIntersected = useMarketplaceContext(v => v.scrollIntersected) const handleTagsChange = useCallback((tags: string[]) => { setSelectedTags(tags) @@ -29,7 +29,7 @@ const SearchBox = ({
{ + const scrollDisabled = usePluginPageContext(v => v.scrollDisabled) + + return ( + + ) +} + +export default Wrapper diff --git a/web/app/components/plugins/plugin-page/context.tsx b/web/app/components/plugins/plugin-page/context.tsx index 94351484b1..57e00d9c5d 100644 --- a/web/app/components/plugins/plugin-page/context.tsx +++ b/web/app/components/plugins/plugin-page/context.tsx @@ -1,29 +1,49 @@ 'use client' import type { ReactNode } from 'react' -import { useRef } from 'react' -import { createContext } from 'use-context-selector' +import { + useRef, + useState, +} from 'react' +import { + createContext, + useContextSelector, +} from 'use-context-selector' export type PluginPageContextValue = { containerRef: React.RefObject + scrollDisabled: boolean + setScrollDisabled: (scrollDisabled: boolean) => void } export const PluginPageContext = createContext({ containerRef: { current: null }, + scrollDisabled: false, + setScrollDisabled: () => {}, }) type PluginPageContextProviderProps = { children: ReactNode } +export function usePluginPageContext(selector: (value: PluginPageContextValue) => any) { + return useContextSelector(PluginPageContext, selector) +} + export const PluginPageContextProvider = ({ children, }: PluginPageContextProviderProps) => { const containerRef = useRef(null) + const [scrollDisabled, setScrollDisabled] = useState(false) + return ( - + {children} ) diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index fa62a592c7..f79e0af275 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -2,7 +2,6 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useContextSelector } from 'use-context-selector' import { RiArrowRightUpLine, RiBugLine, @@ -10,8 +9,8 @@ import { RiEqualizer2Line, } from '@remixicon/react' import { - PluginPageContext, PluginPageContextProvider, + usePluginPageContext, } from './context' import InstallPluginDropdown from './install-plugin-dropdown' import { useTabSearchParams } from '@/hooks/use-tab-searchparams' @@ -32,7 +31,8 @@ const PluginPage = ({ }: PluginPageProps) => { const { t } = useTranslation() const { setShowPluginSettingModal } = useModalContext() as any - const containerRef = useContextSelector(PluginPageContext, v => v.containerRef) + const containerRef = usePluginPageContext(v => v.containerRef) + const scrollDisabled = usePluginPageContext(v => v.scrollDisabled) const options = useMemo(() => { return [ @@ -51,9 +51,14 @@ const PluginPage = ({ className={cn('grow relative flex flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins' ? 'rounded-t-xl bg-components-panel-bg' : 'bg-background-body', + activeTab === 'discover' && scrollDisabled && 'overflow-hidden', )} > -
+