diff --git a/web/app/(commonLayout)/_layout-client.tsx b/web/app/(commonLayout)/_layout-client.tsx index 799f8985d3..b19a103d77 100644 --- a/web/app/(commonLayout)/_layout-client.tsx +++ b/web/app/(commonLayout)/_layout-client.tsx @@ -1,6 +1,6 @@ 'use client' -import { FC, useRef } from 'react' -import React, { useEffect, useState } from 'react' +import type { FC } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { usePathname, useRouter, useSelectedLayoutSegments } from 'next/navigation' import useSWR, { SWRConfig } from 'swr' import Header from '../components/header' @@ -50,7 +50,7 @@ const CommonLayout: FC = ({ children }) => { if (!appList || !userProfile || !langeniusVersionInfo) return - const curApp = appList?.data.find(opt => opt.id === appId) + const curAppId = segments[0] === 'app' && segments[2] const currentDatasetId = segments[0] === 'datasets' && segments[2] const currentDataset = datasetList?.data?.find(opt => opt.id === currentDatasetId) @@ -70,12 +70,18 @@ const CommonLayout: FC = ({ children }) => { return (
-
+
{children}
diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index bad3431f3f..44df71205a 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -1,6 +1,9 @@ +'use client' +import { useCallback, useState } from 'react' import type { FC } from 'react' -import { useState } from 'react' +import useSWRInfinite from 'swr/infinite' import { useTranslation } from 'react-i18next' +import { flatten } from 'lodash-es' import { useRouter, useSelectedLayoutSegment } from 'next/navigation' import classNames from 'classnames' import { CircleStackIcon, PuzzlePieceIcon } from '@heroicons/react/24/outline' @@ -9,11 +12,12 @@ import Link from 'next/link' import AccountDropdown from './account-dropdown' import Nav from './nav' import s from './index.module.css' -import type { AppDetailResponse } from '@/models/app' +import type { AppListResponse } from '@/models/app' import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog' import { WorkspaceProvider } from '@/context/workspace-context' import { useDatasetsContext } from '@/context/datasets-context' +import { fetchAppList } from '@/service/apps' const BuildAppsIcon = ({ isSelected }: { isSelected: boolean }) => ( @@ -22,8 +26,7 @@ const BuildAppsIcon = ({ isSelected }: { isSelected: boolean }) => ( ) export type IHeaderProps = { - appItems: AppDetailResponse[] - curApp: AppDetailResponse + curAppId?: string userProfile: UserProfileResponse onLogout: () => void langeniusVersionInfo: LangGeniusVersionResponse @@ -38,15 +41,36 @@ const headerEnvClassName: { [k: string]: string } = { DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]', TESTING: 'bg-[#A5F0FC] border-[#67E3F9] text-[#164C63]', } -const Header: FC = ({ appItems, curApp, userProfile, onLogout, langeniusVersionInfo, isBordered }) => { +const getKey = (pageIndex: number, previousPageData: AppListResponse) => { + if (!pageIndex || previousPageData.has_more) + return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } } + return null +} +const Header: FC = ({ + curAppId, + userProfile, + onLogout, + langeniusVersionInfo, + isBordered, +}) => { const { t } = useTranslation() const [showNewAppDialog, setShowNewAppDialog] = useState(false) + const { data: appsData, isLoading, setSize } = useSWRInfinite(curAppId ? getKey : () => null, fetchAppList, { revalidateFirstPage: false }) const { datasets, currentDataset } = useDatasetsContext() const router = useRouter() const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT' const selectedSegment = useSelectedLayoutSegment() const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon' const isExplore = selectedSegment === 'explore' + const appItems = flatten(appsData?.map(appData => appData.data)) + + const handleLoadmore = useCallback(() => { + if (isLoading) + return + + setSize(size => size + 1) + }, [setSize, isLoading]) + return (
= ({ appItems, curApp, userProfile, onLogout, lan text={t('common.menus.apps')} activeSegment={['apps', 'app']} link='/apps' - curNav={curApp && { id: curApp.id, name: curApp.name, icon: curApp.icon, icon_background: curApp.icon_background }} + curNav={appItems.find(appItem => appItem.id === curAppId)} navs={appItems.map(item => ({ id: item.id, name: item.name, @@ -94,6 +118,7 @@ const Header: FC = ({ appItems, curApp, userProfile, onLogout, lan }))} createText={t('common.menus.newApp')} onCreate={() => setShowNewAppDialog(true)} + onLoadmore={handleLoadmore} /> { const [hovered, setHovered] = useState(false) const segment = useSelectedLayoutSegment() @@ -62,6 +63,7 @@ const Nav = ({ navs={navs} createText={createText} onCreate={onCreate} + onLoadmore={onLoadmore} /> ) diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index ee0b630cd5..27e0931a5d 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -1,8 +1,9 @@ 'use client' -import { Fragment } from 'react' +import { useCallback } from 'react' import { ChevronDownIcon, PlusIcon } from '@heroicons/react/24/solid' -import { Menu, Transition } from '@headlessui/react' +import { Menu } from '@headlessui/react' import { useRouter } from 'next/navigation' +import { debounce } from 'lodash-es' import Indicator from '../../indicator' import AppIcon from '@/app/components/base/app-icon' @@ -13,11 +14,12 @@ type NavItem = { icon: string icon_background: string } -export interface INavSelectorProps { +export type INavSelectorProps = { navs: NavItem[] curNav?: Omit createText: string onCreate: () => void + onLoadmore?: () => void } const itemClassName = ` @@ -25,9 +27,18 @@ const itemClassName = ` rounded-lg font-normal hover:bg-gray-100 cursor-pointer ` -const NavSelector = ({ curNav, navs, createText, onCreate }: INavSelectorProps) => { +const NavSelector = ({ curNav, navs, createText, onCreate, onLoadmore }: INavSelectorProps) => { const router = useRouter() + const handleScroll = useCallback(debounce((e) => { + if (typeof onLoadmore === 'function') { + const { clientHeight, scrollHeight, scrollTop } = e.target + + if (clientHeight + scrollTop > scrollHeight - 50) + onLoadmore() + } + }, 50), []) + return (
@@ -46,59 +57,49 @@ const NavSelector = ({ curNav, navs, createText, onCreate }: INavSelectorProps) />
- - -
- { - navs.map((nav) => ( - -
router.push(nav.link)}> -
- -
- -
+
+ { + navs.map(nav => ( + +
router.push(nav.link)}> +
+ +
+
- {nav.name}
- - )) - } -
- -
-
-
- + {nav.name}
-
{createText}
+ + )) + } +
+ +
+
+
+
+
{createText}
- - - +
+
+
)