import { Formik, Form } from 'formik' import { useEffect, useMemo, useCallback } from 'react' import classNames from 'classnames' import { useFormikContext } from 'formik' import { Link } from 'react-router-dom' import { DateTime } from 'luxon' import IconClose from '~/icons/compiled/Close' import IVButton from '~/components/IVButton' import IVInputField from '~/components/IVInputField' import IVSelect from '~/components/IVSelect' import { trpc, client, inferMutationInput, inferQueryOutput, } from '~/utils/trpc' import AsyncIVSelect from '~/components/IVSelect/async' import { SchedulePeriod, toScheduleInput } from '~/utils/actionSchedule' import relativeTime, { DAY_NAMES, DAYS_OF_MONTH, displayStringToTime, timeToDisplayString, numberWithOrdinal, } from '~/utils/date' import { TIMEZONE_OPTIONS } from '~/utils/timezones' import IVAlert from '~/components/IVAlert' import IVCheckbox from '~/components/IVCheckbox' import IconInfo from '~/icons/compiled/Info' import { displayName } from '~/utils/user' import { isBackgroundable } from '~/utils/actions' import { useOrgParams } from '~/utils/organization' const times = [ '12:00 AM', '12:30 AM', '1:00 AM', '1:30 AM', '2:00 AM', '2:30 AM', '3:00 AM', '3:30 AM', '4:00 AM', '4:30 AM', '5:00 AM', '5:30 AM', '6:00 AM', '6:30 AM', '7:00 AM', '7:30 AM', '8:00 AM', '8:30 AM', '9:00 AM', '9:30 AM', '10:00 AM', '10:30 AM', '11:00 AM', '11:30 AM', '12:00 PM', '12:30 PM', '1:00 PM', '1:30 PM', '2:00 PM', '2:30 PM', '3:00 PM', '3:30 PM', '4:00 PM', '4:30 PM', '5:00 PM', '5:30 PM', '6:00 PM', '6:30 PM', '7:00 PM', '7:30 PM', '8:00 PM', '8:30 PM', '9:00 PM', '9:30 PM', '10:00 PM', '10:30 PM', '11:00 PM', '11:30 PM', ] function usersToSelect(users: inferQueryOutput<'organization.users'>) { return ( users?.map(({ user }) => { const name = [user.firstName, user.lastName].join(' ') return { value: user.id, label: name || user.email, } }) ?? [] ) } function ActionSchedule({ action, }: { action: inferQueryOutput<'action.one'> }) { const { orgEnvSlug } = useOrgParams() const { values, setFieldValue } = useFormikContext['data']>() const actionScheduleInput = values.actionScheduleInputs?.[0] const actionScheduleTransactions = useMemo( () => action.transactions .filter(t => t.actionScheduleId === actionScheduleInput?.id) .sort((a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()), [actionScheduleInput?.id, action.transactions] ) const time = useMemo(() => { const { hours, minutes } = actionScheduleInput ?? {} if (hours === undefined) { return '8:00 AM' } return timeToDisplayString(hours, minutes) }, [actionScheduleInput]) const input = useMemo(() => { return { schedulePeriod: actionScheduleInput?.schedulePeriod ?? 'hour', timeZoneName: actionScheduleInput?.timeZoneName ?? DateTime.now().zoneName, dayOfWeek: actionScheduleInput?.dayOfWeek ?? 0, dayOfMonth: actionScheduleInput?.dayOfMonth ?? 1, hours: actionScheduleInput?.hours ?? 8, minutes: actionScheduleInput?.minutes ?? 0, runnerId: actionScheduleInput?.runnerId ?? undefined, runnerName: actionScheduleInput?.['runnerName'] ?? undefined, notifyOnSuccess: actionScheduleInput?.notifyOnSuccess ?? false, } }, [actionScheduleInput]) const updateInput = useCallback( (updates: Partial = {}) => { setFieldValue('actionScheduleInputs', [{ ...input, ...updates }]) }, [input, setFieldValue] ) // Update form values on refetch useEffect(() => { setFieldValue( 'actionScheduleInputs', action.schedules.map(s => ({ id: s.id, runnerName: s.runner ? displayName(s.runner) : null, ...toScheduleInput(s), })) ) }, [setFieldValue, action.schedules]) const handleUserSearch = useCallback(async (searchQuery: string) => { const accesses = await client.query('organization.users', { searchQuery, limit: 50, }) return usersToSelect(accesses) }, []) return (
{actionScheduleInput ? (
updateInput({ schedulePeriod: e.target.value as SchedulePeriod, }) } value={input.schedulePeriod} options={[ { label: 'Every hour', value: 'hour', }, { label: 'Every day', value: 'day', }, { label: 'Every week', value: 'week', }, { label: 'Every month', value: 'month', }, ]} /> {input.schedulePeriod === 'week' && ( <>
on
({ label: day, value: i.toString(), }))} value={input.dayOfWeek} onChange={event => { updateInput({ dayOfWeek: Number(event.target.value) }) }} /> )} {input.schedulePeriod === 'month' && ( <>
on the
({ label: numberWithOrdinal(day), value: String(day), }))} value={input.dayOfMonth} onChange={event => { updateInput({ dayOfMonth: Number(event.target.value) }) }} /> )} {input.schedulePeriod !== 'hour' && ( <>
at
({ label: time, value: time }))} value={time} onChange={event => { const { hours, minutes } = displayStringToTime( event.target.value ) updateInput({ hours, minutes }) }} /> { updateInput({ timeZoneName: event.target.value }) }} /> )}
{ updateInput({ runnerId }) }} isClearable />

If enabled, will notify the action runner when the action completes successfully.

The runner will always be notified when the action is unsuccessful or if it requires input.

} checked={input.notifyOnSuccess} onChange={e => { updateInput({ notifyOnSuccess: e.target.checked }) }} />
{actionScheduleTransactions.length > 0 && (
Last run: {relativeTime(actionScheduleTransactions[0].createdAt, { fullDateThresholdInHours: 24, })} View history ›
)}
) : (

Add a schedule to run this action automatically.

{ updateInput() }} />
)}
) } export default function ActionScheduleSettings({ action, onSuccess, onError, }: { action: inferQueryOutput<'action.one'> onSuccess: () => void onError: () => void refetch: () => void }) { const updateMeta = trpc.useMutation('action.schedule.update') const actionScheduleInputs = useMemo( () => action.schedules.map(s => ({ id: s.id, runnerName: s.runner ? displayName(s.runner) : undefined, ...toScheduleInput(s), })), [action.schedules] ) if (!isBackgroundable(action)) { return (

Please enable Allow running in background in General settings before adding a schedule.

) } return ( ['data']> initialValues={{ actionScheduleInputs, }} validateOnBlur={false} validateOnChange={false} onSubmit={async data => { if (updateMeta.isLoading) return updateMeta.mutate( { actionId: action.id, data, }, { onSuccess, onError } ) }} > {({ isValid }) => (
)} ) }