From c57052a1dabe4e2f4077dab5e5df025c3655b239 Mon Sep 17 00:00:00 2001 From: Stephen Wright Date: Tue, 9 Dec 2025 10:29:11 +0000 Subject: [PATCH] feat: Enable time saved node for testing (#22650) --- .../frontend/@n8n/i18n/src/locales/en.json | 2 +- .../WorkflowProductionChecklist.vue | 7 -- .../app/components/WorkflowSettings.test.ts | 24 ++++-- .../src/app/components/WorkflowSettings.vue | 76 +++++-------------- .../src/app/constants/experiments.ts | 5 -- .../nodes/TimeSaved/TimeSaved.node.ts | 7 +- packages/nodes-base/package.json | 1 + 7 files changed, 43 insertions(+), 79 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 7ac659907ea..6fbe5872434 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -3180,7 +3180,7 @@ "workflowSettings.timeSavedPerExecution.nodesDetected": "Active - {count} time saved nodes currently setup", "workflowSettings.timeSavedPerExecution.nodesDetected.hint": "Time saved is calculated dynamically based on each execution", "workflowSettings.timeSavedPerExecution.nodesDetected.addMore": "Add more time saved nodes", - "workflowSettings.timeSavedPerExecution.fixedTabWarning": "There are one or more {link} calculating time saved on this workflows dynamically. While your workflow is configured for fix time saved values, these nodes will be ignored.", + "workflowSettings.timeSavedPerExecution.fixedTabWarning": "Time saved for this workflow is tracked using a fixed value. Any Time Saved nodes in this workflow are ignored with this setting.", "workflowSettings.timeSavedPerExecution.fixedTabWarning.link": "time saved nodes", "workflowSettings.availableInMCP": "Available in MCP", "workflowSettings.availableInMCP.tooltip": "Make this workflow visible to AI Agents through n8n MCP", diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue b/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue index 2438f4c901a..06eab37c0f6 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowProductionChecklist.vue @@ -16,7 +16,6 @@ import { EVALUATIONS_DOCS_URL, ERROR_WORKFLOW_DOCS_URL, TIME_SAVED_DOCS_URL, - TIME_SAVED_NODE_EXPERIMENT, TIME_SAVED_NODE_TYPE, } from '@/app/constants'; import { useMessage } from '@/app/composables/useMessage'; @@ -28,7 +27,6 @@ import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp'; import { N8nSuggestedActions } from '@n8n/design-system'; import { useSettingsStore } from '@/app/stores/settings.store'; import { useUsersStore } from '@/features/settings/users/users.store'; -import { usePostHog } from '@/app/stores/posthog.store'; const props = defineProps<{ workflow: IWorkflowDb; @@ -46,7 +44,6 @@ const sourceControlStore = useSourceControlStore(); const settingsStore = useSettingsStore(); const { isEligibleForMcpAccess } = useMcp(); const usersStore = useUsersStore(); -const posthogStore = usePostHog(); const isPopoverOpen = ref(false); const cachedSettings = ref(null); @@ -68,10 +65,6 @@ const hasErrorWorkflow = computed(() => { }); const hasSavedTimeNodes = computed(() => { - if (!posthogStore.isFeatureEnabled(TIME_SAVED_NODE_EXPERIMENT.name)) { - return false; - } - if (!props.workflow?.nodes) return false; return props.workflow.nodes.some( (node) => node.type === TIME_SAVED_NODE_TYPE && node.disabled !== true, diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts index 70309ff8738..de5501160f6 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts +++ b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.test.ts @@ -1,7 +1,7 @@ import { nextTick, reactive } from 'vue'; import { createTestingPinia } from '@pinia/testing'; import type { MockInstance } from 'vitest'; -import { within, waitFor } from '@testing-library/vue'; +import { waitFor, within } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import type { FrontendSettings } from '@n8n/api-types'; import { createComponentRenderer } from '@/__tests__/render'; @@ -224,13 +224,15 @@ describe('WorkflowSettingsVue', () => { ); it('should save time saved per execution correctly', async () => { + workflowsStore.workflowSettings.timeSavedMode = 'fixed'; const { getByTestId, getByRole } = createComponent({ pinia }); await nextTick(); + await waitFor(() => { + expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible(); + }); const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); - expect(timeSavedPerExecutionInput).toBeVisible(); - await userEvent.type(timeSavedPerExecutionInput as Element, '10'); expect(timeSavedPerExecutionInput).toHaveValue(10); @@ -242,14 +244,16 @@ describe('WorkflowSettingsVue', () => { }); it('should remove time saved per execution setting', async () => { + workflowsStore.workflowSettings.timeSavedMode = 'fixed'; workflowsStore.workflowSettings.timeSavedPerExecution = 10; const { getByTestId, getByRole } = createComponent({ pinia }); await nextTick(); + await waitFor(() => { + expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible(); + }); const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); - - expect(timeSavedPerExecutionInput).toBeVisible(); await waitFor(() => expect(timeSavedPerExecutionInput).toHaveValue(10)); await userEvent.clear(timeSavedPerExecutionInput as Element); @@ -265,18 +269,22 @@ describe('WorkflowSettingsVue', () => { }); it('should disable save time saved per execution if env is read-only', async () => { + workflowsStore.workflowSettings.timeSavedMode = 'fixed'; sourceControlStore.preferences.branchReadOnly = true; const { getByTestId } = createComponent({ pinia }); await nextTick(); + await waitFor(() => { + expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible(); + }); const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); - expect(timeSavedPerExecutionInput).toBeVisible(); expect(timeSavedPerExecutionInput).toBeDisabled(); }); it('should disable save time saved per execution if user has no permission to update workflow', async () => { + workflowsStore.workflowSettings.timeSavedMode = 'fixed'; workflowsStore.getWorkflowById.mockImplementation(() => ({ id: '1', name: 'Test Workflow', @@ -293,10 +301,12 @@ describe('WorkflowSettingsVue', () => { const { getByTestId } = createComponent({ pinia }); await nextTick(); + await waitFor(() => { + expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible(); + }); const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); - expect(timeSavedPerExecutionInput).toBeVisible(); expect(timeSavedPerExecutionInput).toBeDisabled(); }); }); diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue index 4d558fe9dca..8c35f719f0d 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue @@ -9,7 +9,6 @@ import { EnterpriseEditionFeature, PLACEHOLDER_EMPTY_WORKFLOW_ID, WORKFLOW_SETTINGS_MODAL_KEY, - TIME_SAVED_NODE_EXPERIMENT, NODE_CREATOR_OPEN_SOURCES, TIME_SAVED_NODE_TYPE, } from '@/app/constants'; @@ -31,7 +30,6 @@ import { useDebounce } from '@/app/composables/useDebounce'; import { injectWorkflowState } from '@/app/composables/useWorkflowState'; import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp'; import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions'; -import { usePostHog } from '@/app/stores/posthog.store'; import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store'; import { ElCol, ElRow, ElSwitch } from 'element-plus'; @@ -51,7 +49,6 @@ const sourceControlStore = useSourceControlStore(); const workflowsStore = useWorkflowsStore(); const workflowState = injectWorkflowState(); const workflowsEEStore = useWorkflowsEEStore(); -const posthogStore = usePostHog(); const nodeCreatorStore = useNodeCreatorStore(); const isLoading = ref(true); @@ -131,15 +128,7 @@ const isEligibleForMcp = computed(() => { return isEligibleForMcpAccess(workflow.value); }); -const isTimeSavedNodeExperimentEnabled = computed(() => { - return posthogStore.isFeatureEnabled(TIME_SAVED_NODE_EXPERIMENT.name); -}); - const savedTimeNodes = computed(() => { - if (!isTimeSavedNodeExperimentEnabled.value) { - return []; - } - if (!workflow?.value?.nodes) return []; return workflow.value.nodes.filter( (node) => node.type === TIME_SAVED_NODE_TYPE && node.disabled !== true, @@ -147,10 +136,6 @@ const savedTimeNodes = computed(() => { }); const hasSavedTimeNodes = computed(() => { - if (!isTimeSavedNodeExperimentEnabled.value) { - return false; - } - return savedTimeNodes.value.length > 0; }); @@ -160,7 +145,7 @@ const timeSavedModeOptions = computed(() => [ value: 'fixed' as const, }, { - label: 'Dynamic (node based)', + label: 'Dynamic (uses time saved nodes)', value: 'dynamic' as const, }, ]); @@ -503,6 +488,7 @@ onMounted(async () => { try { await Promise.all([ + workflowsStore.fetchWorkflow(workflowId.value), loadWorkflows(), loadSaveDataErrorExecutionOptions(), loadSaveDataSuccessExecutionOptions(), @@ -972,19 +958,7 @@ onBeforeUnmount(() => { -
- - {{ i18n.baseText('workflowSettings.timeSavedPerExecution.hint') }} -
-
+
{
- - + -
+
{
- +
@@ -1047,13 +1012,7 @@ onBeforeUnmount(() => { - +
@@ -1084,13 +1043,7 @@ onBeforeUnmount(() => { - +
@@ -1175,6 +1128,19 @@ onBeforeUnmount(() => { } } +.time-saved-input { + display: flex; + align-items: center; + + :global(.el-input) { + width: var(--spacing--3xl); + } + + span { + margin-left: var(--spacing--2xs); + } +} + .time-saved-dropdown { margin-bottom: var(--spacing--sm); } diff --git a/packages/frontend/editor-ui/src/app/constants/experiments.ts b/packages/frontend/editor-ui/src/app/constants/experiments.ts index 372824e31e5..ede73967a1c 100644 --- a/packages/frontend/editor-ui/src/app/constants/experiments.ts +++ b/packages/frontend/editor-ui/src/app/constants/experiments.ts @@ -89,10 +89,6 @@ export const PERSONALIZED_TEMPLATES_V3 = { variant: 'variant', }; -export const TIME_SAVED_NODE_EXPERIMENT = { - name: '053_time_saved_node', -}; - export const TEMPLATE_SETUP_EXPERIENCE = { name: '055_template_setup_experience', control: 'control', @@ -123,6 +119,5 @@ export const EXPERIMENTS_TO_TRACK = [ UPGRADE_PLAN_CTA_EXPERIMENT.name, AI_BUILDER_TEMPLATE_EXAMPLES_EXPERIMENT.name, AI_BUILDER_MULTI_AGENT_EXPERIMENT.name, - TIME_SAVED_NODE_EXPERIMENT.name, TEMPLATE_SETUP_EXPERIENCE.name, ]; diff --git a/packages/nodes-base/nodes/TimeSaved/TimeSaved.node.ts b/packages/nodes-base/nodes/TimeSaved/TimeSaved.node.ts index 0a2629d12d2..e847c608917 100644 --- a/packages/nodes-base/nodes/TimeSaved/TimeSaved.node.ts +++ b/packages/nodes-base/nodes/TimeSaved/TimeSaved.node.ts @@ -13,7 +13,8 @@ export class TimeSaved implements INodeType { icon: 'fa:timer', group: ['organization'], version: 1, - description: 'Track dynamic time savings for this workflow execution', + description: + 'Dynamically track time saved based on the workflow’s execution path and the number of items processed', defaults: { name: 'Time Saved', color: '#1E90FF', @@ -23,7 +24,7 @@ export class TimeSaved implements INodeType { properties: [ { displayName: - 'Calculate time saved dynamically based on execution data. This allows you to track variable time savings (e.g., 5 minutes per item processed) instead of using a fixed value in workflow settings.', + 'For each run, time saved is the sum of all Time Saved nodes that execute. Use this when different execution paths or items save different amounts of time.', name: 'notice', type: 'notice', default: '', @@ -69,8 +70,6 @@ export class TimeSaved implements INodeType { location: 'outputPane', }, ], - // TODO: see if we can use posthog here - hidden: true, }; async execute(this: IExecuteFunctions): Promise { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 822ca55ae71..208f6f7077f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -797,6 +797,7 @@ "dist/nodes/TravisCi/TravisCi.node.js", "dist/nodes/Trello/Trello.node.js", "dist/nodes/Trello/TrelloTrigger.node.js", + "dist/nodes/TimeSaved/TimeSaved.node.js", "dist/nodes/Twake/Twake.node.js", "dist/nodes/Twilio/Twilio.node.js", "dist/nodes/Twilio/TwilioTrigger.node.js",