mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
148 lines
5.7 KiB
TypeScript
148 lines
5.7 KiB
TypeScript
'use client'
|
|
import type { FC } from 'react'
|
|
import React, { useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useContext } from 'use-context-selector'
|
|
import { useSelectedLayoutSegments } from 'next/navigation'
|
|
import Link from 'next/link'
|
|
import Toast from '../../base/toast'
|
|
import Item from './app-nav-item'
|
|
import cn from '@/utils/classnames'
|
|
import ExploreContext from '@/context/explore-context'
|
|
import Confirm from '@/app/components/base/confirm'
|
|
import Divider from '@/app/components/base/divider'
|
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
|
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
|
|
import { RiAppsFill, RiExpandRightLine, RiLayoutLeft2Line } from '@remixicon/react'
|
|
import { useBoolean } from 'ahooks'
|
|
import NoApps from './no-apps'
|
|
|
|
export type IExploreSideBarProps = {
|
|
controlUpdateInstalledApps: number
|
|
}
|
|
|
|
const SideBar: FC<IExploreSideBarProps> = ({
|
|
controlUpdateInstalledApps,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
const segments = useSelectedLayoutSegments()
|
|
const lastSegment = segments.slice(-1)[0]
|
|
const isDiscoverySelected = lastSegment === 'apps'
|
|
const { installedApps, setInstalledApps, setIsFetchingInstalledApps } = useContext(ExploreContext)
|
|
const { isFetching: isFetchingInstalledApps, data: ret, refetch: fetchInstalledAppList } = useGetInstalledApps()
|
|
const { mutateAsync: uninstallApp } = useUninstallApp()
|
|
const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus()
|
|
|
|
const media = useBreakpoints()
|
|
const isMobile = media === MediaType.mobile
|
|
const [isFold, {
|
|
toggle: toggleIsFold,
|
|
}] = useBoolean(false)
|
|
|
|
const [showConfirm, setShowConfirm] = useState(false)
|
|
const [currId, setCurrId] = useState('')
|
|
const handleDelete = async () => {
|
|
const id = currId
|
|
await uninstallApp(id)
|
|
setShowConfirm(false)
|
|
Toast.notify({
|
|
type: 'success',
|
|
message: t('common.api.remove'),
|
|
})
|
|
}
|
|
|
|
const handleUpdatePinStatus = async (id: string, isPinned: boolean) => {
|
|
await updatePinStatus({ appId: id, isPinned })
|
|
Toast.notify({
|
|
type: 'success',
|
|
message: t('common.api.success'),
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
const installed_apps = (ret as any)?.installed_apps
|
|
if (installed_apps && installed_apps.length > 0)
|
|
setInstalledApps(installed_apps)
|
|
else
|
|
setInstalledApps([])
|
|
}, [ret, setInstalledApps])
|
|
|
|
useEffect(() => {
|
|
setIsFetchingInstalledApps(isFetchingInstalledApps)
|
|
}, [isFetchingInstalledApps, setIsFetchingInstalledApps])
|
|
|
|
useEffect(() => {
|
|
fetchInstalledAppList()
|
|
}, [controlUpdateInstalledApps, fetchInstalledAppList])
|
|
|
|
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
|
return (
|
|
<div className={cn('relative w-fit shrink-0 cursor-pointer px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
|
|
<Link
|
|
href='/explore/apps'
|
|
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover',
|
|
'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')}
|
|
>
|
|
<div className='flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid'>
|
|
<RiAppsFill className='size-3.5 text-components-avatar-shape-fill-stop-100' />
|
|
</div>
|
|
{!isMobile && !isFold && <div className={cn('truncate', isDiscoverySelected ? 'system-sm-semibold text-components-menu-item-text-active' : 'system-sm-regular text-components-menu-item-text')}>{t('explore.sidebar.title')}</div>}
|
|
</Link>
|
|
{installedApps.length > 0 && (
|
|
<div className='mt-5'>
|
|
{!isMobile && !isFold && <p className='system-xs-medium-uppercase mb-1.5 break-all pl-2 uppercase text-text-tertiary mobile:px-0'>{t('explore.sidebar.webApps')}</p>}
|
|
{installedApps.length === 0 && !isMobile && !isFold && <NoApps />}
|
|
<div className='space-y-0.5 overflow-y-auto overflow-x-hidden'
|
|
style={{
|
|
height: 'calc(100vh - 250px)',
|
|
}}
|
|
>
|
|
{installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => (
|
|
<React.Fragment key={id}>
|
|
<Item
|
|
isMobile={isMobile || isFold}
|
|
name={name}
|
|
icon_type={icon_type}
|
|
icon={icon}
|
|
icon_background={icon_background}
|
|
icon_url={icon_url}
|
|
id={id}
|
|
isSelected={lastSegment?.toLowerCase() === id}
|
|
isPinned={is_pinned}
|
|
togglePin={() => handleUpdatePinStatus(id, !is_pinned)}
|
|
uninstallable={uninstallable}
|
|
onDelete={(id) => {
|
|
setCurrId(id)
|
|
setShowConfirm(true)
|
|
}}
|
|
/>
|
|
{index === pinnedAppsCount - 1 && index !== installedApps.length - 1 && <Divider />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!isMobile && (
|
|
<div className='absolute bottom-3 left-3 flex size-8 cursor-pointer items-center justify-center text-text-tertiary' onClick={toggleIsFold}>
|
|
{isFold ? <RiExpandRightLine className='size-4.5' /> : (
|
|
<RiLayoutLeft2Line className='size-4.5' />
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{showConfirm && (
|
|
<Confirm
|
|
title={t('explore.sidebar.delete.title')}
|
|
content={t('explore.sidebar.delete.content')}
|
|
isShow={showConfirm}
|
|
onConfirm={handleDelete}
|
|
onCancel={() => setShowConfirm(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(SideBar)
|