mirror of
https://github.com/langgenius/dify.git
synced 2026-03-19 06:17:04 +00:00
Compare commits
6 Commits
3-18-no-gl
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d3f408f9 | ||
|
|
e58d7344f1 | ||
|
|
818026d095 | ||
|
|
728a29b84f | ||
|
|
660f317bc7 | ||
|
|
c463576afd |
@@ -7,17 +7,21 @@
|
||||
*/
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import SideBar from '@/app/components/explore/sidebar'
|
||||
import { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
let mockMediaType: string = MediaType.pc
|
||||
const mockSegments = ['apps']
|
||||
const mockPush = vi.fn()
|
||||
const mockUninstall = vi.fn()
|
||||
const mockUpdatePinStatus = vi.fn()
|
||||
let mockInstalledApps: InstalledApp[] = []
|
||||
let mockIsUninstallPending = false
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useSelectedLayoutSegments: () => mockSegments,
|
||||
@@ -42,12 +46,22 @@ vi.mock('@/service/use-explore', () => ({
|
||||
}),
|
||||
useUninstallApp: () => ({
|
||||
mutateAsync: mockUninstall,
|
||||
isPending: mockIsUninstallPending,
|
||||
}),
|
||||
useUpdateAppPinStatus: () => ({
|
||||
mutateAsync: mockUpdatePinStatus,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
add: mockToastAdd,
|
||||
close: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const createInstalledApp = (overrides: Partial<InstalledApp> = {}): InstalledApp => ({
|
||||
id: overrides.id ?? 'app-1',
|
||||
uninstallable: overrides.uninstallable ?? false,
|
||||
@@ -74,7 +88,7 @@ describe('Sidebar Lifecycle Flow', () => {
|
||||
vi.clearAllMocks()
|
||||
mockMediaType = MediaType.pc
|
||||
mockInstalledApps = []
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
mockIsUninstallPending = false
|
||||
})
|
||||
|
||||
describe('Pin / Unpin / Delete Flow', () => {
|
||||
@@ -91,7 +105,7 @@ describe('Sidebar Lifecycle Flow', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePinStatus).toHaveBeenCalledWith({ appId: 'app-1', isPinned: true })
|
||||
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
}))
|
||||
})
|
||||
@@ -110,7 +124,7 @@ describe('Sidebar Lifecycle Flow', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePinStatus).toHaveBeenCalledWith({ appId: 'app-1', isPinned: false })
|
||||
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
}))
|
||||
})
|
||||
@@ -136,9 +150,9 @@ describe('Sidebar Lifecycle Flow', () => {
|
||||
// Step 4: Uninstall API called and success toast shown
|
||||
await waitFor(() => {
|
||||
expect(mockUninstall).toHaveBeenCalledWith('app-1')
|
||||
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
message: 'common.api.remove',
|
||||
title: 'common.api.remove',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -88,7 +88,6 @@ describe('scroll-area wrapper', () => {
|
||||
'hover:opacity-100',
|
||||
'data-[orientation=vertical]:absolute',
|
||||
'data-[orientation=vertical]:inset-y-0',
|
||||
'data-[orientation=vertical]:right-0',
|
||||
'data-[orientation=vertical]:w-3',
|
||||
'data-[orientation=vertical]:justify-center',
|
||||
)
|
||||
@@ -129,7 +128,6 @@ describe('scroll-area wrapper', () => {
|
||||
'hover:opacity-100',
|
||||
'data-[orientation=horizontal]:absolute',
|
||||
'data-[orientation=horizontal]:inset-x-0',
|
||||
'data-[orientation=horizontal]:bottom-0',
|
||||
'data-[orientation=horizontal]:h-3',
|
||||
'data-[orientation=horizontal]:items-center',
|
||||
)
|
||||
@@ -166,6 +164,24 @@ describe('scroll-area wrapper', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should let callers control scrollbar inset spacing via margin-based className overrides', async () => {
|
||||
renderScrollArea({
|
||||
verticalScrollbarClassName: 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:[margin-inline-end:-0.75rem]',
|
||||
horizontalScrollbarClassName: 'data-[orientation=horizontal]:mx-2 data-[orientation=horizontal]:mb-2',
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('scroll-area-vertical-scrollbar')).toHaveClass(
|
||||
'data-[orientation=vertical]:my-2',
|
||||
'data-[orientation=vertical]:[margin-inline-end:-0.75rem]',
|
||||
)
|
||||
expect(screen.getByTestId('scroll-area-horizontal-scrollbar')).toHaveClass(
|
||||
'data-[orientation=horizontal]:mx-2',
|
||||
'data-[orientation=horizontal]:mb-2',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Corner', () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ const meta = {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compound scroll container built on Base UI ScrollArea. These stories focus on panel-style compositions that already exist throughout Dify: dense sidebars, sticky list headers, multi-pane workbenches, horizontal rails, and overlay surfaces.',
|
||||
component: 'Compound scroll container built on Base UI ScrollArea. These stories focus on panel-style compositions that already exist throughout Dify: dense sidebars, sticky list headers, multi-pane workbenches, horizontal rails, and overlay surfaces. Scrollbar placement should be adjusted by consumer spacing classes such as margin-based overrides instead of right/bottom positioning utilities.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -35,12 +35,12 @@ const titleClassName = 'text-text-primary system-sm-semibold'
|
||||
const bodyClassName = 'text-text-secondary system-sm-regular'
|
||||
const insetScrollAreaClassName = 'h-full p-1'
|
||||
const insetViewportClassName = 'rounded-[20px] bg-components-panel-bg'
|
||||
const insetScrollbarClassName = 'data-[orientation=vertical]:top-1 data-[orientation=vertical]:bottom-1 data-[orientation=vertical]:right-1 data-[orientation=horizontal]:bottom-1 data-[orientation=horizontal]:left-1 data-[orientation=horizontal]:right-1'
|
||||
const insetScrollbarClassName = 'data-[orientation=vertical]:my-1 data-[orientation=vertical]:[margin-inline-end:0.25rem] data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-1'
|
||||
const storyButtonClassName = 'flex w-full items-center justify-between gap-3 rounded-xl border border-divider-subtle bg-components-panel-bg-alt px-3 py-2.5 text-left text-text-secondary transition-colors hover:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-components-input-border-hover motion-reduce:transition-none'
|
||||
const sidebarScrollAreaClassName = 'h-full pr-2'
|
||||
const sidebarViewportClassName = 'overscroll-contain pr-2'
|
||||
const sidebarContentClassName = 'space-y-0.5 pr-2'
|
||||
const sidebarScrollbarClassName = 'data-[orientation=vertical]:right-0.5'
|
||||
const sidebarScrollAreaClassName = 'h-full'
|
||||
const sidebarViewportClassName = 'overscroll-contain'
|
||||
const sidebarContentClassName = 'space-y-0.5'
|
||||
const sidebarScrollbarClassName = 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:[margin-inline-end:-0.75rem]'
|
||||
const appNavButtonClassName = 'group flex h-8 w-full items-center justify-between gap-3 rounded-lg px-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-components-input-border-hover motion-reduce:transition-none'
|
||||
const appNavMetaClassName = 'shrink-0 rounded-md border border-divider-subtle bg-components-panel-bg-alt px-1.5 py-0.5 text-text-quaternary system-2xs-medium-uppercase tracking-[0.08em]'
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ export const scrollAreaScrollbarClassName = cn(
|
||||
'pointer-events-none data-[hovering]:pointer-events-auto data-[hovering]:opacity-100',
|
||||
'data-[scrolling]:pointer-events-auto data-[scrolling]:opacity-100',
|
||||
'hover:pointer-events-auto hover:opacity-100',
|
||||
'data-[orientation=vertical]:absolute data-[orientation=vertical]:inset-y-0 data-[orientation=vertical]:right-0 data-[orientation=vertical]:w-3 data-[orientation=vertical]:justify-center',
|
||||
'data-[orientation=horizontal]:absolute data-[orientation=horizontal]:inset-x-0 data-[orientation=horizontal]:bottom-0 data-[orientation=horizontal]:h-3 data-[orientation=horizontal]:items-center',
|
||||
'data-[orientation=vertical]:absolute data-[orientation=vertical]:inset-y-0 data-[orientation=vertical]:w-3 data-[orientation=vertical]:justify-center',
|
||||
'data-[orientation=horizontal]:absolute data-[orientation=horizontal]:inset-x-0 data-[orientation=horizontal]:h-3 data-[orientation=horizontal]:items-center',
|
||||
)
|
||||
|
||||
export const scrollAreaThumbClassName = cn(
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import SideBar from '../index'
|
||||
|
||||
const { mockToastAdd } = vi.hoisted(() => ({
|
||||
mockToastAdd: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockSegments = ['apps']
|
||||
const mockPush = vi.fn()
|
||||
const mockUninstall = vi.fn()
|
||||
const mockUpdatePinStatus = vi.fn()
|
||||
let mockIsPending = false
|
||||
let mockIsUninstallPending = false
|
||||
let mockInstalledApps: InstalledApp[] = []
|
||||
let mockMediaType: string = MediaType.pc
|
||||
|
||||
@@ -36,12 +40,22 @@ vi.mock('@/service/use-explore', () => ({
|
||||
}),
|
||||
useUninstallApp: () => ({
|
||||
mutateAsync: mockUninstall,
|
||||
isPending: mockIsUninstallPending,
|
||||
}),
|
||||
useUpdateAppPinStatus: () => ({
|
||||
mutateAsync: mockUpdatePinStatus,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
add: mockToastAdd,
|
||||
close: vi.fn(),
|
||||
update: vi.fn(),
|
||||
promise: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const createInstalledApp = (overrides: Partial<InstalledApp> = {}): InstalledApp => ({
|
||||
id: overrides.id ?? 'app-123',
|
||||
uninstallable: overrides.uninstallable ?? false,
|
||||
@@ -67,9 +81,9 @@ describe('SideBar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsPending = false
|
||||
mockIsUninstallPending = false
|
||||
mockInstalledApps = []
|
||||
mockMediaType = MediaType.pc
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@@ -84,6 +98,7 @@ describe('SideBar', () => {
|
||||
renderSideBar()
|
||||
|
||||
expect(screen.getByText('explore.sidebar.webApps')).toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'explore.sidebar.webApps' })).toBeInTheDocument()
|
||||
expect(screen.getByText('My App')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -135,9 +150,9 @@ describe('SideBar', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUninstall).toHaveBeenCalledWith('app-123')
|
||||
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
message: 'common.api.remove',
|
||||
title: 'common.api.remove',
|
||||
}))
|
||||
})
|
||||
})
|
||||
@@ -152,9 +167,9 @@ describe('SideBar', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePinStatus).toHaveBeenCalledWith({ appId: 'app-123', isPinned: true })
|
||||
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
message: 'common.api.success',
|
||||
title: 'common.api.success',
|
||||
}))
|
||||
})
|
||||
})
|
||||
@@ -187,6 +202,18 @@ describe('SideBar', () => {
|
||||
expect(mockUninstall).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable dialog actions while uninstall is pending', async () => {
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
mockIsUninstallPending = true
|
||||
renderSideBar()
|
||||
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
fireEvent.click(await screen.findByText('explore.sidebar.action.delete'))
|
||||
|
||||
expect(screen.getByText('common.operation.cancel')).toBeDisabled()
|
||||
expect(screen.getByText('common.operation.confirm')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
|
||||
@@ -3,17 +3,40 @@ import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@/app/components/base/ui/alert-dialog'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@/app/components/base/ui/scroll-area'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import Link from '@/next/link'
|
||||
import { useSelectedLayoutSegments } from '@/next/navigation'
|
||||
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Toast from '../../base/toast'
|
||||
import Item from './app-nav-item'
|
||||
import NoApps from './no-apps'
|
||||
|
||||
const expandedSidebarScrollAreaClassNames = {
|
||||
root: 'h-full',
|
||||
viewport: 'overscroll-contain',
|
||||
content: 'space-y-0.5',
|
||||
scrollbar: 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:[margin-inline-end:-0.75rem]',
|
||||
thumb: 'rounded-full',
|
||||
} as const
|
||||
|
||||
const SideBar = () => {
|
||||
const { t } = useTranslation()
|
||||
const segments = useSelectedLayoutSegments()
|
||||
@@ -21,7 +44,7 @@ const SideBar = () => {
|
||||
const isDiscoverySelected = lastSegment === 'apps'
|
||||
const { data, isPending } = useGetInstalledApps()
|
||||
const installedApps = data?.installed_apps ?? []
|
||||
const { mutateAsync: uninstallApp } = useUninstallApp()
|
||||
const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp()
|
||||
const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus()
|
||||
|
||||
const media = useBreakpoints()
|
||||
@@ -36,23 +59,48 @@ const SideBar = () => {
|
||||
const id = currId
|
||||
await uninstallApp(id)
|
||||
setShowConfirm(false)
|
||||
Toast.notify({
|
||||
toast.add({
|
||||
type: 'success',
|
||||
message: t('api.remove', { ns: 'common' }),
|
||||
title: t('api.remove', { ns: 'common' }),
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdatePinStatus = async (id: string, isPinned: boolean) => {
|
||||
await updatePinStatus({ appId: id, isPinned })
|
||||
Toast.notify({
|
||||
toast.add({
|
||||
type: 'success',
|
||||
message: t('api.success', { ns: 'common' }),
|
||||
title: t('api.success', { ns: 'common' }),
|
||||
})
|
||||
}
|
||||
|
||||
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
||||
const shouldUseExpandedScrollArea = !isMobile && !isFold
|
||||
const webAppsLabelId = React.useId()
|
||||
const installedAppItems = 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>
|
||||
))
|
||||
|
||||
return (
|
||||
<div className={cn('relative w-fit shrink-0 cursor-pointer px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
|
||||
<div className={cn('flex h-full w-fit shrink-0 cursor-pointer flex-col px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
|
||||
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
|
||||
<Link
|
||||
href="/explore/apps"
|
||||
@@ -73,59 +121,69 @@ const SideBar = () => {
|
||||
)}
|
||||
|
||||
{installedApps.length > 0 && (
|
||||
<div className="mt-5">
|
||||
{!isMobile && !isFold && <p className="mb-1.5 break-all pl-2 uppercase text-text-tertiary system-xs-medium-uppercase mobile:px-0">{t('sidebar.webApps', { ns: 'explore' })}</p>}
|
||||
<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
|
||||
? <span className="i-ri-expand-right-line" />
|
||||
<div className="mt-5 flex min-h-0 flex-1 flex-col">
|
||||
{!isMobile && !isFold && <p id={webAppsLabelId} className="mb-1.5 break-all pl-2 uppercase text-text-tertiary system-xs-medium-uppercase mobile:px-0">{t('sidebar.webApps', { ns: 'explore' })}</p>}
|
||||
{shouldUseExpandedScrollArea
|
||||
? (
|
||||
<div className="min-h-0 flex-1">
|
||||
<ScrollArea className={expandedSidebarScrollAreaClassNames.root}>
|
||||
<ScrollAreaViewport
|
||||
aria-labelledby={webAppsLabelId}
|
||||
className={expandedSidebarScrollAreaClassNames.viewport}
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent className={expandedSidebarScrollAreaClassNames.content}>
|
||||
{installedAppItems}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className={expandedSidebarScrollAreaClassNames.scrollbar}>
|
||||
<ScrollAreaThumb className={expandedSidebarScrollAreaClassNames.thumb} />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<span className="i-ri-layout-left-2-line" />
|
||||
<div
|
||||
className="h-full min-h-0 flex-1 space-y-0.5 overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
{installedAppItems}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showConfirm && (
|
||||
<Confirm
|
||||
title={t('sidebar.delete.title', { ns: 'explore' })}
|
||||
content={t('sidebar.delete.content', { ns: 'explore' })}
|
||||
isShow={showConfirm}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
/>
|
||||
{!isMobile && (
|
||||
<div className="mt-auto flex pb-3 pt-3">
|
||||
<div className="flex size-8 cursor-pointer items-center justify-center text-text-tertiary" onClick={toggleIsFold}>
|
||||
{isFold
|
||||
? <span className="i-ri-expand-right-line" />
|
||||
: (
|
||||
<span className="i-ri-layout-left-2-line" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AlertDialog open={showConfirm} onOpenChange={setShowConfirm}>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col items-start gap-2 self-stretch pb-4 pl-6 pr-6 pt-6">
|
||||
<AlertDialogTitle className="w-full text-text-primary title-2xl-semi-bold">
|
||||
{t('sidebar.delete.title', { ns: 'explore' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="w-full whitespace-pre-wrap break-words text-text-tertiary system-md-regular">
|
||||
{t('sidebar.delete.content', { ns: 'explore' })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton disabled={isUninstalling}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton loading={isUninstalling} disabled={isUninstalling} onClick={handleDelete}>
|
||||
{t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4336,11 +4336,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/explore/sidebar/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/explore/sidebar/no-apps/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
|
||||
Reference in New Issue
Block a user