feat: Enable time saved node for testing (#22650)

This commit is contained in:
Stephen Wright
2025-12-09 10:29:11 +00:00
committed by GitHub
parent 7e08952455
commit c57052a1da
7 changed files with 43 additions and 79 deletions
@@ -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",
@@ -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<WorkflowSettings | null>(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,
@@ -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();
});
});
@@ -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(() => {
</label>
</ElCol>
<ElCol :span="14">
<div v-if="!isTimeSavedNodeExperimentEnabled" :class="$style['time-saved']">
<N8nInput
id="timeSavedPerExecution"
v-model="workflowSettings.timeSavedPerExecution"
:disabled="readOnlyEnv || !workflowPermissions.update"
data-test-id="workflow-settings-time-saved-per-execution"
type="number"
min="0"
@update:model-value="updateTimeSavedPerExecution"
/>
<span>{{ i18n.baseText('workflowSettings.timeSavedPerExecution.hint') }}</span>
</div>
<div v-else class="ignore-key-press-canvas">
<div class="ignore-key-press-canvas">
<N8nSelect
v-model="workflowSettings.timeSavedMode"
:disabled="readOnlyEnv || !workflowPermissions.update"
@@ -1003,12 +977,9 @@ onBeforeUnmount(() => {
</div>
</ElCol>
</ElRow>
<!-- Fixed mode warning section (only shown in fixed mode when nodes exist) -->
<ElRow
v-if="isTimeSavedNodeExperimentEnabled && workflowSettings.timeSavedMode === 'fixed'"
>
<ElRow v-if="workflowSettings.timeSavedMode === 'fixed'">
<ElCol :span="14" :offset="10">
<div :class="$style['time-saved']">
<div :class="$style['time-saved-input']">
<N8nInput
id="timeSavedPerExecution"
v-model="workflowSettings.timeSavedPerExecution"
@@ -1022,13 +993,7 @@ onBeforeUnmount(() => {
</div>
</ElCol>
</ElRow>
<ElRow
v-if="
isTimeSavedNodeExperimentEnabled &&
workflowSettings.timeSavedMode === 'fixed' &&
hasSavedTimeNodes
"
>
<ElRow v-if="workflowSettings.timeSavedMode === 'fixed' && hasSavedTimeNodes">
<ElCol :span="14" :offset="10">
<div :class="$style['time-saved-content']">
<div :class="$style['time-saved-warning']">
@@ -1047,13 +1012,7 @@ onBeforeUnmount(() => {
</ElRow>
<!-- Minutes saved section (only shown in fixed mode) -->
<!-- Active nodes section (only shown in dynamic mode when nodes exist) -->
<ElRow
v-if="
isTimeSavedNodeExperimentEnabled &&
workflowSettings.timeSavedMode === 'dynamic' &&
hasSavedTimeNodes
"
>
<ElRow v-if="workflowSettings.timeSavedMode === 'dynamic' && hasSavedTimeNodes">
<ElCol :span="14" :offset="10">
<div :class="$style['time-saved-content']">
<div :class="$style['time-saved-nodes-active']">
@@ -1084,13 +1043,7 @@ onBeforeUnmount(() => {
</ElCol>
</ElRow>
<!-- No nodes detected section (only shown in dynamic mode when no nodes) -->
<ElRow
v-if="
isTimeSavedNodeExperimentEnabled &&
workflowSettings.timeSavedMode === 'dynamic' &&
!hasSavedTimeNodes
"
>
<ElRow v-if="workflowSettings.timeSavedMode === 'dynamic' && !hasSavedTimeNodes">
<ElCol :span="14" :offset="10">
<div :class="$style['time-saved-content']">
<div :class="$style['time-saved-no-nodes']">
@@ -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);
}
@@ -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,
];
@@ -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<INodeExecutionData[][]> {
+1
View File
@@ -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",