diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/date-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/date-input.tsx new file mode 100644 index 0000000000..8aa5d44a92 --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/date-input.tsx @@ -0,0 +1,47 @@ +'use client' + +import * as React from 'react' +import { format } from 'date-fns' +import { Calendar as CalendarIcon } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Calendar } from '@/components/ui/calendar' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { cn } from '@/lib/utils' +import { useSubBlockValue } from '../hooks/use-sub-block-value' + +interface DateInputProps { + blockId: string + subBlockId: string + placeholder?: string +} + +export function DateInput({ blockId, subBlockId, placeholder }: DateInputProps) { + const [value, setValue] = useSubBlockValue(blockId, subBlockId, true) + + const date = value ? new Date(value) : undefined + + return ( + + + + + + setValue(date?.toISOString() || '')} + initialFocus + /> + + + ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/time-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/time-input.tsx new file mode 100644 index 0000000000..4ce53b838c --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/time-input.tsx @@ -0,0 +1,153 @@ +'use client' + +import * as React from 'react' +import { Clock } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { cn } from '@/lib/utils' +import { useSubBlockValue } from '../hooks/use-sub-block-value' + +interface TimeInputProps { + blockId: string + subBlockId: string + placeholder?: string +} + +export function TimeInput({ blockId, subBlockId, placeholder }: TimeInputProps) { + const [value, setValue] = useSubBlockValue(blockId, subBlockId, true) + const [isOpen, setIsOpen] = React.useState(false) + + // Convert 24h time string to display format (12h with AM/PM) + const formatDisplayTime = (time: string) => { + if (!time) return '' + const [hours, minutes] = time.split(':') + const hour = parseInt(hours, 10) + const ampm = hour >= 12 ? 'PM' : 'AM' + const displayHour = hour % 12 || 12 + return `${displayHour}:${minutes} ${ampm}` + } + + // Convert display time to 24h format for storage + const formatStorageTime = (hour: number, minute: number, ampm: string) => { + const hours24 = ampm === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour + return `${hours24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}` + } + + const [hour, setHour] = React.useState('12') + const [minute, setMinute] = React.useState('00') + const [ampm, setAmpm] = React.useState<'AM' | 'PM'>('AM') + + // Update the time when any component changes + const updateTime = (newHour?: string, newMinute?: string, newAmpm?: 'AM' | 'PM') => { + const h = parseInt(newHour ?? hour) || 12 + const m = parseInt(newMinute ?? minute) || 0 + const p = newAmpm ?? ampm + setValue(formatStorageTime(h, m, p)) + } + + // Initialize from existing value + React.useEffect(() => { + if (value) { + const [hours, minutes] = value.split(':') + const hour24 = parseInt(hours, 10) + const minute = parseInt(minutes, 10) + const isAM = hour24 < 12 + setHour((hour24 % 12 || 12).toString()) + setMinute(minutes) + setAmpm(isAM ? 'AM' : 'PM') + } + }, [value]) + + const handleBlur = () => { + updateTime() + setIsOpen(false) + } + + return ( + { + setIsOpen(open) + if (!open) { + handleBlur() + } + }} + > + + + + +
+ { + const val = e.target.value.replace(/[^0-9]/g, '') + if (val === '') { + setHour('') + return + } + const numVal = parseInt(val) + if (!isNaN(numVal)) { + const newHour = Math.min(12, Math.max(1, numVal)).toString() + setHour(newHour) + updateTime(newHour) + } + }} + onBlur={() => { + const numVal = parseInt(hour) || 12 + setHour(numVal.toString()) + updateTime(numVal.toString()) + }} + type="text" + /> + : + { + const val = e.target.value.replace(/[^0-9]/g, '') + if (val === '') { + setMinute('') + return + } + const numVal = parseInt(val) + if (!isNaN(numVal)) { + const newMinute = Math.min(59, Math.max(0, numVal)).toString().padStart(2, '0') + setMinute(newMinute) + updateTime(undefined, newMinute) + } + }} + onBlur={() => { + const numVal = parseInt(minute) || 0 + setMinute(numVal.toString().padStart(2, '0')) + updateTime(undefined, numVal.toString()) + }} + type="text" + /> + +
+
+
+ ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index 59841f03c8..74e1bafafd 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -3,6 +3,7 @@ import { SubBlockConfig } from '../../../../../../../blocks/types' import { CheckboxList } from './components/checkbox-list' import { Code } from './components/code' import { ConditionInput } from './components/condition-input' +import { DateInput } from './components/date-input' import { Dropdown } from './components/dropdown' import { EvalInput } from './components/eval-input' import { LongInput } from './components/long-input' @@ -10,6 +11,7 @@ import { ShortInput } from './components/short-input' import { SliderInput } from './components/slider-input' import { Switch } from './components/switch' import { Table } from './components/table' +import { TimeInput } from './components/time-input' import { ToolInput } from './components/tool-input' interface SubBlockProps { @@ -97,6 +99,14 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) { ) case 'eval-input': return + case 'date-input': + return ( + + ) + case 'time-input': + return ( + + ) default: return null } diff --git a/blocks/blocks/starter.ts b/blocks/blocks/starter.ts index 6545cb65cb..ff16c4197b 100644 --- a/blocks/blocks/starter.ts +++ b/blocks/blocks/starter.ts @@ -40,7 +40,23 @@ export const StarterBlock: BlockConfig = { password: true, condition: { field: 'startWorkflow', value: 'webhook' }, }, - // Schedule configuration + // Common schedule fields for all frequency types + { + id: 'scheduleStartAt', + title: 'Start At', + type: 'date-input', + layout: 'half', + placeholder: 'Select day', + condition: { field: 'startWorkflow', value: 'schedule' }, + }, + { + id: 'scheduleTime', + title: 'Time', + type: 'time-input', + layout: 'half', + condition: { field: 'startWorkflow', value: 'schedule' }, + }, + // Frequency configuration { id: 'scheduleType', title: 'Frequency', @@ -73,124 +89,6 @@ export const StarterBlock: BlockConfig = { }, }, }, - { - id: 'minutesStartingAt', - title: 'Starting At', - type: 'short-input', - layout: 'full', - placeholder: '14:30 (24-hour format)', - condition: { - field: 'scheduleType', - value: 'minutes', - and: { - field: 'startWorkflow', - value: 'schedule', - }, - }, - }, - // Hourly schedule options - { - id: 'hourlyMinute', - title: 'Start at Minute', - type: 'short-input', - layout: 'full', - placeholder: '00-59', - condition: { - field: 'scheduleType', - value: 'hourly', - and: { - field: 'startWorkflow', - value: 'schedule', - }, - }, - }, - // Daily schedule options - { - id: 'dailyTime', - title: 'Time', - type: 'short-input', - layout: 'full', - placeholder: '14:30 (24-hour format)', - condition: { - field: 'scheduleType', - value: 'daily', - and: { - field: 'startWorkflow', - value: 'schedule', - }, - }, - }, - // Weekly schedule options - { - id: 'weeklyDay', - title: 'Day of Week', - type: 'dropdown', - layout: 'half', - options: [ - { label: 'Monday', id: 'MON' }, - { label: 'Tuesday', id: 'TUE' }, - { label: 'Wednesday', id: 'WED' }, - { label: 'Thursday', id: 'THU' }, - { label: 'Friday', id: 'FRI' }, - { label: 'Saturday', id: 'SAT' }, - { label: 'Sunday', id: 'SUN' }, - ], - value: () => 'MON', - condition: { - field: 'scheduleType', - value: 'weekly', - and: { - field: 'startWorkflow', - value: 'schedule', - }, - }, - }, - { - id: 'weeklyDayTime', - title: 'Time', - type: 'short-input', - layout: 'half', - placeholder: '14:30 (24-hour format)', - condition: { - field: 'scheduleType', - value: 'weekly', - and: { - field: 'startWorkflow', - value: 'schedule', - }, - }, - }, - // Monthly schedule options - { - id: 'monthlyDay', - title: 'Day of Month', - type: 'short-input', - layout: 'half', - placeholder: '1-31', - condition: { - field: 'scheduleType', - value: 'monthly', - and: { - field: 'startWorkflow', - value: 'schedule', - }, - }, - }, - { - id: 'monthlyTime', - title: 'Time', - type: 'short-input', - layout: 'half', - placeholder: '14:30 (24-hour format)', - condition: { - field: 'scheduleType', - value: 'monthly', - and: { - field: 'startWorkflow', - value: 'schedule', - }, - }, - }, // Custom cron options { id: 'cronExpression', diff --git a/blocks/types.ts b/blocks/types.ts index 22980fbc26..d848b93a0c 100644 --- a/blocks/types.ts +++ b/blocks/types.ts @@ -23,6 +23,8 @@ export type SubBlockType = | 'checkbox-list' // Multiple selection | 'condition-input' // Conditional logic | 'eval-input' // Evaluation input + | 'date-input' // Date input + | 'time-input' // Time input // Component width setting export type SubBlockLayout = 'full' | 'half' diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx new file mode 100644 index 0000000000..d1713131b2 --- /dev/null +++ b/components/ui/calendar.tsx @@ -0,0 +1,62 @@ +'use client' + +import * as React from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { DayPicker } from 'react-day-picker' +import { buttonVariants } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +export type CalendarProps = React.ComponentProps + +function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { + return ( + ( + + ), + IconRight: ({ className, ...props }) => ( + + ), + }} + {...props} + /> + ) +} +Calendar.displayName = 'Calendar' + +export { Calendar } diff --git a/package-lock.json b/package-lock.json index 714d2bb889..4861bdfc14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slider": "^1.2.2", - "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", @@ -28,7 +28,7 @@ "cmdk": "^1.0.0", "cron-parser": "^5.0.2", "croner": "^9.0.0", - "date-fns": "^4.1.0", + "date-fns": "^3.6.0", "drizzle-orm": "^0.39.3", "lodash.debounce": "^4.0.8", "lucide-react": "^0.469.0", @@ -37,6 +37,7 @@ "postgres": "^3.4.5", "prismjs": "^1.29.0", "react": "^18.2.0", + "react-day-picker": "^8.10.1", "react-dom": "^18.2.0", "react-simple-code-editor": "^0.14.1", "reactflow": "^11.11.4", @@ -3005,6 +3006,24 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", @@ -3084,6 +3103,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -3177,6 +3214,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -3369,6 +3424,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.5.tgz", @@ -3433,6 +3506,24 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", @@ -3536,6 +3627,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", @@ -3641,6 +3750,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slider": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz", @@ -3675,9 +3802,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" @@ -3785,6 +3912,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -6138,9 +6283,9 @@ } }, "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", "funding": { "type": "github", @@ -9761,6 +9906,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/package.json b/package.json index b2268ed002..742ca7b91d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slider": "^1.2.2", - "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", @@ -36,7 +36,7 @@ "cmdk": "^1.0.0", "cron-parser": "^5.0.2", "croner": "^9.0.0", - "date-fns": "^4.1.0", + "date-fns": "^3.6.0", "drizzle-orm": "^0.39.3", "lodash.debounce": "^4.0.8", "lucide-react": "^0.469.0", @@ -45,6 +45,7 @@ "postgres": "^3.4.5", "prismjs": "^1.29.0", "react": "^18.2.0", + "react-day-picker": "^8.10.1", "react-dom": "^18.2.0", "react-simple-code-editor": "^0.14.1", "reactflow": "^11.11.4",