1- import { useEffect , useState } from 'react'
1+ import { useCallback , useEffect , useState } from 'react'
22import { Calendar , ExternalLink } from 'lucide-react'
33import { useParams } from 'next/navigation'
44import { Button } from '@/components/ui/button'
@@ -33,17 +33,23 @@ export function ScheduleConfig({
3333 disabled = false ,
3434} : ScheduleConfigProps ) {
3535 const [ error , setError ] = useState < string | null > ( null )
36- const [ scheduleId , setScheduleId ] = useState < string | null > ( null )
37- const [ nextRunAt , setNextRunAt ] = useState < string | null > ( null )
38- const [ lastRanAt , setLastRanAt ] = useState < string | null > ( null )
39- const [ cronExpression , setCronExpression ] = useState < string | null > ( null )
40- const [ timezone , setTimezone ] = useState < string > ( 'UTC' )
36+ const [ scheduleData , setScheduleData ] = useState < {
37+ id : string | null
38+ nextRunAt : string | null
39+ lastRanAt : string | null
40+ cronExpression : string | null
41+ timezone : string
42+ } > ( {
43+ id : null ,
44+ nextRunAt : null ,
45+ lastRanAt : null ,
46+ cronExpression : null ,
47+ timezone : 'UTC' ,
48+ } )
4149 const [ isLoading , setIsLoading ] = useState ( false )
4250 const [ isSaving , setIsSaving ] = useState ( false )
4351 const [ isDeleting , setIsDeleting ] = useState ( false )
4452 const [ isModalOpen , setIsModalOpen ] = useState ( false )
45- // Track when we need to force a refresh of schedule data
46- const [ refreshCounter , setRefreshCounter ] = useState ( 0 )
4753
4854 const params = useParams ( )
4955 const workflowId = params . workflowId as string
@@ -61,79 +67,88 @@ export function ScheduleConfig({
6167 const blockWithValues = getBlockWithValues ( blockId )
6268 const isScheduleTriggerBlock = blockWithValues ?. type === 'schedule'
6369
64- // Function to check if schedule exists in the database
65- const checkSchedule = async ( ) => {
70+ // Fetch schedule data from API
71+ const fetchSchedule = useCallback ( async ( ) => {
72+ if ( ! workflowId ) return
73+
6674 setIsLoading ( true )
6775 try {
68- // Check if there's a schedule for this workflow, passing the mode parameter
69- // For schedule trigger blocks, include blockId to get the specific schedule
70- const url = new URL ( '/api/schedules' , window . location . origin )
71- url . searchParams . set ( 'workflowId' , workflowId )
72- url . searchParams . set ( 'mode' , 'schedule' )
73-
76+ const params = new URLSearchParams ( {
77+ workflowId,
78+ mode : 'schedule' ,
79+ } )
7480 if ( isScheduleTriggerBlock ) {
75- url . searchParams . set ( 'blockId' , blockId )
81+ params . set ( 'blockId' , blockId )
7682 }
7783
78- const response = await fetch ( url . toString ( ) , {
79- // Add cache: 'no-store' to prevent caching of this request
84+ const response = await fetch ( `/api/schedules?${ params } ` , {
8085 cache : 'no-store' ,
81- headers : {
82- 'Cache-Control' : 'no-cache' ,
83- } ,
86+ headers : { 'Cache-Control' : 'no-cache' } ,
8487 } )
8588
8689 if ( response . ok ) {
8790 const data = await response . json ( )
88- logger . debug ( 'Schedule check response:' , data )
89-
9091 if ( data . schedule ) {
91- setScheduleId ( data . schedule . id )
92- setNextRunAt ( data . schedule . nextRunAt )
93- setLastRanAt ( data . schedule . lastRanAt )
94- setCronExpression ( data . schedule . cronExpression )
95- setTimezone ( data . schedule . timezone || 'UTC' )
96-
97- // Note: We no longer set global schedule status from individual components
98- // The global schedule status should be managed by a higher-level component
92+ setScheduleData ( {
93+ id : data . schedule . id ,
94+ nextRunAt : data . schedule . nextRunAt ,
95+ lastRanAt : data . schedule . lastRanAt ,
96+ cronExpression : data . schedule . cronExpression ,
97+ timezone : data . schedule . timezone || 'UTC' ,
98+ } )
9999 } else {
100- setScheduleId ( null )
101- setNextRunAt ( null )
102- setLastRanAt ( null )
103- setCronExpression ( null )
104-
105- // Note: We no longer set global schedule status from individual components
100+ setScheduleData ( {
101+ id : null ,
102+ nextRunAt : null ,
103+ lastRanAt : null ,
104+ cronExpression : null ,
105+ timezone : 'UTC' ,
106+ } )
106107 }
107108 }
108109 } catch ( error ) {
109- logger . error ( 'Error checking schedule:' , { error } )
110- setError ( 'Failed to check schedule status' )
110+ logger . error ( 'Error fetching schedule:' , error )
111111 } finally {
112112 setIsLoading ( false )
113113 }
114- }
114+ } , [ workflowId , blockId , isScheduleTriggerBlock ] )
115+
116+ // Fetch schedule data on mount and when dependencies change
117+ useEffect ( ( ) => {
118+ fetchSchedule ( )
119+ } , [ fetchSchedule ] )
115120
116- // Check for schedule on mount and when relevant dependencies change
121+ // Separate effect for event listener to avoid removing/re-adding on every dependency change
117122 useEffect ( ( ) => {
118- // Check for schedules when workflowId changes, modal opens, or on initial mount
119- if ( workflowId ) {
120- checkSchedule ( )
123+ const handleScheduleUpdate = ( event : CustomEvent ) => {
124+ if ( event . detail ?. workflowId === workflowId && event . detail ?. blockId === blockId ) {
125+ logger . debug ( 'Schedule update event received in schedule-config, refetching' )
126+ fetchSchedule ( )
127+ }
121128 }
122129
123- // Cleanup function to reset loading state
130+ window . addEventListener ( 'schedule-updated' , handleScheduleUpdate as EventListener )
131+
124132 return ( ) => {
125- setIsLoading ( false )
133+ window . removeEventListener ( 'schedule-updated' , handleScheduleUpdate as EventListener )
126134 }
127- } , [ workflowId , isModalOpen , refreshCounter ] )
135+ } , [ workflowId , blockId , fetchSchedule ] )
136+
137+ // Refetch when modal opens to get latest data
138+ useEffect ( ( ) => {
139+ if ( isModalOpen ) {
140+ fetchSchedule ( )
141+ }
142+ } , [ isModalOpen , fetchSchedule ] )
128143
129144 // Format the schedule information for display
130145 const getScheduleInfo = ( ) => {
131- if ( ! scheduleId || ! nextRunAt ) return null
146+ if ( ! scheduleData . id || ! scheduleData . nextRunAt ) return null
132147
133148 let scheduleTiming = 'Unknown schedule'
134149
135- if ( cronExpression ) {
136- scheduleTiming = parseCronToHumanReadable ( cronExpression )
150+ if ( scheduleData . cronExpression ) {
151+ scheduleTiming = parseCronToHumanReadable ( scheduleData . cronExpression , scheduleData . timezone )
137152 } else if ( scheduleType ) {
138153 scheduleTiming = `${ scheduleType . charAt ( 0 ) . toUpperCase ( ) + scheduleType . slice ( 1 ) } `
139154 }
@@ -142,8 +157,14 @@ export function ScheduleConfig({
142157 < >
143158 < div className = 'truncate font-normal text-sm' > { scheduleTiming } </ div >
144159 < div className = 'text-muted-foreground text-xs' >
145- < div > Next run: { formatDateTime ( new Date ( nextRunAt ) , timezone ) } </ div >
146- { lastRanAt && < div > Last run: { formatDateTime ( new Date ( lastRanAt ) , timezone ) } </ div > }
160+ < div >
161+ Next run: { formatDateTime ( new Date ( scheduleData . nextRunAt ) , scheduleData . timezone ) }
162+ </ div >
163+ { scheduleData . lastRanAt && (
164+ < div >
165+ Last run: { formatDateTime ( new Date ( scheduleData . lastRanAt ) , scheduleData . timezone ) }
166+ </ div >
167+ ) }
147168 </ div >
148169 </ >
149170 )
@@ -154,16 +175,11 @@ export function ScheduleConfig({
154175 setIsModalOpen ( true )
155176 }
156177
157- const handleCloseModal = ( ) => {
178+ const handleCloseModal = useCallback ( ( ) => {
158179 setIsModalOpen ( false )
159- // Force a refresh when closing the modal
160- // Use a small timeout to ensure backend updates are complete
161- setTimeout ( ( ) => {
162- setRefreshCounter ( ( prev ) => prev + 1 )
163- } , 500 )
164- }
180+ } , [ ] )
165181
166- const handleSaveSchedule = async ( ) : Promise < boolean > => {
182+ const handleSaveSchedule = useCallback ( async ( ) : Promise < boolean > => {
167183 if ( isPreview || disabled ) return false
168184
169185 setIsSaving ( true )
@@ -246,17 +262,24 @@ export function ScheduleConfig({
246262 logger . debug ( 'Schedule save response:' , responseData )
247263
248264 // 5. Update our local state with the response data
249- if ( responseData . cronExpression ) {
250- setCronExpression ( responseData . cronExpression )
265+ if ( responseData . cronExpression || responseData . nextRunAt ) {
266+ setScheduleData ( ( prev ) => ( {
267+ ...prev ,
268+ cronExpression : responseData . cronExpression || prev . cronExpression ,
269+ nextRunAt :
270+ typeof responseData . nextRunAt === 'string'
271+ ? responseData . nextRunAt
272+ : responseData . nextRunAt ?. toISOString ?.( ) || prev . nextRunAt ,
273+ } ) )
251274 }
252275
253- if ( responseData . nextRunAt ) {
254- setNextRunAt (
255- typeof responseData . nextRunAt === 'string'
256- ? responseData . nextRunAt
257- : responseData . nextRunAt . toISOString ?. ( ) || responseData . nextRunAt
258- )
259- }
276+ // 6. Dispatch custom event to notify parent workflow-block component to refetch schedule info
277+ // This ensures the badge updates immediately after saving
278+ const event = new CustomEvent ( 'schedule-updated' , {
279+ detail : { workflowId , blockId } ,
280+ } )
281+ window . dispatchEvent ( event )
282+ logger . debug ( 'Dispatched schedule-updated event' , { workflowId , blockId } )
260283
261284 // 6. Update the schedule status and trigger a workflow update
262285 // Note: Global schedule status is managed at a higher level
@@ -266,15 +289,8 @@ export function ScheduleConfig({
266289 workflowStore . updateLastSaved ( )
267290 workflowStore . triggerUpdate ( )
268291
269- // 8. Force a refresh to update the UI
270- // Use a timeout to ensure the API changes are completed
271- setTimeout ( ( ) => {
272- logger . debug ( 'Refreshing schedule information after save' )
273- setRefreshCounter ( ( prev ) => prev + 1 )
274-
275- // Make a separate API call to ensure we get the latest schedule info
276- checkSchedule ( )
277- } , 500 )
292+ // 8. Refetch the schedule to update local state
293+ await fetchSchedule ( )
278294
279295 return true
280296 } catch ( error ) {
@@ -284,10 +300,10 @@ export function ScheduleConfig({
284300 } finally {
285301 setIsSaving ( false )
286302 }
287- }
303+ } , [ workflowId , blockId , isScheduleTriggerBlock , setStartWorkflow , fetchSchedule ] )
288304
289- const handleDeleteSchedule = async ( ) : Promise < boolean > => {
290- if ( isPreview || ! scheduleId || disabled ) return false
305+ const handleDeleteSchedule = useCallback ( async ( ) : Promise < boolean > => {
306+ if ( isPreview || ! scheduleData . id || disabled ) return false
291307
292308 setIsDeleting ( true )
293309 try {
@@ -315,7 +331,7 @@ export function ScheduleConfig({
315331 }
316332
317333 // 4. Make the DELETE API call to remove the schedule
318- const response = await fetch ( `/api/schedules/${ scheduleId } ` , {
334+ const response = await fetch ( `/api/schedules/${ scheduleData . id } ` , {
319335 method : 'DELETE' ,
320336 } )
321337
@@ -326,14 +342,23 @@ export function ScheduleConfig({
326342 }
327343
328344 // 5. Clear schedule state
329- setScheduleId ( null )
330- setNextRunAt ( null )
331- setLastRanAt ( null )
332- setCronExpression ( null )
345+ setScheduleData ( {
346+ id : null ,
347+ nextRunAt : null ,
348+ lastRanAt : null ,
349+ cronExpression : null ,
350+ timezone : 'UTC' ,
351+ } )
333352
334353 // 6. Update schedule status and refresh UI
335354 // Note: Global schedule status is managed at a higher level
336- setRefreshCounter ( ( prev ) => prev + 1 )
355+
356+ // 7. Dispatch custom event to notify parent workflow-block component
357+ const event = new CustomEvent ( 'schedule-updated' , {
358+ detail : { workflowId, blockId } ,
359+ } )
360+ window . dispatchEvent ( event )
361+ logger . debug ( 'Dispatched schedule-updated event after delete' , { workflowId, blockId } )
337362
338363 return true
339364 } catch ( error ) {
@@ -343,10 +368,18 @@ export function ScheduleConfig({
343368 } finally {
344369 setIsDeleting ( false )
345370 }
346- }
371+ } , [
372+ scheduleData . id ,
373+ isPreview ,
374+ disabled ,
375+ isScheduleTriggerBlock ,
376+ setStartWorkflow ,
377+ workflowId ,
378+ blockId ,
379+ ] )
347380
348381 // Check if the schedule is active
349- const isScheduleActive = ! ! scheduleId && ! ! nextRunAt
382+ const isScheduleActive = ! ! scheduleData . id && ! ! scheduleData . nextRunAt
350383
351384 return (
352385 < div className = 'w-full' onClick = { ( e ) => e . stopPropagation ( ) } >
@@ -399,7 +432,7 @@ export function ScheduleConfig({
399432 blockId = { blockId }
400433 onSave = { handleSaveSchedule }
401434 onDelete = { handleDeleteSchedule }
402- scheduleId = { scheduleId }
435+ scheduleId = { scheduleData . id }
403436 />
404437 </ Dialog >
405438 </ div >
0 commit comments