dify
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import type { ScheduleFrequency } from '../types'
|
||||
|
||||
type FrequencySelectorProps = {
|
||||
frequency: ScheduleFrequency
|
||||
onChange: (frequency: ScheduleFrequency) => void
|
||||
}
|
||||
|
||||
const FrequencySelector = ({ frequency, onChange }: FrequencySelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const frequencies = useMemo(() => [
|
||||
{ value: 'frequency-header', name: t('workflow.nodes.triggerSchedule.frequency.label'), isGroup: true },
|
||||
{ value: 'hourly', name: t('workflow.nodes.triggerSchedule.frequency.hourly') },
|
||||
{ value: 'daily', name: t('workflow.nodes.triggerSchedule.frequency.daily') },
|
||||
{ value: 'weekly', name: t('workflow.nodes.triggerSchedule.frequency.weekly') },
|
||||
{ value: 'monthly', name: t('workflow.nodes.triggerSchedule.frequency.monthly') },
|
||||
], [t])
|
||||
|
||||
return (
|
||||
<SimpleSelect
|
||||
key={`${frequency}-${frequencies[0]?.name}`} // Include translation in key to force re-render
|
||||
items={frequencies}
|
||||
defaultValue={frequency}
|
||||
onSelect={item => onChange(item.value as ScheduleFrequency)}
|
||||
placeholder={t('workflow.nodes.triggerSchedule.selectFrequency')}
|
||||
className="w-full py-2"
|
||||
wrapperClassName="h-auto"
|
||||
optionWrapClassName="min-w-40"
|
||||
notClearable={true}
|
||||
allowSearch={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FrequencySelector
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCalendarLine, RiCodeLine } from '@remixicon/react'
|
||||
import { SegmentedControl } from '@/app/components/base/segmented-control'
|
||||
import type { ScheduleMode } from '../types'
|
||||
|
||||
type ModeSwitcherProps = {
|
||||
mode: ScheduleMode
|
||||
onChange: (mode: ScheduleMode) => void
|
||||
}
|
||||
|
||||
const ModeSwitcher = ({ mode, onChange }: ModeSwitcherProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const options = [
|
||||
{
|
||||
Icon: RiCalendarLine,
|
||||
text: t('workflow.nodes.triggerSchedule.mode.visual'),
|
||||
value: 'visual' as const,
|
||||
},
|
||||
{
|
||||
Icon: RiCodeLine,
|
||||
text: t('workflow.nodes.triggerSchedule.mode.cron'),
|
||||
value: 'cron' as const,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
options={options}
|
||||
value={mode}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModeSwitcher
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Asterisk, CalendarCheckLine } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import type { ScheduleMode } from '../types'
|
||||
|
||||
type ModeToggleProps = {
|
||||
mode: ScheduleMode
|
||||
onChange: (mode: ScheduleMode) => void
|
||||
}
|
||||
|
||||
const ModeToggle = ({ mode, onChange }: ModeToggleProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleToggle = () => {
|
||||
const newMode = mode === 'visual' ? 'cron' : 'visual'
|
||||
onChange(newMode)
|
||||
}
|
||||
|
||||
const currentText = mode === 'visual'
|
||||
? t('workflow.nodes.triggerSchedule.useCronExpression')
|
||||
: t('workflow.nodes.triggerSchedule.useVisualPicker')
|
||||
|
||||
const currentIcon = mode === 'visual' ? Asterisk : CalendarCheckLine
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
className="flex cursor-pointer items-center gap-1 rounded-lg px-2 py-1 text-sm text-text-secondary hover:bg-state-base-hover"
|
||||
>
|
||||
{React.createElement(currentIcon, { className: 'w-4 h-4' })}
|
||||
<span>{currentText}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModeToggle
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type MonthlyDaysSelectorProps = {
|
||||
selectedDays: (number | 'last')[]
|
||||
onChange: (days: (number | 'last')[]) => void
|
||||
}
|
||||
|
||||
const MonthlyDaysSelector = ({ selectedDays, onChange }: MonthlyDaysSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleDayClick = (day: number | 'last') => {
|
||||
const current = selectedDays || []
|
||||
const newSelected = current.includes(day)
|
||||
? current.filter(d => d !== day)
|
||||
: [...current, day]
|
||||
// Ensure at least one day is selected (consistent with WeekdaySelector)
|
||||
onChange(newSelected.length > 0 ? newSelected : [day])
|
||||
}
|
||||
|
||||
const isDaySelected = (day: number | 'last') => selectedDays?.includes(day) || false
|
||||
|
||||
const days = Array.from({ length: 31 }, (_, i) => i + 1)
|
||||
const rows = [
|
||||
days.slice(0, 7),
|
||||
days.slice(7, 14),
|
||||
days.slice(14, 21),
|
||||
days.slice(21, 28),
|
||||
[29, 30, 31, 'last' as const],
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="mb-2 block text-xs font-medium text-text-tertiary">
|
||||
{t('workflow.nodes.triggerSchedule.days')}
|
||||
</label>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div key={rowIndex} className="grid grid-cols-7 gap-1.5">
|
||||
{row.map(day => (
|
||||
<button
|
||||
key={day}
|
||||
type="button"
|
||||
onClick={() => handleDayClick(day)}
|
||||
className={`rounded-lg border bg-components-option-card-option-bg py-1 text-xs transition-colors ${
|
||||
day === 'last' ? 'col-span-2 min-w-0' : ''
|
||||
} ${
|
||||
isDaySelected(day)
|
||||
? 'border-util-colors-blue-brand-blue-brand-600 text-text-secondary'
|
||||
: 'border-divider-subtle text-text-tertiary hover:border-divider-regular hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
{day === 'last' ? (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span>{t('workflow.nodes.triggerSchedule.lastDay')}</span>
|
||||
<Tooltip
|
||||
popupContent={t('workflow.nodes.triggerSchedule.lastDayTooltip')}
|
||||
>
|
||||
<RiQuestionLine className="h-3 w-3 text-text-quaternary" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
day
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
{/* Fill empty cells in the last row (Last day takes 2 cols, so need 1 less) */}
|
||||
{rowIndex === rows.length - 1 && Array.from({ length: 7 - row.length - 1 }, (_, i) => (
|
||||
<div key={`empty-${i}`} className="invisible"></div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Warning message for day 31 - aligned with grid */}
|
||||
{selectedDays?.includes(31) && (
|
||||
<div className="mt-1.5 grid grid-cols-7 gap-1.5">
|
||||
<div className="col-span-7 text-xs text-gray-500">
|
||||
{t('workflow.nodes.triggerSchedule.lastDayTooltip')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MonthlyDaysSelector
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ScheduleTriggerNodeType } from '../types'
|
||||
import { getFormattedExecutionTimes } from '../utils/execution-time-calculator'
|
||||
|
||||
type NextExecutionTimesProps = {
|
||||
data: ScheduleTriggerNodeType
|
||||
}
|
||||
|
||||
const NextExecutionTimes = ({ data }: NextExecutionTimesProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!data.frequency)
|
||||
return null
|
||||
|
||||
const executionTimes = getFormattedExecutionTimes(data, 5)
|
||||
|
||||
if (executionTimes.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-medium text-gray-500">
|
||||
{t('workflow.nodes.triggerSchedule.nextExecutionTimes')}
|
||||
</label>
|
||||
<div className="flex min-h-[80px] flex-col rounded-xl bg-components-input-bg-normal py-2">
|
||||
{executionTimes.map((time, index) => (
|
||||
<div key={index} className="flex items-baseline text-xs">
|
||||
<span className="w-6 select-none text-right font-mono font-normal leading-[150%] tracking-wider text-text-quaternary">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span className="pl-2 pr-3 font-mono font-normal leading-[150%] tracking-wider text-text-secondary">
|
||||
{time}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NextExecutionTimes
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
type OnMinuteSelectorProps = {
|
||||
value?: number
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const OnMinuteSelector = ({ value = 0, onChange }: OnMinuteSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-medium text-gray-500">
|
||||
{t('workflow.nodes.triggerSchedule.onMinute')}
|
||||
</label>
|
||||
<div className="relative flex h-8 items-center rounded-lg bg-components-input-bg-normal">
|
||||
<div className="flex h-full w-12 shrink-0 items-center justify-center text-[13px] text-components-input-text-filled">
|
||||
{value}
|
||||
</div>
|
||||
<div className="absolute left-12 top-0 h-full w-px bg-components-panel-bg"></div>
|
||||
<div className="flex h-full grow items-center pl-4 pr-3">
|
||||
<Slider
|
||||
className="w-full"
|
||||
value={value}
|
||||
min={0}
|
||||
max={59}
|
||||
step={1}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnMinuteSelector
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type WeekdaySelectorProps = {
|
||||
selectedDays: string[]
|
||||
onChange: (days: string[]) => void
|
||||
}
|
||||
|
||||
const WeekdaySelector = ({ selectedDays, onChange }: WeekdaySelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const weekdays = [
|
||||
{ key: 'sun', label: 'Sun' },
|
||||
{ key: 'mon', label: 'Mon' },
|
||||
{ key: 'tue', label: 'Tue' },
|
||||
{ key: 'wed', label: 'Wed' },
|
||||
{ key: 'thu', label: 'Thu' },
|
||||
{ key: 'fri', label: 'Fri' },
|
||||
{ key: 'sat', label: 'Sat' },
|
||||
]
|
||||
|
||||
const handleDaySelect = (dayKey: string) => {
|
||||
const current = selectedDays || []
|
||||
const newSelected = current.includes(dayKey)
|
||||
? current.filter(d => d !== dayKey)
|
||||
: [...current, dayKey]
|
||||
onChange(newSelected.length > 0 ? newSelected : [dayKey])
|
||||
}
|
||||
|
||||
const isDaySelected = (dayKey: string) => selectedDays.includes(dayKey)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="mb-2 block text-xs font-medium text-text-tertiary">
|
||||
{t('workflow.nodes.triggerSchedule.weekdays')}
|
||||
</label>
|
||||
<div className="flex gap-1.5">
|
||||
{weekdays.map(day => (
|
||||
<button
|
||||
key={day.key}
|
||||
type="button"
|
||||
className={`flex-1 rounded-lg border bg-components-option-card-option-bg py-1 text-xs transition-colors ${
|
||||
isDaySelected(day.key)
|
||||
? 'border-util-colors-blue-brand-blue-brand-600 text-text-secondary'
|
||||
: 'border-divider-subtle text-text-tertiary hover:border-divider-regular hover:text-text-secondary'
|
||||
}`}
|
||||
onClick={() => handleDaySelect(day.key)}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WeekdaySelector
|
||||
Reference in New Issue
Block a user