From ed86dbbc06dcfc3becf8d9f9eeb54cedb17baef8 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Wed, 14 Oct 2026 09:13:58 +0200 Subject: [PATCH 1/3] fix #5247 --- CHANGELOG.md | 2 ++ .../components/inspector/trigger/CronConfigureStep.tsx | 10 +++++++--- .../components/inspector/trigger/CronShowPanel.tsx | 7 +++++-- .../components/inspector/trigger/ReadOnlyField.tsx | 9 +++++++-- .../inspector/trigger/CronShowPanel.test.tsx | 3 ++- .../inspector/trigger/TriggerEditWizard.test.tsx | 1 + 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2baa0fba31..5bb67f05cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to ### Changed +- The cron trigger panels now say that schedule times are in UTC + [#5247](https://github.com/OpenFn/lightning/issues/5247) - Update Project and Workflow yaml exports to match the v4 portability spec [#4718](https://github.com/OpenFn/lightning/issues/4718) - The workflow version hash no longer includes a trigger's `enabled` state diff --git a/assets/js/collaborative-editor/components/inspector/trigger/CronConfigureStep.tsx b/assets/js/collaborative-editor/components/inspector/trigger/CronConfigureStep.tsx index b03d48a9eb0..ce2a30c298e 100644 --- a/assets/js/collaborative-editor/components/inspector/trigger/CronConfigureStep.tsx +++ b/assets/js/collaborative-editor/components/inspector/trigger/CronConfigureStep.tsx @@ -71,9 +71,13 @@ export function CronConfigureStep({ />
-

- When should this run? -

+ {/* Cron schedules always run in UTC, whatever the viewer's zone. */} +
+

+ When should this run? +

+ All times in UTC +
mergeDraft({ cron_expression: expr })} diff --git a/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx b/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx index bf7f717304f..c8caaa37f49 100644 --- a/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx +++ b/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx @@ -18,7 +18,8 @@ interface CronShowPanelProps { * Read-only "show / resting" panel for a configured cron trigger. * * Renders inside {@link InspectorLayout}: the green "Schedule / Cron" badge, a - * **Frequency** label + a read-only box with the humanized schedule, and a + * **Frequency** label (with an "All times in UTC" note, since cron schedules + * always run in UTC) + a read-only box with the humanized schedule, and a * **Cron Input Source** field (the selected step's name, or "Final run state * (default)" when unset). The footer holds a single secondary **Edit** action * (left) that hands off to the edit wizard. @@ -62,7 +63,9 @@ export function CronShowPanel({
- {frequency} + + {frequency} + {inputSource} diff --git a/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx b/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx index 3f3bb8ca03e..e006be7c524 100644 --- a/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx +++ b/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx @@ -3,6 +3,8 @@ import type { ReactNode } from 'react'; interface ReadOnlyFieldProps { /** Field label rendered above the value box. */ label: string; + /** Optional muted note shown at the end of the label row. */ + hint?: ReactNode; /** The read-only value (text) shown in the bordered box. */ children: ReactNode; } @@ -12,10 +14,13 @@ interface ReadOnlyFieldProps { * label above a bordered, muted value box. Keeps the "label + box" markup * identical across the trigger resting panels. */ -export function ReadOnlyField({ label, children }: ReadOnlyFieldProps) { +export function ReadOnlyField({ label, hint, children }: ReadOnlyFieldProps) { return (
- {label} +
+ {label} + {hint && {hint}} +
{ workflowMap.set('deleted_at', null); }); - test('renders the cron badge, Frequency label, and the humanized schedule', async () => { + test('renders the cron badge, Frequency label, UTC note, and the humanized schedule', async () => { const workflowStore = createConnectedWorkflowStore(ydoc); await setup(makeCronTrigger(), workflowStore); expect(screen.getByText('Schedule / Cron')).toBeInTheDocument(); expect(screen.getByText('Frequency')).toBeInTheDocument(); + expect(screen.getByText('All times in UTC')).toBeInTheDocument(); expect(screen.getByText('At 09:00 AM')).toBeInTheDocument(); }); diff --git a/assets/test/collaborative-editor/components/inspector/trigger/TriggerEditWizard.test.tsx b/assets/test/collaborative-editor/components/inspector/trigger/TriggerEditWizard.test.tsx index 7fd9416b5b5..720ad6d284b 100644 --- a/assets/test/collaborative-editor/components/inspector/trigger/TriggerEditWizard.test.tsx +++ b/assets/test/collaborative-editor/components/inspector/trigger/TriggerEditWizard.test.tsx @@ -422,6 +422,7 @@ describe('TriggerEditWizard — cron', () => { // Next → Configure (Configure-only control present). await userEvent.click(screen.getByRole('button', { name: 'Next' })); expect(screen.getByLabelText('Cron Input Source')).toBeInTheDocument(); + expect(screen.getByText('All times in UTC')).toBeInTheDocument(); // Breadcrumb "Choose" crumb returns to Choose. await userEvent.click(screen.getByRole('button', { name: 'Choose' })); From 2dc807cea1297d31f668c2015fbe62a013a04324 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Fri, 23 Oct 2026 08:37:08 +0100 Subject: [PATCH 2/3] Show UTC after the time in cron schedules, on the canvas and in the trigger panels --- CHANGELOG.md | 4 +-- .../inspector/trigger/CronShowPanel.tsx | 7 ++--- .../inspector/trigger/ReadOnlyField.tsx | 9 ++---- .../inspector/trigger/cronSchedule.ts | 17 +++++++--- assets/js/workflow-diagram/nodes/Trigger.tsx | 15 +++------ .../trigger/CronFieldBuilder.test.tsx | 2 +- .../inspector/trigger/CronShowPanel.test.tsx | 5 ++- .../inspector/trigger/cronSchedule.test.ts | 31 +++++++++++++++---- .../workflow-diagram/nodes/Trigger.test.tsx | 27 ++++++++++++++-- 9 files changed, 75 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb67f05cfb..2357d2af945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,8 @@ and this project adheres to ### Changed -- The cron trigger panels now say that schedule times are in UTC - [#5247](https://github.com/OpenFn/lightning/issues/5247) +- Cron schedules now show "UTC" after the time, on the canvas and in the trigger + panels [#5247](https://github.com/OpenFn/lightning/issues/5247) - Update Project and Workflow yaml exports to match the v4 portability spec [#4718](https://github.com/OpenFn/lightning/issues/4718) - The workflow version hash no longer includes a trigger's `enabled` state diff --git a/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx b/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx index c8caaa37f49..bf7f717304f 100644 --- a/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx +++ b/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx @@ -18,8 +18,7 @@ interface CronShowPanelProps { * Read-only "show / resting" panel for a configured cron trigger. * * Renders inside {@link InspectorLayout}: the green "Schedule / Cron" badge, a - * **Frequency** label (with an "All times in UTC" note, since cron schedules - * always run in UTC) + a read-only box with the humanized schedule, and a + * **Frequency** label + a read-only box with the humanized schedule, and a * **Cron Input Source** field (the selected step's name, or "Final run state * (default)" when unset). The footer holds a single secondary **Edit** action * (left) that hands off to the edit wizard. @@ -63,9 +62,7 @@ export function CronShowPanel({
- - {frequency} - + {frequency} {inputSource}
diff --git a/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx b/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx index e006be7c524..3f3bb8ca03e 100644 --- a/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx +++ b/assets/js/collaborative-editor/components/inspector/trigger/ReadOnlyField.tsx @@ -3,8 +3,6 @@ import type { ReactNode } from 'react'; interface ReadOnlyFieldProps { /** Field label rendered above the value box. */ label: string; - /** Optional muted note shown at the end of the label row. */ - hint?: ReactNode; /** The read-only value (text) shown in the bordered box. */ children: ReactNode; } @@ -14,13 +12,10 @@ interface ReadOnlyFieldProps { * label above a bordered, muted value box. Keeps the "label + box" markup * identical across the trigger resting panels. */ -export function ReadOnlyField({ label, hint, children }: ReadOnlyFieldProps) { +export function ReadOnlyField({ label, children }: ReadOnlyFieldProps) { return (
-
- {label} - {hint && {hint}} -
+ {label}
, secondaryIcon: null, }; diff --git a/assets/test/collaborative-editor/components/inspector/trigger/CronFieldBuilder.test.tsx b/assets/test/collaborative-editor/components/inspector/trigger/CronFieldBuilder.test.tsx index 092da532dea..2dc9a313ab7 100644 --- a/assets/test/collaborative-editor/components/inspector/trigger/CronFieldBuilder.test.tsx +++ b/assets/test/collaborative-editor/components/inspector/trigger/CronFieldBuilder.test.tsx @@ -485,7 +485,7 @@ describe('CronFieldBuilder - Advanced Section', () => { // cronstrue humanizes "0 9 * * 1-5". expect( - screen.getByText('At 09:00 AM, Monday through Friday') + screen.getByText('At 09:00 AM UTC, Monday through Friday') ).toBeInTheDocument(); }); diff --git a/assets/test/collaborative-editor/components/inspector/trigger/CronShowPanel.test.tsx b/assets/test/collaborative-editor/components/inspector/trigger/CronShowPanel.test.tsx index 49133d1fb4f..a5ca5f7968b 100644 --- a/assets/test/collaborative-editor/components/inspector/trigger/CronShowPanel.test.tsx +++ b/assets/test/collaborative-editor/components/inspector/trigger/CronShowPanel.test.tsx @@ -105,14 +105,13 @@ describe('CronShowPanel', () => { workflowMap.set('deleted_at', null); }); - test('renders the cron badge, Frequency label, UTC note, and the humanized schedule', async () => { + test('renders the cron badge, Frequency label, and the humanized schedule', async () => { const workflowStore = createConnectedWorkflowStore(ydoc); await setup(makeCronTrigger(), workflowStore); expect(screen.getByText('Schedule / Cron')).toBeInTheDocument(); expect(screen.getByText('Frequency')).toBeInTheDocument(); - expect(screen.getByText('All times in UTC')).toBeInTheDocument(); - expect(screen.getByText('At 09:00 AM')).toBeInTheDocument(); + expect(screen.getByText('At 09:00 AM UTC')).toBeInTheDocument(); }); describe('edit button gating', () => { diff --git a/assets/test/collaborative-editor/components/inspector/trigger/cronSchedule.test.ts b/assets/test/collaborative-editor/components/inspector/trigger/cronSchedule.test.ts index 07b2c8a280b..dd5ee58c6a1 100644 --- a/assets/test/collaborative-editor/components/inspector/trigger/cronSchedule.test.ts +++ b/assets/test/collaborative-editor/components/inspector/trigger/cronSchedule.test.ts @@ -2,8 +2,9 @@ * cronSchedule utility tests * * Covers `humanizeCron` — the cron → friendly text helper used by the cron - * show panel. (Schedule authoring is handled by the CronFieldBuilder dropdown, - * which does its own parsing, so there is no natural-language parser to test.) + * show panel, the builder preview and the diagram node. (Schedule authoring is + * handled by the CronFieldBuilder dropdown, which does its own parsing, so + * there is no natural-language parser to test.) */ import { describe, expect, test } from 'vitest'; @@ -12,15 +13,33 @@ import { humanizeCron } from '../../../../../js/collaborative-editor/components/ describe('humanizeCron', () => { test.each<[string, string, string | null]>([ - ['daily at 9 AM', '0 9 * * *', 'At 09:00 AM'], + ['daily at 9 AM', '0 9 * * *', 'At 09:00 AM UTC'], ['every 15 minutes', '*/15 * * * *', 'Every 15 minutes'], - ['weekdays at 9 AM', '0 9 * * 1-5', 'At 09:00 AM, Monday through Friday'], + [ + 'every 2 hours', + '0 */2 * * *', + 'At 0 minutes past the hour, every 2 hours', + ], + [ + 'weekdays at 9 AM', + '0 9 * * 1-5', + 'At 09:00 AM UTC, Monday through Friday', + ], [ 'monthly on the 15th', '30 9 15 * *', - 'At 09:30 AM, on day 15 of the month', + 'At 09:30 AM UTC, on day 15 of the month', + ], + [ + 'weekly on Sunday midnight', + '0 0 * * 0', + 'At 12:00 AM UTC, only on Sunday', + ], + [ + 'hour range', + '0 9-17 * * 1-5', + 'Every hour, between 09:00 AM and 05:00 PM UTC, Monday through Friday', ], - ['weekly on Sunday midnight', '0 0 * * 0', 'At 12:00 AM, only on Sunday'], ['empty string → null', '', null], ['invalid expression → null', 'not a cron', null], ])('%s', (_label, expression, expected) => { diff --git a/assets/test/workflow-diagram/nodes/Trigger.test.tsx b/assets/test/workflow-diagram/nodes/Trigger.test.tsx index 19bc2d16be3..675c1bcea13 100644 --- a/assets/test/workflow-diagram/nodes/Trigger.test.tsx +++ b/assets/test/workflow-diagram/nodes/Trigger.test.tsx @@ -2,7 +2,8 @@ * Trigger Node Component Tests * * Tests that trigger nodes display correct icons for all trigger types - * (webhook, cron) and handle edge cases gracefully. + * (webhook, cron) and handle edge cases gracefully, and that a cron node's + * sublabel shows the humanized schedule. */ import { render } from '@testing-library/react'; @@ -15,11 +16,12 @@ import type { Lightning } from '../../../js/workflow-diagram/types'; /** * Helper to render a trigger node within ReactFlow context * - * Note: TriggerNode expects data.trigger to contain the trigger info, - * which is how from-workflow structures it + * Mirrors from-workflow: data holds the trigger's own fields (which + * TriggerNode reads) plus a data.trigger summary */ function renderTriggerNode(trigger: Lightning.TriggerNode) { const data: any = { + ...trigger, trigger: { type: trigger.type, enabled: trigger.enabled, @@ -127,3 +129,22 @@ describe('Trigger Node - Icon Display', () => { expect(svgElements.length).toBeGreaterThanOrEqual(1); }); }); + +describe('Trigger Node - Cron Sublabel', () => { + test.each([ + ['0 9 * * 1-5', 'At 09:00 AM UTC, Monday through Friday'], + ['invalid cron', 'Invalid cron expression'], + ['', 'Not configured'], + ])('%j shows "%s"', (cron_expression, expected) => { + const { getByText } = renderTriggerNode({ + id: 'trigger-5', + name: 'Cron Trigger', + type: 'cron', + enabled: true, + cron_expression, + workflow_id: 'test-workflow', + }); + + expect(getByText(expected)).toBeInTheDocument(); + }); +}); From 2d145fc99569aa23c11c3b98484aa830747022d3 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Fri, 23 Oct 2026 08:42:08 +0100 Subject: [PATCH 3/3] Move humanizeCron to utils --- .../components/inspector/trigger/CronFieldBuilder.tsx | 4 ++-- .../components/inspector/trigger/CronShowPanel.tsx | 3 ++- .../trigger/cronSchedule.ts => utils/humanizeCron.ts} | 0 assets/js/workflow-diagram/nodes/Trigger.tsx | 2 +- .../cronSchedule.test.ts => utils/humanizeCron.test.ts} | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) rename assets/js/{collaborative-editor/components/inspector/trigger/cronSchedule.ts => utils/humanizeCron.ts} (100%) rename assets/test/{collaborative-editor/components/inspector/trigger/cronSchedule.test.ts => utils/humanizeCron.test.ts} (90%) diff --git a/assets/js/collaborative-editor/components/inspector/trigger/CronFieldBuilder.tsx b/assets/js/collaborative-editor/components/inspector/trigger/CronFieldBuilder.tsx index bcf8d41a419..7b3f006e6b8 100644 --- a/assets/js/collaborative-editor/components/inspector/trigger/CronFieldBuilder.tsx +++ b/assets/js/collaborative-editor/components/inspector/trigger/CronFieldBuilder.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useMemo } from 'react'; -import { cn } from '../../../../utils/cn'; +import { humanizeCron } from '#/utils/humanizeCron'; -import { humanizeCron } from './cronSchedule'; +import { cn } from '../../../../utils/cn'; interface CronFieldBuilderProps { value: string; diff --git a/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx b/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx index bf7f717304f..5f62673c051 100644 --- a/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx +++ b/assets/js/collaborative-editor/components/inspector/trigger/CronShowPanel.tsx @@ -1,8 +1,9 @@ +import { humanizeCron } from '#/utils/humanizeCron'; + import { useWorkflowState } from '../../../hooks/useWorkflow'; import type { Workflow } from '../../../types/workflow'; import { InspectorLayout } from '../InspectorLayout'; -import { humanizeCron } from './cronSchedule'; import { EditFooter } from './EditFooter'; import { ReadOnlyField } from './ReadOnlyField'; import { TriggerTypeBadge } from './TriggerTypeBadge'; diff --git a/assets/js/collaborative-editor/components/inspector/trigger/cronSchedule.ts b/assets/js/utils/humanizeCron.ts similarity index 100% rename from assets/js/collaborative-editor/components/inspector/trigger/cronSchedule.ts rename to assets/js/utils/humanizeCron.ts diff --git a/assets/js/workflow-diagram/nodes/Trigger.tsx b/assets/js/workflow-diagram/nodes/Trigger.tsx index 451672584a5..f5c48a49a7e 100644 --- a/assets/js/workflow-diagram/nodes/Trigger.tsx +++ b/assets/js/workflow-diagram/nodes/Trigger.tsx @@ -2,7 +2,7 @@ import { ClockIcon, GlobeAltIcon } from '@heroicons/react/24/outline'; import { Position } from '@xyflow/react'; import { memo } from 'react'; -import { humanizeCron } from '#/collaborative-editor/components/inspector/trigger/cronSchedule'; +import { humanizeCron } from '#/utils/humanizeCron'; import { lockClosedIcon } from '../components/trigger-icons'; import type { Lightning } from '../types'; diff --git a/assets/test/collaborative-editor/components/inspector/trigger/cronSchedule.test.ts b/assets/test/utils/humanizeCron.test.ts similarity index 90% rename from assets/test/collaborative-editor/components/inspector/trigger/cronSchedule.test.ts rename to assets/test/utils/humanizeCron.test.ts index dd5ee58c6a1..8bb415400cb 100644 --- a/assets/test/collaborative-editor/components/inspector/trigger/cronSchedule.test.ts +++ b/assets/test/utils/humanizeCron.test.ts @@ -1,5 +1,5 @@ /** - * cronSchedule utility tests + * humanizeCron tests * * Covers `humanizeCron` — the cron → friendly text helper used by the cron * show panel, the builder preview and the diagram node. (Schedule authoring is @@ -9,7 +9,7 @@ import { describe, expect, test } from 'vitest'; -import { humanizeCron } from '../../../../../js/collaborative-editor/components/inspector/trigger/cronSchedule'; +import { humanizeCron } from '../../js/utils/humanizeCron'; describe('humanizeCron', () => { test.each<[string, string, string | null]>([