Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2025 Sim Studio, Inc.
Copyright 2026 Sim Studio, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Sim Studio
Copyright 2025 Sim Studio
Copyright 2026 Sim Studio

This product includes software developed for the Sim project.
27 changes: 27 additions & 0 deletions apps/sim/app/playground/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
TableHeader,
TableRow,
Textarea,
TimePicker,
Tooltip,
Trash,
Trash2,
Expand Down Expand Up @@ -125,6 +126,7 @@ export default function PlaygroundPage() {
const [switchValue, setSwitchValue] = useState(false)
const [checkboxValue, setCheckboxValue] = useState(false)
const [sliderValue, setSliderValue] = useState([50])
const [timeValue, setTimeValue] = useState('09:30')
const [activeTab, setActiveTab] = useState('profile')
const [isDarkMode, setIsDarkMode] = useState(false)

Expand Down Expand Up @@ -491,6 +493,31 @@ export default function PlaygroundPage() {
</VariantRow>
</Section>

{/* TimePicker */}
<Section title='TimePicker'>
<VariantRow label='default'>
<div className='w-48'>
<TimePicker value={timeValue} onChange={setTimeValue} placeholder='Select time' />
</div>
<span className='text-[var(--text-secondary)] text-sm'>{timeValue}</span>
</VariantRow>
<VariantRow label='size sm'>
<div className='w-48'>
<TimePicker value='14:00' onChange={() => {}} placeholder='Small size' size='sm' />
</div>
</VariantRow>
<VariantRow label='no value'>
<div className='w-48'>
<TimePicker placeholder='Select time...' onChange={() => {}} />
</div>
</VariantRow>
<VariantRow label='disabled'>
<div className='w-48'>
<TimePicker value='09:00' disabled />
</div>
</VariantRow>
</Section>

{/* Breadcrumb */}
<Section title='Breadcrumb'>
<Breadcrumb
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ interface DropdownProps {
) => Promise<Array<{ label: string; id: string }>>
/** Field dependencies that trigger option refetch when changed */
dependsOn?: SubBlockConfig['dependsOn']
/** Enable search input in dropdown */
searchable?: boolean
}

/**
Expand All @@ -70,6 +72,7 @@ export function Dropdown({
multiSelect = false,
fetchOptions,
dependsOn,
searchable = false,
}: DropdownProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string | string[]>(blockId, subBlockId) as [
string | string[] | null | undefined,
Expand Down Expand Up @@ -369,7 +372,7 @@ export function Dropdown({
)
}, [multiSelect, multiValues, optionMap])

const isSearchable = subBlockId === 'operation'
const isSearchable = searchable || (subBlockId === 'operation' && comboboxOptions.length > 5)

return (
<Combobox
Expand All @@ -391,7 +394,7 @@ export function Dropdown({
isLoading={isLoadingOptions}
error={fetchError}
searchable={isSearchable}
searchPlaceholder='Search operations...'
searchPlaceholder='Search...'
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'use client'

import * as React from 'react'
import { Button, Input, Popover, PopoverContent, PopoverTrigger } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { TimePicker } from '@/components/emcn'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'

interface TimeInputProps {
Expand All @@ -15,6 +13,10 @@ interface TimeInputProps {
disabled?: boolean
}

/**
* Time input wrapper for sub-block editor.
* Connects the EMCN TimePicker to the sub-block store.
*/
export function TimeInput({
blockId,
subBlockId,
Expand All @@ -26,143 +28,20 @@ export function TimeInput({
}: TimeInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)

// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
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 = Number.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<string>('12')
const [minute, setMinute] = React.useState<string>('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 handleChange = (newValue: string) => {
if (isPreview || disabled) return
const h = Number.parseInt(newHour ?? hour) || 12
const m = Number.parseInt(newMinute ?? minute) || 0
const p = newAmpm ?? ampm
setStoreValue(formatStorageTime(h, m, p))
}

// Initialize from existing value
React.useEffect(() => {
if (value) {
const [hours, minutes] = value.split(':')
const hour24 = Number.parseInt(hours, 10)
const _minute = Number.parseInt(minutes, 10)
const isAM = hour24 < 12
setHour((hour24 % 12 || 12).toString())
setMinute(minutes)
setAmpm(isAM ? 'AM' : 'PM')
}
}, [value])

const handleBlur = () => {
updateTime()
setIsOpen(false)
setStoreValue(newValue)
}

return (
<Popover
open={isOpen}
onOpenChange={(open) => {
setIsOpen(open)
if (!open) {
handleBlur()
}
}}
>
<PopoverTrigger asChild>
<div className='relative w-full cursor-pointer'>
<Input
readOnly
disabled={isPreview || disabled}
value={value ? formatDisplayTime(value) : ''}
placeholder={placeholder || 'Select time'}
autoComplete='off'
className={cn('cursor-pointer', !value && 'text-muted-foreground', className)}
/>
</div>
</PopoverTrigger>
<PopoverContent className='w-auto p-4'>
<div className='flex items-center space-x-2'>
<Input
className='w-[4rem]'
value={hour}
onChange={(e) => {
const val = e.target.value.replace(/[^0-9]/g, '')
if (val === '') {
setHour('')
return
}
const numVal = Number.parseInt(val)
if (!Number.isNaN(numVal)) {
const newHour = Math.min(12, Math.max(1, numVal)).toString()
setHour(newHour)
updateTime(newHour)
}
}}
onBlur={() => {
const numVal = Number.parseInt(hour) || 12
setHour(numVal.toString())
updateTime(numVal.toString())
}}
type='text'
autoComplete='off'
/>
<span className='text-[var(--text-primary)]'>:</span>
<Input
className='w-[4rem]'
value={minute}
onChange={(e) => {
const val = e.target.value.replace(/[^0-9]/g, '')
if (val === '') {
setMinute('')
return
}
const numVal = Number.parseInt(val)
if (!Number.isNaN(numVal)) {
const newMinute = Math.min(59, Math.max(0, numVal)).toString().padStart(2, '0')
setMinute(newMinute)
updateTime(undefined, newMinute)
}
}}
onBlur={() => {
const numVal = Number.parseInt(minute) || 0
setMinute(numVal.toString().padStart(2, '0'))
updateTime(undefined, numVal.toString())
}}
type='text'
autoComplete='off'
/>
<Button
variant='outline'
className='w-[4rem]'
onClick={() => {
const newAmpm = ampm === 'AM' ? 'PM' : 'AM'
setAmpm(newAmpm)
updateTime(undefined, undefined, newAmpm)
}}
>
{ampm}
</Button>
</div>
</PopoverContent>
</Popover>
<TimePicker
value={value || undefined}
onChange={handleChange}
placeholder={placeholder || 'Select time'}
disabled={isPreview || disabled}
className={className}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ function SubBlockComponent({
multiSelect={config.multiSelect}
fetchOptions={config.fetchOptions}
dependsOn={config.dependsOn}
searchable={config.searchable}
/>
</div>
)
Expand Down
3 changes: 1 addition & 2 deletions apps/sim/background/schedule-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,7 @@ function calculateNextRunTime(
return nextDate
}

const lastRanAt = schedule.lastRanAt ? new Date(schedule.lastRanAt) : null
return calculateNextTime(scheduleType, scheduleValues, lastRanAt)
return calculateNextTime(scheduleType, scheduleValues)
}

export async function executeScheduleJob(payload: ScheduleExecutionPayload) {
Expand Down
30 changes: 27 additions & 3 deletions apps/sim/blocks/blocks/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,24 +128,48 @@ export const ScheduleBlock: BlockConfig = {
id: 'timezone',
type: 'dropdown',
title: 'Timezone',
searchable: true,
options: [
// UTC
{ label: 'UTC', id: 'UTC' },
{ label: 'US Eastern (UTC-5)', id: 'America/New_York' },
{ label: 'US Central (UTC-6)', id: 'America/Chicago' },
{ label: 'US Mountain (UTC-7)', id: 'America/Denver' },
// Americas
{ label: 'US Pacific (UTC-8)', id: 'America/Los_Angeles' },
{ label: 'US Mountain (UTC-7)', id: 'America/Denver' },
{ label: 'US Central (UTC-6)', id: 'America/Chicago' },
{ label: 'US Eastern (UTC-5)', id: 'America/New_York' },
{ label: 'US Alaska (UTC-9)', id: 'America/Anchorage' },
{ label: 'US Hawaii (UTC-10)', id: 'Pacific/Honolulu' },
{ label: 'Canada Toronto (UTC-5)', id: 'America/Toronto' },
{ label: 'Canada Vancouver (UTC-8)', id: 'America/Vancouver' },
{ label: 'Mexico City (UTC-6)', id: 'America/Mexico_City' },
{ label: 'São Paulo (UTC-3)', id: 'America/Sao_Paulo' },
{ label: 'Buenos Aires (UTC-3)', id: 'America/Argentina/Buenos_Aires' },
// Europe
{ label: 'London (UTC+0)', id: 'Europe/London' },
{ label: 'Paris (UTC+1)', id: 'Europe/Paris' },
{ label: 'Berlin (UTC+1)', id: 'Europe/Berlin' },
{ label: 'Amsterdam (UTC+1)', id: 'Europe/Amsterdam' },
{ label: 'Madrid (UTC+1)', id: 'Europe/Madrid' },
{ label: 'Rome (UTC+1)', id: 'Europe/Rome' },
{ label: 'Moscow (UTC+3)', id: 'Europe/Moscow' },
// Middle East / Africa
{ label: 'Dubai (UTC+4)', id: 'Asia/Dubai' },
{ label: 'Tel Aviv (UTC+2)', id: 'Asia/Tel_Aviv' },
{ label: 'Cairo (UTC+2)', id: 'Africa/Cairo' },
{ label: 'Johannesburg (UTC+2)', id: 'Africa/Johannesburg' },
// Asia
{ label: 'India (UTC+5:30)', id: 'Asia/Kolkata' },
{ label: 'Bangkok (UTC+7)', id: 'Asia/Bangkok' },
{ label: 'Jakarta (UTC+7)', id: 'Asia/Jakarta' },
{ label: 'Singapore (UTC+8)', id: 'Asia/Singapore' },
{ label: 'China (UTC+8)', id: 'Asia/Shanghai' },
{ label: 'Hong Kong (UTC+8)', id: 'Asia/Hong_Kong' },
{ label: 'Seoul (UTC+9)', id: 'Asia/Seoul' },
{ label: 'Tokyo (UTC+9)', id: 'Asia/Tokyo' },
// Australia / Pacific
{ label: 'Perth (UTC+8)', id: 'Australia/Perth' },
{ label: 'Sydney (UTC+10)', id: 'Australia/Sydney' },
{ label: 'Melbourne (UTC+10)', id: 'Australia/Melbourne' },
{ label: 'Auckland (UTC+12)', id: 'Pacific/Auckland' },
],
value: () => 'UTC',
Expand Down
1 change: 1 addition & 0 deletions apps/sim/components/emcn/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ export {
TableRow,
} from './table/table'
export { Textarea } from './textarea/textarea'
export { TimePicker, type TimePickerProps, timePickerVariants } from './time-picker/time-picker'
export { Tooltip } from './tooltip/tooltip'
Loading