workflow publish

This commit is contained in:
StyleZhang 2024-03-18 18:38:17 +08:00
parent 7320ac41af
commit 4eb7546177
23 changed files with 375 additions and 80 deletions

View File

@ -9,9 +9,11 @@ import { Plus02 } from '@/app/components/base/icons/src/vender/line/general'
type ChooseFeatureProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const ChooseFeature = ({
onChange,
disabled,
}: ChooseFeatureProps) => {
const { t } = useTranslation()
const showFeaturesModal = useFeatures(s => s.showFeaturesModal)
@ -19,8 +21,11 @@ const ChooseFeature = ({
return (
<>
<Button
className='px-3 py-0 h-8 rounded-lg border border-primary-100 bg-primary-25 shadow-xs text-xs font-semibold text-primary-600'
onClick={() => setShowFeaturesModal(true)}
className={`
px-3 py-0 h-8 rounded-lg border border-primary-100 bg-primary-25 shadow-xs text-xs font-semibold text-primary-600
${disabled && 'cursor-not-allowed opacity-50'}
`}
onClick={() => !disabled && setShowFeaturesModal(true)}
>
<Plus02 className='mr-1 w-4 h-4' />
{t('appDebug.operation.addFeature')}

View File

@ -13,9 +13,11 @@ import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
type FileUploadProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const FileUpload = ({
onChange,
disabled,
}: FileUploadProps) => {
const { t } = useTranslation()
const featuresStore = useFeaturesStore()
@ -45,11 +47,12 @@ const FileUpload = ({
</div>
<div className='grow' />
<div className='flex items-center'>
<ParamConfig onChange={onChange} />
<ParamConfig onChange={onChange} disabled={disabled} />
<div className='ml-4 mr-3 w-[1px] h-3.5 bg-gray-200'></div>
<Switch
defaultValue={file.image.enabled}
onChange={handleSwitch}
disabled={disabled}
size='md'
/>
</div>

View File

@ -14,9 +14,11 @@ import {
type ParamsConfigProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const ParamsConfig = ({
onChange,
disabled,
}: ParamsConfigProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
@ -30,8 +32,8 @@ const ParamsConfig = ({
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<PortalToFollowElemTrigger onClick={() => !disabled && setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200', disabled && 'cursor-not-allowed opacity-50')}>
<Settings01 className='w-3.5 h-3.5 ' />
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
</div>

View File

@ -17,10 +17,12 @@ import Moderation from './moderation'
export type FeaturePanelProps = {
onChange?: OnFeaturesChange
openingStatementProps: OpeningStatementProps
disabled?: boolean
}
const FeaturePanel = ({
onChange,
openingStatementProps,
disabled,
}: FeaturePanelProps) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
@ -35,7 +37,10 @@ const FeaturePanel = ({
return (
<div className='space-y-3'>
<FileUpload onChange={onChange} />
<FileUpload
onChange={onChange}
disabled={disabled}
/>
{
showAdvanceFeature && (
<div>
@ -51,7 +56,11 @@ const FeaturePanel = ({
<div className='py-2 space-y-2'>
{
features.opening.enabled && (
<OpeningStatement {...openingStatementProps} onChange={onChange} />
<OpeningStatement
{...openingStatementProps}
onChange={onChange}
readonly={disabled}
/>
)
}
{
@ -61,7 +70,7 @@ const FeaturePanel = ({
}
{
features.text2speech.enabled && (
<TextToSpeech onChange={onChange} />
<TextToSpeech onChange={onChange} disabled={disabled} />
)
}
{
@ -93,7 +102,7 @@ const FeaturePanel = ({
<div className='py-2 space-y-2'>
{
features.moderation.enabled && (
<Moderation onChange={onChange} />
<Moderation onChange={onChange} disabled={disabled} />
)
}
</div>

View File

@ -16,9 +16,11 @@ import I18n from '@/context/i18n'
type ModerationProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const Moderation = ({
onChange,
disabled,
}: ModerationProps) => {
const { t } = useTranslation()
const { setShowModerationSettingModal } = useModalContext()
@ -32,6 +34,9 @@ const Moderation = ({
)
const handleOpenModerationSettingModal = () => {
if (disabled)
return
const {
features,
setFeatures,
@ -89,6 +94,7 @@ const Moderation = ({
className={`
shrink-0 flex items-center px-3 h-7 cursor-pointer rounded-md
text-xs text-gray-700 font-medium hover:bg-gray-200
${disabled && '!cursor-not-allowed'}
`}
onClick={handleOpenModerationSettingModal}
>

View File

@ -13,9 +13,11 @@ import AudioBtn from '@/app/components/base/audio-btn'
type TextToSpeechProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const TextToSpeech = ({
onChange,
disabled,
}: TextToSpeechProps) => {
const { t } = useTranslation()
const textToSpeech = useFeatures(s => s.features.text2speech)
@ -50,7 +52,7 @@ const TextToSpeech = ({
)}
</div>
<div className='shrink-0 flex items-center'>
<ParamsConfig onChange={onChange} />
<ParamsConfig onChange={onChange} disabled={disabled} />
</div>
</div>
)

View File

@ -13,9 +13,11 @@ import {
type ParamsConfigProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const ParamsConfig = ({
onChange,
disabled,
}: ParamsConfigProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
@ -29,7 +31,7 @@ const ParamsConfig = ({
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<PortalToFollowElemTrigger onClick={() => !disabled && setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<Settings01 className='w-3.5 h-3.5 ' />
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>

View File

@ -0,0 +1,9 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="clock-refresh">
<g id="Solid">
<path d="M9.76984 2.8375L9.71551 3.04027C8.27602 1.22839 5.68891 0.69459 3.62457 1.88644C2.25681 2.67611 1.43067 4.04369 1.27558 5.50073C1.24636 5.77532 1.44526 6.02161 1.71985 6.05084C1.99444 6.08007 2.24074 5.88116 2.26997 5.60657C2.39268 4.4537 3.04533 3.37556 4.12456 2.75247C5.7025 1.84145 7.66731 2.20754 8.82211 3.53002L8.65016 3.48395C8.38343 3.41248 8.10926 3.57077 8.03779 3.8375C7.96632 4.10424 8.12461 4.37841 8.39134 4.44988L9.75737 4.8159C10.0241 4.88737 10.2983 4.72908 10.3697 4.46235L10.7358 3.09632C10.8072 2.82959 10.6489 2.55542 10.3822 2.48395C10.1155 2.41248 9.84131 2.57077 9.76984 2.8375Z" fill="#667085"/>
<path d="M10.2792 5.94921C10.5538 5.97844 10.7527 6.22473 10.7235 6.49932C10.5684 7.95635 9.74225 9.32394 8.3745 10.1136C6.31011 11.3055 3.72295 10.7716 2.28347 8.95968L2.22918 9.1623C2.15771 9.42903 1.88354 9.58732 1.61681 9.51585C1.35008 9.44438 1.19178 9.17021 1.26325 8.90348L1.62928 7.53746C1.70075 7.27072 1.97492 7.11243 2.24165 7.1839L3.60768 7.54993C3.87441 7.6214 4.0327 7.89557 3.96123 8.1623C3.88976 8.42903 3.61559 8.58732 3.34886 8.51585L3.17668 8.46972C4.33144 9.79246 6.29644 10.1587 7.8745 9.24758C8.95373 8.62449 9.60638 7.54634 9.72909 6.39348C9.75832 6.11889 10.0046 5.91998 10.2792 5.94921Z" fill="#667085"/>
<path d="M6.49954 3.74997C6.49954 3.47382 6.27568 3.24997 5.99954 3.24997C5.7234 3.24997 5.49954 3.47382 5.49954 3.74997V5.99997C5.49954 6.1756 5.59169 6.33835 5.74229 6.42871L6.99229 7.17871C7.22908 7.32079 7.53621 7.244 7.67828 7.00721C7.82036 6.77042 7.74358 6.46329 7.50679 6.32122L6.49954 5.71687V3.74997Z" fill="#667085"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,62 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "12",
"viewBox": "0 0 12 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "clock-refresh"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Solid"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.76984 2.8375L9.71551 3.04027C8.27602 1.22839 5.68891 0.69459 3.62457 1.88644C2.25681 2.67611 1.43067 4.04369 1.27558 5.50073C1.24636 5.77532 1.44526 6.02161 1.71985 6.05084C1.99444 6.08007 2.24074 5.88116 2.26997 5.60657C2.39268 4.4537 3.04533 3.37556 4.12456 2.75247C5.7025 1.84145 7.66731 2.20754 8.82211 3.53002L8.65016 3.48395C8.38343 3.41248 8.10926 3.57077 8.03779 3.8375C7.96632 4.10424 8.12461 4.37841 8.39134 4.44988L9.75737 4.8159C10.0241 4.88737 10.2983 4.72908 10.3697 4.46235L10.7358 3.09632C10.8072 2.82959 10.6489 2.55542 10.3822 2.48395C10.1155 2.41248 9.84131 2.57077 9.76984 2.8375Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10.2792 5.94921C10.5538 5.97844 10.7527 6.22473 10.7235 6.49932C10.5684 7.95635 9.74225 9.32394 8.3745 10.1136C6.31011 11.3055 3.72295 10.7716 2.28347 8.95968L2.22918 9.1623C2.15771 9.42903 1.88354 9.58732 1.61681 9.51585C1.35008 9.44438 1.19178 9.17021 1.26325 8.90348L1.62928 7.53746C1.70075 7.27072 1.97492 7.11243 2.24165 7.1839L3.60768 7.54993C3.87441 7.6214 4.0327 7.89557 3.96123 8.1623C3.88976 8.42903 3.61559 8.58732 3.34886 8.51585L3.17668 8.46972C4.33144 9.79246 6.29644 10.1587 7.8745 9.24758C8.95373 8.62449 9.60638 7.54634 9.72909 6.39348C9.75832 6.11889 10.0046 5.91998 10.2792 5.94921Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M6.49954 3.74997C6.49954 3.47382 6.27568 3.24997 5.99954 3.24997C5.7234 3.24997 5.49954 3.47382 5.49954 3.74997V5.99997C5.49954 6.1756 5.59169 6.33835 5.74229 6.42871L6.99229 7.17871C7.22908 7.32079 7.53621 7.244 7.67828 7.00721C7.82036 6.77042 7.74358 6.46329 7.50679 6.32122L6.49954 5.71687V3.74997Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "ClockRefresh"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ClockRefresh.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ClockRefresh'
export default Icon

View File

@ -1,2 +1,3 @@
export { default as ClockFastForward } from './ClockFastForward'
export { default as ClockPlay } from './ClockPlay'
export { default as ClockRefresh } from './ClockRefresh'

View File

@ -17,7 +17,8 @@ import {
const Features = () => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
const setShowFeaturesPanel = useStore(state => state.setShowFeaturesPanel)
const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel)
const isRestoring = useStore(s => s.isRestoring)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const handleFeaturesChange = useCallback(() => {
@ -32,7 +33,10 @@ const Features = () => {
{
isChatMode && (
<>
<FeaturesChoose onChange={handleFeaturesChange} />
<FeaturesChoose
disabled={isRestoring}
onChange={handleFeaturesChange}
/>
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
</>
)
@ -47,6 +51,7 @@ const Features = () => {
</div>
<div className='p-4'>
<FeaturesPanel
disabled={isRestoring}
onChange={handleFeaturesChange}
openingStatementProps={{
onAutoAddPromptVariable: () => {},

View File

@ -1,11 +1,13 @@
import { memo } from 'react'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { useWorkflow } from '../hooks'
import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general'
import { useStore } from '@/app/components/workflow/store'
const EditingTitle = () => {
const { t } = useTranslation()
const { formatTimeFromNow } = useWorkflow()
const draftUpdatedAt = useStore(state => state.draftUpdatedAt)
const publishedAt = useStore(state => state.publishedAt)
@ -14,17 +16,17 @@ const EditingTitle = () => {
<Edit03 className='mr-1 w-3 h-3 text-gray-400' />
{t('workflow.common.editing')}
{
draftUpdatedAt && (
!!draftUpdatedAt && (
<>
<span className='flex items-center mx-1'>·</span>
{t('workflow.common.autoSaved')} {dayjs(draftUpdatedAt * 1000).format('HH:mm:ss')}
{t('workflow.common.autoSaved')} {dayjs(draftUpdatedAt).format('HH:mm:ss')}
</>
)
}
<span className='flex items-center mx-1'>·</span>
{
publishedAt
? `${t('workflow.common.published')} ${dayjs(publishedAt).fromNow()}`
? `${t('workflow.common.published')} ${formatTimeFromNow(publishedAt)}`
: t('workflow.common.unpublished')
}
</div>

View File

@ -12,6 +12,7 @@ import { useWorkflowRun } from '../hooks'
import RunAndHistory from './run-and-history'
import EditingTitle from './editing-title'
import RunningTitle from './running-title'
import RestoringTitle from './restoring-title'
import Publish from './publish'
import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout'
import Button from '@/app/components/base/button'
@ -24,7 +25,11 @@ const Header: FC = () => {
const appDetail = useAppStore(s => s.appDetail)
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
const runningStatus = useStore(s => s.runningStatus)
const { handleRunSetting } = useWorkflowRun()
const isRestoring = useStore(s => s.isRestoring)
const {
handleRunSetting,
handleRestoreFromPublishedWorkflow,
} = useWorkflowRun()
const handleShowFeatures = useCallback(() => {
if (runningStatus)
@ -37,6 +42,15 @@ const Header: FC = () => {
handleRunSetting(true)
}, [handleRunSetting])
const handleCancelRestore = useCallback(() => {
workflowStore.setState({ isRestoring: false })
}, [workflowStore])
const handleRestore = useCallback(() => {
handleRestoreFromPublishedWorkflow()
workflowStore.setState({ isRestoring: false })
}, [handleRestoreFromPublishedWorkflow, workflowStore])
return (
<div
className='absolute top-0 left-0 flex items-center justify-between px-3 w-full h-14 z-10'
@ -51,42 +65,80 @@ const Header: FC = () => {
)
}
{
!runningStatus && <EditingTitle />
!runningStatus && !isRestoring && <EditingTitle />
}
{
runningStatus && <RunningTitle />
runningStatus && !isRestoring && <RunningTitle />
}
{
isRestoring && <RestoringTitle />
}
</div>
<div className='flex items-center'>
{
runningStatus && (
{
!isRestoring && (
<div className='flex items-center'>
{
runningStatus && (
<Button
className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-primary-600
border-[0.5px] border-gray-200 shadow-xs
`}
onClick={handleGoBackToEdit}
>
<ArrowNarrowLeft className='mr-1 w-4 h-4' />
{t('workflow.common.goBackToEdit')}
</Button>
)
}
<RunAndHistory />
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Button
className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-primary-600
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
border-[0.5px] border-gray-200 shadow-xs
${runningStatus && '!cursor-not-allowed opacity-50'}
`}
onClick={handleGoBackToEdit}
onClick={handleShowFeatures}
>
<ArrowNarrowLeft className='mr-1 w-4 h-4' />
{t('workflow.common.goBackToEdit')}
<Grid01 className='mr-1 w-4 h-4 text-gray-500' />
{t('workflow.common.features')}
</Button>
)
}
<RunAndHistory />
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Button
className={`
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
border-[0.5px] border-gray-200 shadow-xs
${runningStatus && '!cursor-not-allowed opacity-50'}
`}
onClick={handleShowFeatures}
>
<Grid01 className='mr-1 w-4 h-4 text-gray-500' />
{t('workflow.common.features')}
</Button>
<Publish />
</div>
<Publish />
</div>
)
}
{
isRestoring && (
<div className='flex items-center'>
<Button
className={`
px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
border-[0.5px] border-gray-200 shadow-xs
${runningStatus && '!cursor-not-allowed opacity-50'}
`}
onClick={handleShowFeatures}
>
<Grid01 className='mr-1 w-4 h-4 text-gray-500' />
{t('workflow.common.features')}
</Button>
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Button
className='mr-2 px-3 py-0 h-8 bg-white text-[13px] text-gray-700 font-medium border-[0.5px] border-gray-200 shadow-xs'
onClick={handleCancelRestore}
>
{t('common.operation.cancel')}
</Button>
<Button
className='px-3 py-0 h-8 text-[13px] font-medium shadow-xs'
onClick={handleRestore}
type='primary'
>
{t('workflow.common.restore')}
</Button>
</div>
)
}
</div>
)
}

View File

@ -1,5 +1,6 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
@ -7,6 +8,7 @@ import {
useStore,
useWorkflowStore,
} from '../store'
import { useWorkflow } from '../hooks'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
@ -18,8 +20,12 @@ import { useStore as useAppStore } from '@/app/components/app/store'
const Publish = () => {
const { t } = useTranslation()
const [published, setPublished] = useState(false)
const workflowStore = useWorkflowStore()
const { formatTimeFromNow } = useWorkflow()
const runningStatus = useStore(s => s.runningStatus)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const publishedAt = useStore(s => s.publishedAt)
const [open, setOpen] = useState(false)
const handlePublish = async () => {
@ -27,13 +33,34 @@ const Publish = () => {
try {
const res = await publishWorkflow(`/apps/${appId}/workflows/publish`)
if (res)
workflowStore.setState({ publishedAt: res.created_at })
if (res) {
setPublished(true)
workflowStore.getState().setPublishedAt(res.created_at)
}
}
catch (e) {
setPublished(false)
}
}
const handleRestore = useCallback(() => {
workflowStore.getState().setIsRestoring(true)
setOpen(false)
}, [workflowStore])
const handleTrigger = useCallback(() => {
if (runningStatus)
return
if (open)
setOpen(false)
if (!open) {
setOpen(true)
setPublished(false)
}
}, [runningStatus, open])
return (
<PortalToFollowElem
open={open}
@ -44,12 +71,7 @@ const Publish = () => {
crossAxis: -5,
}}
>
<PortalToFollowElemTrigger onClick={() => {
if (runningStatus)
return
setOpen(v => !v)
}}>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<Button
type='primary'
className={`
@ -67,29 +89,44 @@ const Publish = () => {
{t('workflow.common.currentDraft').toLocaleUpperCase()}
</div>
<div className='flex items-center h-[18px] text-[13px] font-medium text-gray-700'>
{t('workflow.common.autoSaved')} 3 min ago · Evan
{t('workflow.common.autoSaved')} {formatTimeFromNow(draftUpdatedAt)}
</div>
<Button
type='primary'
className='mt-3 px-3 py-0 w-full h-8 border-[0.5px] border-primary-700 rounded-lg text-[13px] font-medium'
className={`
mt-3 px-3 py-0 w-full h-8 border-[0.5px] border-primary-700 rounded-lg text-[13px] font-medium
${published && 'border-transparent'}
`}
onClick={handlePublish}
disabled={published}
>
{t('workflow.common.publish')}
</Button>
</div>
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'>
<div className='flex items-center h-6 text-xs font-medium text-gray-500'>
{t('workflow.common.latestPublished').toLocaleUpperCase()}
</div>
<div className='flex justify-between'>
<div className='flex items-center mt-[3px] mb-[3px] leading-[18px] text-[13px] font-medium text-gray-700'>
{t('workflow.common.autoSaved')} 3 min ago · Evan
{
!!publishedAt && (
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'>
<div className='flex items-center h-6 text-xs font-medium text-gray-500'>
{t('workflow.common.latestPublished').toLocaleUpperCase()}
</div>
<div className='flex justify-between'>
<div className='flex items-center mt-[3px] mb-[3px] leading-[18px] text-[13px] font-medium text-gray-700'>
{t('workflow.common.autoSaved')} {formatTimeFromNow(publishedAt)}
</div>
<Button
className={`
ml-2 px-2 py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] border-gray-200
${published && 'opacity-50 border-transparent shadow-none bg-transparent'}
`}
onClick={handleRestore}
disabled={published}
>
{t('workflow.common.restore')}
</Button>
</div>
</div>
<Button className='ml-2 px-2 py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] border-gray-200'>
{t('workflow.common.restore')}
</Button>
</div>
</div>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -0,0 +1,21 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { useWorkflow } from '../hooks'
import { useStore } from '../store'
import { ClockRefresh } from '@/app/components/base/icons/src/vender/line/time'
const RestoringTitle = () => {
const { t } = useTranslation()
const { formatTimeFromNow } = useWorkflow()
const publishedAt = useStore(state => state.publishedAt)
return (
<div className='flex items-center h-[18px] text-xs text-gray-500'>
<ClockRefresh className='mr-1 w-3 h-3 text-gray-500' />
{t('workflow.common.latestPublished')}
{formatTimeFromNow(publishedAt)}
</div>
)
}
export default memo(RestoringTitle)

View File

@ -62,7 +62,7 @@ export const useNodesSyncDraft = () => {
},
},
}).then((res) => {
workflowStore.setState({ draftUpdatedAt: res.updated_at })
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
})
}
}, [store, reactFlow, featuresStore, workflowStore])
@ -73,9 +73,12 @@ export const useNodesSyncDraft = () => {
})
const handleSyncWorkflowDraft = useCallback((shouldDelay?: boolean) => {
const { runningStatus } = workflowStore.getState()
const {
runningStatus,
isRestoring,
} = workflowStore.getState()
if (runningStatus)
if (runningStatus || isRestoring)
return
if (shouldDelay)

View File

@ -12,12 +12,17 @@ import {
import { useStore as useAppStore } from '@/app/components/app/store'
import type { IOtherOptions } from '@/service/base'
import { ssePost } from '@/service/base'
import { stopWorkflowRun } from '@/service/workflow'
import {
fetchPublishedWorkflow,
stopWorkflowRun,
} from '@/service/workflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
export const useWorkflowRun = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const featuresStore = useFeaturesStore()
const handleBackupDraft = useCallback(() => {
const {
@ -178,10 +183,34 @@ export const useWorkflowRun = () => {
stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`)
}, [workflowStore])
const handleRestoreFromPublishedWorkflow = useCallback(async () => {
const appDetail = useAppStore.getState().appDetail
const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
if (publishedWorkflow) {
const {
setNodes,
setEdges,
} = store.getState()
const { setViewport } = reactflow
const nodes = publishedWorkflow.graph.nodes
const edges = publishedWorkflow.graph.edges
const viewport = publishedWorkflow.graph.viewport
setNodes(nodes)
setEdges(edges)
if (viewport)
setViewport(viewport)
featuresStore?.setState({ features: publishedWorkflow.features })
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
}
}, [store, reactflow, featuresStore, workflowStore])
return {
handleBackupDraft,
handleRunSetting,
handleRun,
handleStopRun,
handleRestoreFromPublishedWorkflow,
}
}

View File

@ -2,7 +2,9 @@ import {
useCallback,
useEffect,
} from 'react'
import dayjs from 'dayjs'
import { uniqBy } from 'lodash-es'
import { useContext } from 'use-context-selector'
import useSWR from 'swr'
import produce from 'immer'
import {
@ -33,10 +35,12 @@ import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
fetchNodesDefaultConfigs,
fetchPublishedWorkflow,
fetchWorkflowDraft,
syncWorkflowDraft,
} from '@/service/workflow'
import { fetchCollectionList } from '@/service/tools'
import I18n from '@/context/i18n'
export const useIsChatMode = () => {
const appDetail = useAppStore(s => s.appDetail)
@ -45,6 +49,7 @@ export const useIsChatMode = () => {
}
export const useWorkflow = () => {
const { locale } = useContext(I18n)
const store = useStoreApi()
const reactflow = useReactFlow()
const workflowStore = useWorkflowStore()
@ -211,12 +216,17 @@ export const useWorkflow = () => {
return true
}, [store, nodesExtraData])
const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
}, [locale])
return {
handleLayout,
getTreeLeafNodes,
getBeforeNodesInSameBranch,
getAfterNodesInSameBranch,
isValidConnection,
formatTimeFromNow,
}
}
@ -226,10 +236,11 @@ export const useWorkflowInit = () => {
const appDetail = useAppStore(state => state.appDetail)!
const { data, error, mutate } = useSWR(`/apps/${appDetail.id}/workflows/draft`, fetchWorkflowDraft)
const handleFetchPreloadData = async () => {
const handleFetchPreloadData = useCallback(async () => {
try {
const toolsets = await fetchCollectionList()
const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`)
const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
workflowStore.setState({
toolsets,
@ -245,19 +256,20 @@ export const useWorkflowInit = () => {
return acc
}, {} as Record<string, any>),
})
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
}
catch (e) {
}
}
}, [workflowStore, appDetail])
useEffect(() => {
handleFetchPreloadData()
}, [])
}, [handleFetchPreloadData])
useEffect(() => {
if (data)
workflowStore.setState({ draftUpdatedAt: data.updated_at })
workflowStore.getState().setDraftUpdatedAt(data.updated_at)
}, [data, workflowStore])
if (error && error.json && !error.bodyUsed && appDetail) {
@ -280,7 +292,7 @@ export const useWorkflowInit = () => {
features: {},
},
}).then((res) => {
workflowStore.setState({ draftUpdatedAt: res.updated_at })
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
mutate()
})
}

View File

@ -1,10 +1,12 @@
import { memo } from 'react'
import cn from 'classnames'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { WorkflowRunningStatus } from '../types'
import { useIsChatMode } from '../hooks'
import {
useIsChatMode,
useWorkflow,
} from '../hooks'
import { CheckCircle, XClose } from '@/app/components/base/icons/src/vender/line/general'
import { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import {
@ -19,6 +21,7 @@ const RunHistory = () => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore()
const { formatTimeFromNow } = useWorkflow()
const workflowStore = useWorkflowStore()
const workflowRunId = useRunHistoryStore(state => state.workflowRunId)
const { data: runList, isLoading: runListLoading } = useSWR((appDetail && !isChatMode) ? `/apps/${appDetail.id}/workflow-runs` : null, fetchWorkflowRunHistory)
@ -93,7 +96,7 @@ const RunHistory = () => {
{`Test ${isChatMode ? 'Chat' : 'Run'}#${item.sequence_number}`}
</div>
<div className='flex items-center text-xs text-gray-500 leading-[18px]'>
{item.created_by_account.name} · {dayjs((item.finished_at || item.created_at) * 1000).fromNow()}
{item.created_by_account.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
</div>
</div>
</div>

View File

@ -1,9 +1,9 @@
import { useContext } from 'react'
import {
create,
useStore as useZustandStore,
} from 'zustand'
import type { Viewport } from 'reactflow'
import { useContext } from 'react'
import type {
HelpLineHorizontalPosition,
HelpLineVerticalPosition,
@ -44,6 +44,7 @@ type State = {
notInitialWorkflow: boolean
nodesDefaultConfigs: Record<string, any>
nodeAnimation: boolean
isRestoring: boolean
}
type Action = {
@ -66,6 +67,7 @@ type Action = {
setNotInitialWorkflow: (notInitialWorkflow: boolean) => void
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
setNodeAnimation: (nodeAnimation: boolean) => void
setIsRestoring: (isRestoring: boolean) => void
}
export const createWorkflowStore = () => {
@ -91,9 +93,9 @@ export const createWorkflowStore = () => {
toolsMap: {},
setToolsMap: toolsMap => set(() => ({ toolsMap })),
draftUpdatedAt: 0,
setDraftUpdatedAt: draftUpdatedAt => set(() => ({ draftUpdatedAt })),
setDraftUpdatedAt: draftUpdatedAt => set(() => ({ draftUpdatedAt: draftUpdatedAt ? draftUpdatedAt * 1000 : 0 })),
publishedAt: 0,
setPublishedAt: publishedAt => set(() => ({ publishedAt })),
setPublishedAt: publishedAt => set(() => ({ publishedAt: publishedAt ? publishedAt * 1000 : 0 })),
runningStatus: undefined,
setRunningStatus: runningStatus => set(() => ({ runningStatus })),
showInputsPanel: false,
@ -108,6 +110,8 @@ export const createWorkflowStore = () => {
setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })),
nodeAnimation: false,
setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })),
isRestoring: false,
setIsRestoring: isRestoring => set(() => ({ isRestoring })),
}))
}

View File

@ -36,6 +36,10 @@ export const publishWorkflow = (url: string) => {
return post<CommonResponse & { created_at: number }>(url)
}
export const fetchPublishedWorkflow: Fetcher<FetchWorkflowDraftResponse, string> = (url) => {
return get<FetchWorkflowDraftResponse>(url)
}
export const stopWorkflowRun = (url: string) => {
return post<CommonResponse>(url)
}

View File

@ -40,6 +40,12 @@ export type FetchWorkflowDraftResponse = {
viewport?: Viewport
}
features?: any
created_at: number
created_by: {
id: string
name: string
email: string
}
updated_at: number
}