mirror of
https://github.com/langgenius/dify.git
synced 2026-03-27 17:19:55 +08:00
329 lines
10 KiB
TypeScript
329 lines
10 KiB
TypeScript
import type { FC } from 'react'
|
|
import {
|
|
RiCheckLine,
|
|
RiFullscreenLine,
|
|
RiZoomInLine,
|
|
RiZoomOutLine,
|
|
} from '@remixicon/react'
|
|
import {
|
|
Fragment,
|
|
memo,
|
|
useCallback,
|
|
useState,
|
|
} from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
useReactFlow,
|
|
useViewport,
|
|
} from 'reactflow'
|
|
import {
|
|
PortalToFollowElem,
|
|
PortalToFollowElemContent,
|
|
PortalToFollowElemTrigger,
|
|
} from '@/app/components/base/portal-to-follow-elem'
|
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
|
|
import { cn } from '@/utils/classnames'
|
|
import Divider from '../../base/divider'
|
|
import {
|
|
useNodesSyncDraft,
|
|
useWorkflowReadOnly,
|
|
} from '../hooks'
|
|
import ShortcutsName from '../shortcuts-name'
|
|
import TipPopup from './tip-popup'
|
|
|
|
enum ZoomType {
|
|
zoomIn = 'zoomIn',
|
|
zoomOut = 'zoomOut',
|
|
zoomToFit = 'zoomToFit',
|
|
zoomTo25 = 'zoomTo25',
|
|
zoomTo50 = 'zoomTo50',
|
|
zoomTo75 = 'zoomTo75',
|
|
zoomTo100 = 'zoomTo100',
|
|
zoomTo200 = 'zoomTo200',
|
|
toggleUserComments = 'toggleUserComments',
|
|
toggleUserCursors = 'toggleUserCursors',
|
|
toggleMiniMap = 'toggleMiniMap',
|
|
}
|
|
|
|
type ZoomInOutProps = {
|
|
showMiniMap?: boolean
|
|
onToggleMiniMap?: () => void
|
|
showUserCursors?: boolean
|
|
onToggleUserCursors?: () => void
|
|
showUserComments?: boolean
|
|
onToggleUserComments?: () => void
|
|
isCommentMode?: boolean
|
|
}
|
|
|
|
const ZoomInOut: FC<ZoomInOutProps> = ({
|
|
showMiniMap = true,
|
|
onToggleMiniMap,
|
|
showUserCursors = true,
|
|
onToggleUserCursors,
|
|
showUserComments = true,
|
|
onToggleUserComments,
|
|
isCommentMode = false,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
const {
|
|
zoomIn,
|
|
zoomOut,
|
|
zoomTo,
|
|
fitView,
|
|
} = useReactFlow()
|
|
const { zoom } = useViewport()
|
|
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
|
const [open, setOpen] = useState(false)
|
|
const {
|
|
workflowReadOnly,
|
|
getWorkflowReadOnly,
|
|
} = useWorkflowReadOnly()
|
|
const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
|
|
|
|
const ZOOM_IN_OUT_OPTIONS = [
|
|
[
|
|
{
|
|
key: ZoomType.zoomTo200,
|
|
text: '200%',
|
|
},
|
|
{
|
|
key: ZoomType.zoomTo100,
|
|
text: '100%',
|
|
},
|
|
{
|
|
key: ZoomType.zoomTo75,
|
|
text: '75%',
|
|
},
|
|
{
|
|
key: ZoomType.zoomTo50,
|
|
text: '50%',
|
|
},
|
|
{
|
|
key: ZoomType.zoomTo25,
|
|
text: '25%',
|
|
},
|
|
{
|
|
key: ZoomType.zoomToFit,
|
|
text: t('operator.zoomToFit', { ns: 'workflow' }),
|
|
},
|
|
],
|
|
isCollaborationEnabled
|
|
? [
|
|
{
|
|
key: ZoomType.toggleUserComments,
|
|
text: t('operator.showUserComments', { ns: 'workflow' }),
|
|
},
|
|
{
|
|
key: ZoomType.toggleUserCursors,
|
|
text: t('operator.showUserCursors', { ns: 'workflow' }),
|
|
},
|
|
{
|
|
key: ZoomType.toggleMiniMap,
|
|
text: t('operator.showMiniMap', { ns: 'workflow' }),
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
key: ZoomType.toggleMiniMap,
|
|
text: t('operator.showMiniMap', { ns: 'workflow' }),
|
|
},
|
|
],
|
|
]
|
|
|
|
const handleZoom = (type: string) => {
|
|
if (workflowReadOnly)
|
|
return
|
|
|
|
if (type === ZoomType.zoomToFit)
|
|
fitView()
|
|
|
|
if (type === ZoomType.zoomTo25)
|
|
zoomTo(0.25)
|
|
|
|
if (type === ZoomType.zoomTo50)
|
|
zoomTo(0.5)
|
|
|
|
if (type === ZoomType.zoomTo75)
|
|
zoomTo(0.75)
|
|
|
|
if (type === ZoomType.zoomTo100)
|
|
zoomTo(1)
|
|
|
|
if (type === ZoomType.zoomTo200)
|
|
zoomTo(2)
|
|
|
|
if (type === ZoomType.toggleUserComments) {
|
|
if (!isCommentMode)
|
|
onToggleUserComments?.()
|
|
|
|
return
|
|
}
|
|
|
|
if (type === ZoomType.toggleUserCursors) {
|
|
onToggleUserCursors?.()
|
|
return
|
|
}
|
|
|
|
if (type === ZoomType.toggleMiniMap) {
|
|
onToggleMiniMap?.()
|
|
return
|
|
}
|
|
|
|
handleSyncWorkflowDraft()
|
|
}
|
|
|
|
const handleTrigger = useCallback(() => {
|
|
if (getWorkflowReadOnly())
|
|
return
|
|
|
|
setOpen(v => !v)
|
|
}, [getWorkflowReadOnly])
|
|
|
|
return (
|
|
<PortalToFollowElem
|
|
placement="top-start"
|
|
open={open}
|
|
onOpenChange={setOpen}
|
|
offset={{
|
|
mainAxis: 4,
|
|
crossAxis: -2,
|
|
}}
|
|
>
|
|
<PortalToFollowElemTrigger asChild>
|
|
<div className={`
|
|
h-9 cursor-pointer rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg
|
|
p-0.5 text-[13px] shadow-lg backdrop-blur-[5px]
|
|
hover:bg-state-base-hover
|
|
${workflowReadOnly && '!cursor-not-allowed opacity-50'}
|
|
`}
|
|
>
|
|
<div className={cn(
|
|
'flex h-8 w-[98px] items-center justify-between rounded-lg',
|
|
)}
|
|
>
|
|
<TipPopup
|
|
title={t('operator.zoomOut', { ns: 'workflow' })}
|
|
shortcuts={['ctrl', '-']}
|
|
>
|
|
<div
|
|
className={`flex h-8 w-8 items-center justify-center rounded-lg ${zoom <= 0.25 ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-black/5'}`}
|
|
onClick={(e) => {
|
|
if (zoom <= 0.25)
|
|
return
|
|
|
|
e.stopPropagation()
|
|
zoomOut()
|
|
}}
|
|
>
|
|
<RiZoomOutLine className="h-4 w-4 text-text-tertiary hover:text-text-secondary" />
|
|
</div>
|
|
</TipPopup>
|
|
<div onClick={handleTrigger} className={cn('w-[34px] text-text-tertiary system-sm-medium hover:text-text-secondary')}>
|
|
{Number.parseFloat(`${zoom * 100}`).toFixed(0)}
|
|
%
|
|
</div>
|
|
<TipPopup
|
|
title={t('operator.zoomIn', { ns: 'workflow' })}
|
|
shortcuts={['ctrl', '+']}
|
|
>
|
|
<div
|
|
className={`flex h-8 w-8 items-center justify-center rounded-lg ${zoom >= 2 ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-black/5'}`}
|
|
onClick={(e) => {
|
|
if (zoom >= 2)
|
|
return
|
|
|
|
e.stopPropagation()
|
|
zoomIn()
|
|
}}
|
|
>
|
|
<RiZoomInLine className="h-4 w-4 text-text-tertiary hover:text-text-secondary" />
|
|
</div>
|
|
</TipPopup>
|
|
</div>
|
|
</div>
|
|
</PortalToFollowElemTrigger>
|
|
<PortalToFollowElemContent className="z-[60]">
|
|
<div className="w-[192px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]">
|
|
{
|
|
ZOOM_IN_OUT_OPTIONS.map((options, i) => (
|
|
<Fragment key={i}>
|
|
{
|
|
i !== 0 && (
|
|
<Divider className="m-0" />
|
|
)
|
|
}
|
|
<div className="p-1">
|
|
{
|
|
options.map(option => (
|
|
<div
|
|
key={option.key}
|
|
className={`flex h-8 cursor-pointer items-center justify-between space-x-1 rounded-lg px-2 py-1.5 text-text-secondary system-md-regular hover:bg-state-base-hover ${
|
|
option.key === ZoomType.toggleUserComments && isCommentMode
|
|
? 'cursor-not-allowed opacity-50'
|
|
: ''
|
|
}`}
|
|
onClick={() => handleZoom(option.key)}
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
{option.key === ZoomType.toggleUserComments && showUserComments && (
|
|
<RiCheckLine className="h-4 w-4 text-text-accent" />
|
|
)}
|
|
{option.key === ZoomType.toggleUserComments && !showUserComments && (
|
|
<div className="h-4 w-4" />
|
|
)}
|
|
{option.key === ZoomType.toggleUserCursors && showUserCursors && (
|
|
<RiCheckLine className="h-4 w-4 text-text-accent" />
|
|
)}
|
|
{option.key === ZoomType.toggleUserCursors && !showUserCursors && (
|
|
<div className="h-4 w-4" />
|
|
)}
|
|
{option.key === ZoomType.toggleMiniMap && showMiniMap && (
|
|
<RiCheckLine className="h-4 w-4 text-text-accent" />
|
|
)}
|
|
{option.key === ZoomType.toggleMiniMap && !showMiniMap && (
|
|
<div className="h-4 w-4" />
|
|
)}
|
|
{option.key === ZoomType.zoomToFit && (
|
|
<RiFullscreenLine className="h-4 w-4 text-text-tertiary" />
|
|
)}
|
|
{option.key !== ZoomType.toggleUserComments
|
|
&& option.key !== ZoomType.toggleUserCursors
|
|
&& option.key !== ZoomType.toggleMiniMap
|
|
&& option.key !== ZoomType.zoomToFit && (
|
|
<div className="h-4 w-4" />
|
|
)}
|
|
<span>{option.text}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-0.5">
|
|
{
|
|
option.key === ZoomType.zoomToFit && (
|
|
<ShortcutsName keys={['ctrl', '1']} />
|
|
)
|
|
}
|
|
{
|
|
option.key === ZoomType.zoomTo50 && (
|
|
<ShortcutsName keys={['shift', '5']} />
|
|
)
|
|
}
|
|
{
|
|
option.key === ZoomType.zoomTo100 && (
|
|
<ShortcutsName keys={['shift', '1']} />
|
|
)
|
|
}
|
|
</div>
|
|
</div>
|
|
))
|
|
}
|
|
</div>
|
|
</Fragment>
|
|
))
|
|
}
|
|
</div>
|
|
</PortalToFollowElemContent>
|
|
</PortalToFollowElem>
|
|
)
|
|
}
|
|
|
|
export default memo(ZoomInOut)
|