mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
Merge branch 'n8n-io:master' into master
This commit is contained in:
commit
3cc6048501
|
@ -185,12 +185,9 @@ onMounted(async () => {
|
|||
});
|
||||
}
|
||||
|
||||
await nextTick(() => {
|
||||
uiStore.sidebarMenuCollapsed = window.innerWidth < 900;
|
||||
fullyExpanded.value = !isCollapsed.value;
|
||||
});
|
||||
|
||||
becomeTemplateCreatorStore.startMonitoringCta();
|
||||
|
||||
await nextTick(onResizeEnd);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
@ -273,22 +270,21 @@ const handleSelect = (key: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const onResize = (event: UIEvent) => {
|
||||
void callDebounced(onResizeEnd, { debounceTime: 100 }, event);
|
||||
};
|
||||
function onResize() {
|
||||
void callDebounced(onResizeEnd, { debounceTime: 250 });
|
||||
}
|
||||
|
||||
const onResizeEnd = async (event: UIEvent) => {
|
||||
const browserWidth = (event.target as Window).outerWidth;
|
||||
await checkWidthAndAdjustSidebar(browserWidth);
|
||||
};
|
||||
|
||||
const checkWidthAndAdjustSidebar = async (width: number) => {
|
||||
if (width < 900) {
|
||||
async function onResizeEnd() {
|
||||
if (window.outerWidth < 900) {
|
||||
uiStore.sidebarMenuCollapsed = true;
|
||||
await nextTick();
|
||||
fullyExpanded.value = !isCollapsed.value;
|
||||
} else {
|
||||
uiStore.sidebarMenuCollapsed = uiStore.sidebarMenuCollapsedPreference;
|
||||
}
|
||||
};
|
||||
|
||||
void nextTick(() => {
|
||||
fullyExpanded.value = !isCollapsed.value;
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
menu,
|
||||
|
|
|
@ -7,16 +7,31 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, WOOCOMMERCE_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowActivator);
|
||||
let mockWorkflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let mockCredentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
|
||||
|
||||
vi.mock('@/composables/useToast', () => {
|
||||
const showMessage = vi.fn();
|
||||
return {
|
||||
useToast: () => {
|
||||
return {
|
||||
showMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('WorkflowActivator', () => {
|
||||
beforeEach(() => {
|
||||
createTestingPinia();
|
||||
|
||||
mockWorkflowsStore = mockedStore(useWorkflowsStore);
|
||||
mockCredentialsStore = mockedStore(useCredentialsStore);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -79,4 +94,53 @@ describe('WorkflowActivator', () => {
|
|||
);
|
||||
expect(getByTestId('workflow-activator-status')).toHaveTextContent('Inactive');
|
||||
});
|
||||
|
||||
it('Should show warning toast if the workflow to be activated has free OpenAI credentials', async () => {
|
||||
const toast = useToast();
|
||||
|
||||
mockWorkflowsStore.workflow.usedCredentials = [
|
||||
{
|
||||
id: '1',
|
||||
name: '',
|
||||
credentialType: '',
|
||||
currentUserHasAccess: false,
|
||||
},
|
||||
];
|
||||
|
||||
mockCredentialsStore.state.credentials = {
|
||||
'1': {
|
||||
id: '1',
|
||||
name: 'OpenAI',
|
||||
type: 'openAiApi',
|
||||
data: '',
|
||||
isManaged: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
mockWorkflowsStore.workflowTriggerNodes = [
|
||||
{ type: WOOCOMMERCE_TRIGGER_NODE_TYPE, disabled: false } as never,
|
||||
];
|
||||
|
||||
const { rerender } = renderComponent({
|
||||
props: {
|
||||
workflowActive: false,
|
||||
workflowId: '1',
|
||||
workflowPermissions: { update: true },
|
||||
},
|
||||
});
|
||||
|
||||
await rerender({ workflowActive: true });
|
||||
|
||||
expect(toast.showMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "You're using free OpenAI API credits",
|
||||
message:
|
||||
'To make sure your workflow runs smoothly in the future, replace the free OpenAI API credits with your own API key.',
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,11 +4,13 @@ import { useWorkflowActivate } from '@/composables/useWorkflowActivate';
|
|||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { getActivatableTriggerNodes } from '@/utils/nodeTypesUtils';
|
||||
import type { VNode } from 'vue';
|
||||
import { computed, h } from 'vue';
|
||||
import { computed, h, watch } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { PermissionsRecord } from '@/permissions';
|
||||
import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
||||
import WorkflowActivationErrorMessage from './WorkflowActivationErrorMessage.vue';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import type { IUsedCredential } from '@/Interface';
|
||||
|
||||
const props = defineProps<{
|
||||
workflowActive: boolean;
|
||||
|
@ -20,6 +22,7 @@ const workflowActivate = useWorkflowActivate();
|
|||
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
const isWorkflowActive = computed((): boolean => {
|
||||
const activeWorkflows = workflowsStore.activeWorkflows;
|
||||
|
@ -69,6 +72,14 @@ const disabled = computed((): boolean => {
|
|||
return false;
|
||||
});
|
||||
|
||||
const currentWorkflowHasFreeAiCredits = computed((): boolean => {
|
||||
if (!workflowsStore?.workflow?.usedCredentials) return false;
|
||||
return workflowsStore.workflow.usedCredentials.some(
|
||||
(usedCredential: IUsedCredential) =>
|
||||
credentialsStore.state.credentials[usedCredential.id].isManaged,
|
||||
);
|
||||
});
|
||||
|
||||
async function activeChanged(newActiveState: boolean) {
|
||||
return await workflowActivate.updateWorkflowActivation(props.workflowId, newActiveState);
|
||||
}
|
||||
|
@ -100,6 +111,20 @@ async function displayActivationError() {
|
|||
duration: 0,
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.workflowActive,
|
||||
(workflowActive) => {
|
||||
if (workflowActive && currentWorkflowHasFreeAiCredits.value) {
|
||||
showMessage({
|
||||
title: i18n.baseText('freeAi.credits.showWarning.workflow.activation.title'),
|
||||
message: i18n.baseText('freeAi.credits.showWarning.workflow.activation.description'),
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -2848,5 +2848,7 @@
|
|||
"freeAi.credits.callout.success.title.part2": "gpt-4o-mini, text-embedding-3-small, dall-e-3, tts-1, whisper-1, and text-moderation-latest",
|
||||
"freeAi.credits.credentials.edit": "This is a managed credential and cannot be edited.",
|
||||
"freeAi.credits.showError.claim.title": "Free AI credits",
|
||||
"freeAi.credits.showError.claim.message": "Enable to claim credits"
|
||||
"freeAi.credits.showError.claim.message": "Enable to claim credits",
|
||||
"freeAi.credits.showWarning.workflow.activation.title": "You're using free OpenAI API credits",
|
||||
"freeAi.credits.showWarning.workflow.activation.description": "To make sure your workflow runs smoothly in the future, replace the free OpenAI API credits with your own API key."
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ import {
|
|||
} from './ui.utils';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Connection } from '@vue-flow/core';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
|
||||
let savedTheme: ThemeOption = 'system';
|
||||
|
||||
|
@ -151,7 +152,8 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
});
|
||||
|
||||
const modalStack = ref<string[]>([]);
|
||||
const sidebarMenuCollapsed = ref<boolean>(true);
|
||||
const sidebarMenuCollapsedPreference = useLocalStorage<boolean>('sidebar.collapsed', false);
|
||||
const sidebarMenuCollapsed = ref<boolean>(sidebarMenuCollapsedPreference.value);
|
||||
const currentView = ref<string>('');
|
||||
const draggable = ref<Draggable>({
|
||||
isDragging: false,
|
||||
|
@ -502,7 +504,9 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
};
|
||||
|
||||
const toggleSidebarMenuCollapse = () => {
|
||||
sidebarMenuCollapsed.value = !sidebarMenuCollapsed.value;
|
||||
const newCollapsedState = !sidebarMenuCollapsed.value;
|
||||
sidebarMenuCollapsedPreference.value = newCollapsedState;
|
||||
sidebarMenuCollapsed.value = newCollapsedState;
|
||||
};
|
||||
|
||||
const getCurlToJson = async (curlCommand: string) => {
|
||||
|
@ -592,6 +596,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
nodeViewInitialized,
|
||||
addFirstStepOnLoad,
|
||||
sidebarMenuCollapsed,
|
||||
sidebarMenuCollapsedPreference,
|
||||
bannerStack,
|
||||
theme,
|
||||
modalsById,
|
||||
|
|
Loading…
Reference in a new issue