refactor(editor): Migrate ui.store to use composition API (no-changelog) (#9892)

This commit is contained in:
Ricardo Espinoza
2024-07-03 15:11:40 -04:00
committed by GitHub
parent 22990342df
commit f64ca621e1
42 changed files with 710 additions and 649 deletions

View File

@@ -6,7 +6,6 @@ import type {
TRIGGER_NODE_CREATOR_VIEW, TRIGGER_NODE_CREATOR_VIEW,
REGULAR_NODE_CREATOR_VIEW, REGULAR_NODE_CREATOR_VIEW,
AI_OTHERS_NODE_CREATOR_VIEW, AI_OTHERS_NODE_CREATOR_VIEW,
VIEWS,
ROLE, ROLE,
} from '@/constants'; } from '@/constants';
import type { IMenuItem, NodeCreatorTag } from 'n8n-design-system'; import type { IMenuItem, NodeCreatorTag } from 'n8n-design-system';
@@ -1264,41 +1263,6 @@ export interface NotificationOptions extends Partial<ElementNotificationOptions>
message: string | ElementNotificationOptions['message']; message: string | ElementNotificationOptions['message'];
} }
export interface UIState {
activeActions: string[];
activeCredentialType: string | null;
sidebarMenuCollapsed: boolean;
modalStack: string[];
modals: Modals;
isPageLoading: boolean;
currentView: string;
mainPanelPosition: number;
fakeDoorFeatures: IFakeDoor[];
draggable: {
isDragging: boolean;
type: string;
data: string;
canDrop: boolean;
stickyPosition: null | XYPosition;
};
stateIsDirty: boolean;
lastSelectedNode: string | null;
lastSelectedNodeOutputIndex: number | null;
lastSelectedNodeEndpointUuid: string | null;
nodeViewOffsetPosition: XYPosition;
nodeViewMoveInProgress: boolean;
selectedNodes: INodeUi[];
nodeViewInitialized: boolean;
addFirstStepOnLoad: boolean;
bannersHeight: number;
bannerStack: BannerName[];
theme: ThemeOption;
pendingNotificationsForViews: {
[key in VIEWS]?: NotificationOptions[];
};
isCreateNodeActive: boolean;
}
export type IFakeDoor = { export type IFakeDoor = {
id: FAKE_DOOR_FEATURES; id: FAKE_DOOR_FEATURES;
featureName: BaseTextKey; featureName: BaseTextKey;

View File

@@ -448,8 +448,8 @@ const showSharingContent = computed(() => activeTab.value === 'sharing' && !!cre
onMounted(async () => { onMounted(async () => {
requiredCredentials.value = requiredCredentials.value =
isCredentialModalState(uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY]) && isCredentialModalState(uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY]) &&
uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].showAuthSelector === true; uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY].showAuthSelector === true;
if (props.mode === 'new' && credentialTypeName.value) { if (props.mode === 'new' && credentialTypeName.value) {
credentialName.value = await credentialsStore.getNewCredentialName({ credentialName.value = await credentialsStore.getNewCredentialName({

View File

@@ -56,7 +56,7 @@ export default defineComponent({
return this.rootStore.instanceId; return this.rootStore.instanceId;
}, },
featureInfo(): IFakeDoor | undefined { featureInfo(): IFakeDoor | undefined {
return this.uiStore.getFakeDoorById(this.featureId); return this.uiStore.fakeDoorsById[this.featureId];
}, },
}, },
methods: { methods: {

View File

@@ -61,8 +61,8 @@ const ndvStore = useNDVStore();
const modalBus = createEventBus(); const modalBus = createEventBus();
const formBus = createEventBus(); const formBus = createEventBus();
const initialServiceValue = uiStore.getModalData(GENERATE_CURL_MODAL_KEY)?.service as string; const initialServiceValue = uiStore.modalsById[GENERATE_CURL_MODAL_KEY].data?.service as string;
const initialRequestValue = uiStore.getModalData(GENERATE_CURL_MODAL_KEY)?.request as string; const initialRequestValue = uiStore.modalsById[GENERATE_CURL_MODAL_KEY].data?.request as string;
const formInputs: IFormInput[] = [ const formInputs: IFormInput[] = [
{ {

View File

@@ -66,7 +66,7 @@ const { importCurlCommand } = useImportCurlCommand({
}); });
onMounted(() => { onMounted(() => {
curlCommand.value = (uiStore.getModalData(IMPORT_CURL_MODAL_KEY)?.curlCommand as string) ?? ''; curlCommand.value = (uiStore.modalsById[IMPORT_CURL_MODAL_KEY].data?.curlCommand as string) ?? '';
setTimeout(() => { setTimeout(() => {
inputRef.value?.focus(); inputRef.value?.focus();

View File

@@ -304,7 +304,7 @@ export default defineComponent({
return false; return false;
}, },
workflowRunning(): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
activeNode(): INodeUi | null { activeNode(): INodeUi | null {

View File

@@ -111,7 +111,7 @@ const isNewWorkflow = computed(() => {
}); });
const isWorkflowSaving = computed(() => { const isWorkflowSaving = computed(() => {
return uiStore.isActionActive('workflowSaving'); return uiStore.isActionActive['workflowSaving'];
}); });
const onWorkflowPage = computed(() => { const onWorkflowPage = computed(() => {

View File

@@ -1,6 +1,6 @@
<template> <template>
<el-dialog <el-dialog
:model-value="uiStore.isModalOpen(name)" :model-value="uiStore.modalsById[name].open"
:before-close="closeDialog" :before-close="closeDialog"
:class="{ :class="{
'dialog-wrapper': true, 'dialog-wrapper': true,
@@ -169,7 +169,7 @@ export default defineComponent({
}, },
methods: { methods: {
onWindowKeydown(event: KeyboardEvent) { onWindowKeydown(event: KeyboardEvent) {
if (!this.uiStore.isModalActive(this.name)) { if (!this.uiStore.isModalActiveById[this.name]) {
return; return;
} }
@@ -178,7 +178,7 @@ export default defineComponent({
} }
}, },
handleEnter() { handleEnter() {
if (this.uiStore.isModalActive(this.name)) { if (this.uiStore.isModalActiveById[this.name]) {
this.$emit('enter'); this.$emit('enter');
} }
}, },

View File

@@ -1,7 +1,7 @@
<template> <template>
<ElDrawer <ElDrawer
:direction="direction" :direction="direction"
:model-value="uiStore.isModalOpen(name)" :model-value="uiStore.modalsById[name].open"
:size="width" :size="width"
:before-close="close" :before-close="close"
:modal="modal" :modal="modal"
@@ -74,7 +74,7 @@ export default defineComponent({
}, },
methods: { methods: {
onWindowKeydown(event: KeyboardEvent) { onWindowKeydown(event: KeyboardEvent) {
if (!this.uiStore.isModalActive(this.name)) { if (!this.uiStore.isModalActiveById[this.name]) {
return; return;
} }
@@ -83,7 +83,7 @@ export default defineComponent({
} }
}, },
handleEnter() { handleEnter() {
if (this.uiStore.isModalActive(this.name)) { if (this.uiStore.isModalActiveById[this.name]) {
this.$emit('enter'); this.$emit('enter');
} }
}, },

View File

@@ -1,12 +1,12 @@
<template> <template>
<div v-if="uiStore.isModalOpen(name) || keepAlive"> <div v-if="uiStore.modalsById[name].open || keepAlive">
<slot <slot
:modal-name="name" :modal-name="name"
:active="uiStore.isModalActive(name)" :active="uiStore.isModalActiveById[name]"
:open="uiStore.isModalOpen(name)" :open="uiStore.modalsById[name].open"
:active-id="uiStore.getModalActiveId(name)" :active-id="uiStore.modalsById[name].activeId"
:mode="uiStore.getModalMode(name)" :mode="uiStore.modalsById[name].mode"
:data="uiStore.getModalData(name)" :data="uiStore.modalsById[name].data"
></slot> ></slot>
</div> </div>
</template> </template>

View File

@@ -592,7 +592,7 @@ export default defineComponent({
return undefined; return undefined;
}, },
workflowRunning(): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
nodeStyle() { nodeStyle() {
const returnStyles: { const returnStyles: {

View File

@@ -242,7 +242,7 @@ const activeNodeType = computed(() => {
return null; return null;
}); });
const workflowRunning = computed(() => uiStore.isActionActive('workflowRunning')); const workflowRunning = computed(() => uiStore.isActionActive['workflowRunning']);
const showTriggerWaitingWarning = computed( const showTriggerWaitingWarning = computed(
() => () =>
@@ -432,7 +432,7 @@ const featureRequestUrl = computed(() => {
const outputPanelEditMode = computed(() => ndvStore.outputPanelEditMode); const outputPanelEditMode = computed(() => ndvStore.outputPanelEditMode);
const isWorkflowRunning = computed(() => uiStore.isActionActive('workflowRunning')); const isWorkflowRunning = computed(() => uiStore.isActionActive['workflowRunning']);
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook); const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);

View File

@@ -128,7 +128,7 @@ export default defineComponent({
); );
}, },
workflowRunning(): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
isTriggerNode(): boolean { isTriggerNode(): boolean {
if (!this.node) { if (!this.node) {

View File

@@ -230,7 +230,7 @@ export default defineComponent({
return !!this.node && this.workflowsStore.isNodeExecuting(this.node.name); return !!this.node && this.workflowsStore.isNodeExecuting(this.node.name);
}, },
workflowRunning(): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
workflowExecution(): IExecutionResponse | null { workflowExecution(): IExecutionResponse | null {
return this.workflowsStore.getWorkflowExecution; return this.workflowsStore.getWorkflowExecution;

View File

@@ -8,7 +8,9 @@ import { useTelemetry } from '@/composables/useTelemetry';
vi.mock('@/stores/ui.store', () => ({ vi.mock('@/stores/ui.store', () => ({
useUIStore: vi.fn().mockReturnValue({ useUIStore: vi.fn().mockReturnValue({
isModalOpen: vi.fn().mockReturnValue(() => true), modalsById: vi.fn().mockReturnValue(() => {
open: true;
}),
closeModal: vi.fn(), closeModal: vi.fn(),
}), }),
})); }));

View File

@@ -46,7 +46,9 @@ export default defineComponent({
computed: { computed: {
...mapStores(useRootStore, useSettingsStore, useUIStore), ...mapStores(useRootStore, useSettingsStore, useUIStore),
settingsFakeDoorFeatures(): IFakeDoor[] { settingsFakeDoorFeatures(): IFakeDoor[] {
return this.uiStore.getFakeDoorByLocation('settings'); return Object.keys(this.uiStore.fakeDoorsByLocation)
.filter((location: string) => location.includes('settings'))
.map((location) => this.uiStore.fakeDoorsByLocation[location]);
}, },
sidebarMenuItems(): IMenuItem[] { sidebarMenuItems(): IMenuItem[] {
const menuItems: IMenuItem[] = [ const menuItems: IMenuItem[] = [

View File

@@ -141,7 +141,7 @@ function getContext() {
return 'workflows'; return 'workflows';
} else if ( } else if (
route.fullPath.startsWith('/credentials') || route.fullPath.startsWith('/credentials') ||
uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].open uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY].open
) { ) {
return 'credentials'; return 'credentials';
} else if (route.fullPath.startsWith('/workflow/')) { } else if (route.fullPath.startsWith('/workflow/')) {

View File

@@ -288,7 +288,7 @@ export default defineComponent({
); );
}, },
workflowRunning(): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
}, },
mounted() { mounted() {

View File

@@ -264,7 +264,7 @@ export default defineComponent({
); );
}, },
workflowRunning(): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
isActivelyPolling(): boolean { isActivelyPolling(): boolean {
const triggeredNode = this.workflowsStore.executedNode; const triggeredNode = this.workflowsStore.executedNode;

View File

@@ -219,7 +219,7 @@ export default defineComponent({
computed: { computed: {
...mapStores(useWorkflowsStore, useUIStore, useNodeTypesStore), ...mapStores(useWorkflowsStore, useUIStore, useNodeTypesStore),
isLoading(): boolean { isLoading(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
}, },
async mounted() { async mounted() {

View File

@@ -12,7 +12,7 @@ const renderComponent = createComponentRenderer(ChatEmbedModal, {
pinia: createTestingPinia({ pinia: createTestingPinia({
initialState: { initialState: {
[STORES.UI]: { [STORES.UI]: {
modals: { modalsById: {
[CHAT_EMBED_MODAL_KEY]: { open: true }, [CHAT_EMBED_MODAL_KEY]: { open: true },
}, },
}, },

View File

@@ -17,7 +17,7 @@ const renderComponent = createComponentRenderer(CommunityPackageInstallModal, {
pinia: createTestingPinia({ pinia: createTestingPinia({
initialState: { initialState: {
[STORES.UI]: { [STORES.UI]: {
modals: { modalsById: {
[COMMUNITY_PACKAGE_INSTALL_MODAL_KEY]: { open: true }, [COMMUNITY_PACKAGE_INSTALL_MODAL_KEY]: { open: true },
}, },
}, },

View File

@@ -11,7 +11,7 @@ import { useUsageStore } from '@/stores/usage.store';
const pinia = createTestingPinia({ const pinia = createTestingPinia({
initialState: { initialState: {
[STORES.UI]: { [STORES.UI]: {
modals: { modalsById: {
[PERSONALIZATION_MODAL_KEY]: { open: true }, [PERSONALIZATION_MODAL_KEY]: { open: true },
}, },
}, },

View File

@@ -53,7 +53,7 @@ describe('WorkflowSettingsVue', () => {
versionId: '123', versionId: '123',
} as IWorkflowDb); } as IWorkflowDb);
uiStore.modals[WORKFLOW_SETTINGS_MODAL_KEY] = { uiStore.modalsById[WORKFLOW_SETTINGS_MODAL_KEY] = {
open: true, open: true,
}; };
}); });

View File

@@ -11,7 +11,7 @@ defineEmits<{
const uiStore = useUIStore(); const uiStore = useUIStore();
const locale = useI18n(); const locale = useI18n();
const workflowRunning = computed(() => uiStore.isActionActive('workflowRunning')); const workflowRunning = computed(() => uiStore.isActionActive['workflowRunning']);
const runButtonText = computed(() => { const runButtonText = computed(() => {
if (!workflowRunning.value) { if (!workflowRunning.value) {

View File

@@ -8,7 +8,7 @@ vi.mock('@/stores/ui.store', () => ({
useUIStore: vi.fn(() => ({ useUIStore: vi.fn(() => ({
nodeViewOffsetPosition: [0, 0], nodeViewOffsetPosition: [0, 0],
nodeViewMoveInProgress: false, nodeViewMoveInProgress: false,
isActionActive: vi.fn(), isActionActive: vi.fn().mockReturnValue(() => true),
})), })),
})); }));
@@ -62,7 +62,7 @@ describe('useCanvasPanning()', () => {
vi.mocked(useUIStore).mockReturnValueOnce({ vi.mocked(useUIStore).mockReturnValueOnce({
nodeViewOffsetPosition: [0, 0], nodeViewOffsetPosition: [0, 0],
nodeViewMoveInProgress: true, nodeViewMoveInProgress: true,
isActionActive: vi.fn(), isActionActive: vi.fn().mockReturnValue(() => true),
} as unknown as ReturnType<typeof useUIStore>); } as unknown as ReturnType<typeof useUIStore>);
const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener'); const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener');

View File

@@ -140,21 +140,15 @@ describe('useNodeBase', () => {
it('should handle mouse left click correctly', () => { it('should handle mouse left click correctly', () => {
const { mouseLeftClick } = nodeBase; const { mouseLeftClick } = nodeBase;
const isActionActiveFn = vi.fn().mockReturnValue(false);
// @ts-expect-error Pinia has a known issue when mocking getters, will be solved when migrating the uiStore to composition api
vi.spyOn(uiStore, 'isActionActive', 'get').mockReturnValue(isActionActiveFn);
// @ts-expect-error Pinia has a known issue when mocking getters, will be solved when migrating the uiStore to composition api
vi.spyOn(uiStore, 'isNodeSelected', 'get').mockReturnValue(() => false);
const event = new MouseEvent('click', { const event = new MouseEvent('click', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
}); });
uiStore.addActiveAction('notDragActive');
mouseLeftClick(event); mouseLeftClick(event);
expect(isActionActiveFn).toHaveBeenCalledWith('dragActive');
expect(emit).toHaveBeenCalledWith('deselectAllNodes'); expect(emit).toHaveBeenCalledWith('deselectAllNodes');
expect(emit).toHaveBeenCalledWith('nodeSelected', node.name); expect(emit).toHaveBeenCalledWith('nodeSelected', node.name);
}); });

View File

@@ -25,14 +25,6 @@ vi.mock('@/stores/workflows.store', () => ({
}), }),
})); }));
vi.mock('@/stores/ui.store', () => ({
useUIStore: vi.fn().mockReturnValue({
isActionActive: vi.fn().mockReturnValue(false),
addActiveAction: vi.fn(),
removeActiveAction: vi.fn(),
}),
}));
vi.mock('@/composables/useTelemetry', () => ({ vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: vi.fn().mockReturnValue({ track: vi.fn() }), useTelemetry: vi.fn().mockReturnValue({ track: vi.fn() }),
})); }));
@@ -103,6 +95,10 @@ describe('useRunWorkflow({ router })', () => {
workflowHelpers = useWorkflowHelpers({ router }); workflowHelpers = useWorkflowHelpers({ router });
}); });
beforeEach(() => {
uiStore.activeActions = [];
});
describe('runWorkflowApi()', () => { describe('runWorkflowApi()', () => {
it('should throw an error if push connection is not active', async () => { it('should throw an error if push connection is not active', async () => {
const { runWorkflowApi } = useRunWorkflow({ router }); const { runWorkflowApi } = useRunWorkflow({ router });
@@ -157,7 +153,7 @@ describe('useRunWorkflow({ router })', () => {
describe('runWorkflow()', () => { describe('runWorkflow()', () => {
it('should return undefined if UI action "workflowRunning" is active', async () => { it('should return undefined if UI action "workflowRunning" is active', async () => {
const { runWorkflow } = useRunWorkflow({ router }); const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(uiStore).isActionActive.mockReturnValue(true); uiStore.addActiveAction('workflowRunning');
const result = await runWorkflow({}); const result = await runWorkflow({});
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
@@ -166,7 +162,7 @@ describe('useRunWorkflow({ router })', () => {
const mockExecutionResponse = { executionId: '123' }; const mockExecutionResponse = { executionId: '123' };
const { runWorkflow } = useRunWorkflow({ router }); const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(uiStore).isActionActive.mockReturnValue(false); vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
name: 'Test Workflow', name: 'Test Workflow',
} as unknown as Workflow); } as unknown as Workflow);

View File

@@ -167,7 +167,7 @@ export default function useCanvasMouseSelect() {
return; return;
} }
if (uiStore.isActionActive('dragActive')) { if (uiStore.isActionActive['dragActive']) {
// If a node does currently get dragged we do not activate the selection // If a node does currently get dragged we do not activate the selection
return; return;
} }

View File

@@ -49,7 +49,7 @@ export function useCanvasPanning(
return; return;
} }
if (uiStore.isActionActive('dragActive')) { if (uiStore.isActionActive['dragActive']) {
// If a node does currently get dragged we do not activate the selection // If a node does currently get dragged we do not activate the selection
return; return;
} }
@@ -95,7 +95,7 @@ export function useCanvasPanning(
return; return;
} }
if (uiStore.isActionActive('dragActive')) { if (uiStore.isActionActive['dragActive']) {
return; return;
} }

View File

@@ -621,7 +621,7 @@ export function useNodeBase({
} }
function touchEnd(_e: MouseEvent) { function touchEnd(_e: MouseEvent) {
if (deviceSupport.isTouchDevice && uiStore.isActionActive('dragActive')) { if (deviceSupport.isTouchDevice && uiStore.isActionActive['dragActive']) {
uiStore.removeActiveAction('dragActive'); uiStore.removeActiveAction('dragActive');
} }
} }
@@ -640,14 +640,14 @@ export function useNodeBase({
} }
if (!deviceSupport.isTouchDevice) { if (!deviceSupport.isTouchDevice) {
if (uiStore.isActionActive('dragActive')) { if (uiStore.isActionActive['dragActive']) {
uiStore.removeActiveAction('dragActive'); uiStore.removeActiveAction('dragActive');
} else { } else {
if (!deviceSupport.isCtrlKeyPressed(e)) { if (!deviceSupport.isCtrlKeyPressed(e)) {
emit('deselectAllNodes'); emit('deselectAllNodes');
} }
if (uiStore.isNodeSelected(data.value?.name ?? '')) { if (uiStore.isNodeSelected[data.value?.name ?? '']) {
emit('deselectNode', name); emit('deselectNode', name);
} else { } else {
emit('nodeSelected', name); emit('nodeSelected', name);

View File

@@ -149,7 +149,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
} }
if (receivedData.type === 'nodeExecuteAfter' || receivedData.type === 'nodeExecuteBefore') { if (receivedData.type === 'nodeExecuteAfter' || receivedData.type === 'nodeExecuteBefore') {
if (!uiStore.isActionActive('workflowRunning')) { if (!uiStore.isActionActive['workflowRunning']) {
// No workflow is running so ignore the messages // No workflow is running so ignore the messages
return false; return false;
} }
@@ -169,7 +169,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
let recoveredPushData: IPushDataExecutionFinished | undefined = undefined; let recoveredPushData: IPushDataExecutionFinished | undefined = undefined;
if (receivedData.type === 'executionRecovered') { if (receivedData.type === 'executionRecovered') {
const recoveredExecutionId = receivedData.data?.executionId; const recoveredExecutionId = receivedData.data?.executionId;
const isWorkflowRunning = uiStore.isActionActive('workflowRunning'); const isWorkflowRunning = uiStore.isActionActive['workflowRunning'];
if (isWorkflowRunning && workflowsStore.activeExecutionId === recoveredExecutionId) { if (isWorkflowRunning && workflowsStore.activeExecutionId === recoveredExecutionId) {
// pull execution data for the recovered execution from the server // pull execution data for the recovered execution from the server
const executionData = await workflowsStore.fetchExecutionDataById( const executionData = await workflowsStore.fetchExecutionDataById(
@@ -262,7 +262,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
workflowsStore.finishActiveExecution(pushData); workflowsStore.finishActiveExecution(pushData);
} }
if (!uiStore.isActionActive('workflowRunning')) { if (!uiStore.isActionActive['workflowRunning']) {
// No workflow is running so ignore the messages // No workflow is running so ignore the messages
return false; return false;
} }

View File

@@ -91,7 +91,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
}): Promise<IExecutionPushResponse | undefined> { }): Promise<IExecutionPushResponse | undefined> {
const workflow = workflowHelpers.getCurrentWorkflow(); const workflow = workflowHelpers.getCurrentWorkflow();
if (uiStore.isActionActive('workflowRunning')) { if (uiStore.isActionActive['workflowRunning']) {
return; return;
} }

View File

@@ -175,7 +175,7 @@ export function useToast() {
function showNotificationForViews(views: VIEWS[]) { function showNotificationForViews(views: VIEWS[]) {
const notifications: NotificationOptions[] = []; const notifications: NotificationOptions[] = [];
views.forEach((view) => { views.forEach((view) => {
notifications.push(...uiStore.getNotificationsForView(view)); notifications.push(...(uiStore.pendingNotificationsForViews[view] ?? []));
}); });
if (notifications.length) { if (notifications.length) {
notifications.forEach(async (notification) => { notifications.forEach(async (notification) => {

View File

@@ -4,8 +4,8 @@ import { FAKE_DOOR_FEATURES } from '@/constants';
import type { BaseTextKey } from '@/plugins/i18n'; import type { BaseTextKey } from '@/plugins/i18n';
export function compileFakeDoorFeatures(): IFakeDoor[] { export function compileFakeDoorFeatures(): IFakeDoor[] {
const store = useUIStore(); const uiStore = useUIStore();
const fakeDoorFeatures: IFakeDoor[] = store.fakeDoorFeatures.map((feature) => ({ ...feature })); const fakeDoorFeatures: IFakeDoor[] = uiStore.fakeDoorFeatures.map((feature) => ({ ...feature }));
const environmentsFeature = fakeDoorFeatures.find( const environmentsFeature = fakeDoorFeatures.find(
(feature) => feature.id === FAKE_DOOR_FEATURES.ENVIRONMENTS, (feature) => feature.id === FAKE_DOOR_FEATURES.ENVIRONMENTS,
@@ -28,7 +28,7 @@ export function compileFakeDoorFeatures(): IFakeDoor[] {
} }
export const hooksAddFakeDoorFeatures = () => { export const hooksAddFakeDoorFeatures = () => {
const store = useUIStore(); const uiStore = useUIStore();
store.fakeDoorFeatures = compileFakeDoorFeatures(); uiStore.fakeDoorFeatures = compileFakeDoorFeatures();
}; };

View File

@@ -357,7 +357,7 @@ describe('Resolution-based completions', () => {
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input); vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input);
uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].open = true; uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY].open = true;
set(settingsStore.settings, ['enterprise', EnterpriseEditionFeature.ExternalSecrets], true); set(settingsStore.settings, ['enterprise', EnterpriseEditionFeature.ExternalSecrets], true);
externalSecretsStore.state.secrets = { externalSecretsStore.state.secrets = {
[provider]: secrets, [provider]: secrets,
@@ -380,7 +380,7 @@ describe('Resolution-based completions', () => {
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input); vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input);
uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].open = true; uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY].open = true;
set(settingsStore.settings, ['enterprise', EnterpriseEditionFeature.ExternalSecrets], true); set(settingsStore.settings, ['enterprise', EnterpriseEditionFeature.ExternalSecrets], true);
externalSecretsStore.state.secrets = { externalSecretsStore.state.secrets = {
[provider]: secrets, [provider]: secrets,

View File

@@ -108,7 +108,7 @@ export function hasNoParams(toResolve: string) {
// state-based utils // state-based utils
// ---------------------------------- // ----------------------------------
export const isCredentialsModalOpen = () => useUIStore().modals[CREDENTIAL_EDIT_MODAL_KEY].open; export const isCredentialsModalOpen = () => useUIStore().modalsById[CREDENTIAL_EDIT_MODAL_KEY].open;
export const isInHttpNodePagination = () => { export const isInHttpNodePagination = () => {
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();

View File

@@ -1,5 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { useUIStore } from '@/stores/ui.store'; import { generateUpgradeLinkUrl, useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
@@ -98,7 +98,7 @@ describe('UI store', () => {
'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source', 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
], ],
])( ])(
'"upgradeLinkUrl" should generate the correct URL for "%s" deployment and "%s" license environment and user role "%s"', '"generateUpgradeLinkUrl" should generate the correct URL for "%s" deployment and "%s" license environment and user role "%s"',
async (type, environment, role, expectation) => { async (type, environment, role, expectation) => {
setUser(role as IRole); setUser(role as IRole);
@@ -115,7 +115,7 @@ describe('UI store', () => {
}), }),
); );
const updateLinkUrl = await uiStore.upgradeLinkUrl('test_source', 'utm-test-campaign', type); const updateLinkUrl = await generateUpgradeLinkUrl('test_source', 'utm-test-campaign', type);
expect(updateLinkUrl).toBe(expectation); expect(updateLinkUrl).toBe(expectation);
}, },

View File

@@ -236,7 +236,7 @@ export const useCanvasStore = defineStore('canvas', () => {
if (!nodeName) return; if (!nodeName) return;
isDragging.value = true; isDragging.value = true;
const isSelected = uiStore.isNodeSelected(nodeName); const isSelected = uiStore.isNodeSelected[nodeName];
if (params.e && !isSelected) { if (params.e && !isSelected) {
// Only the node which gets dragged directly gets an event, for all others it is // Only the node which gets dragged directly gets an event, for all others it is
@@ -255,7 +255,7 @@ export const useCanvasStore = defineStore('canvas', () => {
if (!nodeName) return; if (!nodeName) return;
const nodeData = workflowStore.getNodeByName(nodeName); const nodeData = workflowStore.getNodeByName(nodeName);
isDragging.value = false; isDragging.value = false;
if (uiStore.isActionActive('dragActive') && nodeData) { if (uiStore.isActionActive['dragActive'] && nodeData) {
const moveNodes = uiStore.getSelectedNodes.slice(); const moveNodes = uiStore.getSelectedNodes.slice();
const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name); const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name);
if (!selectedNodeNames.includes(nodeData.name)) { if (!selectedNodeNames.includes(nodeData.name)) {
@@ -300,7 +300,7 @@ export const useCanvasStore = defineStore('canvas', () => {
if (moveNodes.length > 1) { if (moveNodes.length > 1) {
historyStore.stopRecordingUndo(); historyStore.stopRecordingUndo();
} }
if (uiStore.isActionActive('dragActive')) { if (uiStore.isActionActive['dragActive']) {
uiStore.removeActiveAction('dragActive'); uiStore.removeActiveAction('dragActive');
} }
} }

View File

@@ -1,8 +1,4 @@
import { import * as onboardingApi from '@/api/workflow-webhooks';
applyForOnboardingCall,
fetchNextOnboardingPrompt,
submitEmailOnSignup,
} from '@/api/workflow-webhooks';
import { import {
ABOUT_MODAL_KEY, ABOUT_MODAL_KEY,
CHAT_EMBED_MODAL_KEY, CHAT_EMBED_MODAL_KEY,
@@ -44,24 +40,21 @@ import {
} from '@/constants'; } from '@/constants';
import type { import type {
CloudUpdateLinkSourceType, CloudUpdateLinkSourceType,
CurlToJSONResponse,
IFakeDoorLocation, IFakeDoorLocation,
INodeUi, INodeUi,
IOnboardingCallPrompt,
UIState,
UTMCampaign, UTMCampaign,
XYPosition, XYPosition,
Modals, Modals,
NewCredentialsModal, NewCredentialsModal,
ThemeOption, ThemeOption,
AppliedThemeOption,
NotificationOptions, NotificationOptions,
ModalState, ModalState,
ModalKey, ModalKey,
IFakeDoor,
} from '@/Interface'; } from '@/Interface';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { getCurlToJson } from '@/api/curlHelper'; import * as curlParserApi from '@/api/curlHelper';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
@@ -77,6 +70,7 @@ import {
isValidTheme, isValidTheme,
updateTheme, updateTheme,
} from './ui.utils'; } from './ui.utils';
import { computed, ref } from 'vue';
let savedTheme: ThemeOption = 'system'; let savedTheme: ThemeOption = 'system';
try { try {
@@ -87,14 +81,21 @@ try {
} }
} catch (e) {} } catch (e) {}
export type UiStore = ReturnType<typeof useUIStore>; type UiStore = ReturnType<typeof useUIStore>;
export const useUIStore = defineStore(STORES.UI, { type Draggable = {
state: (): UIState => ({ isDragging: boolean;
activeActions: [], type: string;
activeCredentialType: null, data: string;
theme: savedTheme, canDrop: boolean;
modals: { stickyPosition: null | XYPosition;
};
export const useUIStore = defineStore(STORES.UI, () => {
const activeActions = ref<string[]>([]);
const activeCredentialType = ref<string | null>(null);
const theme = ref<ThemeOption>(savedTheme);
const modalsById = ref<Record<string, ModalState>>({
...Object.fromEntries( ...Object.fromEntries(
[ [
ABOUT_MODAL_KEY, ABOUT_MODAL_KEY,
@@ -157,13 +158,12 @@ export const useUIStore = defineStore(STORES.UI, {
activeId: null, activeId: null,
showAuthSelector: false, showAuthSelector: false,
} as ModalState, } as ModalState,
}, });
modalStack: [],
sidebarMenuCollapsed: true, const modalStack = ref<string[]>([]);
isPageLoading: true, const sidebarMenuCollapsed = ref<boolean>(true);
currentView: '', const currentView = ref<string>('');
mainPanelPosition: 0.5, const fakeDoorFeatures = ref<IFakeDoor[]>([
fakeDoorFeatures: [
{ {
id: FAKE_DOOR_FEATURES.SSO, id: FAKE_DOOR_FEATURES.SSO,
featureName: 'fakeDoor.settings.sso.name', featureName: 'fakeDoor.settings.sso.name',
@@ -173,44 +173,50 @@ export const useUIStore = defineStore(STORES.UI, {
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=sso', linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=sso',
uiLocations: ['settings/users'], uiLocations: ['settings/users'],
}, },
], ]);
draggable: {
const draggable = ref<Draggable>({
isDragging: false, isDragging: false,
type: '', type: '',
data: '', data: '',
canDrop: false, canDrop: false,
stickyPosition: null, stickyPosition: null,
}, });
stateIsDirty: false,
lastSelectedNode: null, const stateIsDirty = ref<boolean>(false);
lastSelectedNodeOutputIndex: null, const lastSelectedNode = ref<string | null>(null);
lastSelectedNodeEndpointUuid: null, const lastSelectedNodeOutputIndex = ref<number | null>(null);
nodeViewOffsetPosition: [0, 0], const lastSelectedNodeEndpointUuid = ref<string | null>(null);
nodeViewMoveInProgress: false, const nodeViewOffsetPosition = ref<[number, number]>([0, 0]);
selectedNodes: [], const nodeViewMoveInProgress = ref<boolean>(false);
nodeViewInitialized: false, const selectedNodes = ref<INodeUi[]>([]);
addFirstStepOnLoad: false, const nodeViewInitialized = ref<boolean>(false);
bannersHeight: 0, const addFirstStepOnLoad = ref<boolean>(false);
bannerStack: [], const bannersHeight = ref<number>(0);
// Notifications that should show when a view is initialized const bannerStack = ref<BannerName[]>([]);
// This enables us to set a queue of notifications form outside (another component) const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
// and then show them when the view is initialized const isCreateNodeActive = ref<boolean>(false);
pendingNotificationsForViews: {},
isCreateNodeActive: false, const settingsStore = useSettingsStore();
}), const workflowsStore = useWorkflowsStore();
getters: { const rootStore = useRootStore();
appliedTheme(): AppliedThemeOption { const telemetryStore = useTelemetryStore();
return this.theme === 'system' ? getPreferredTheme() : this.theme; const cloudPlanStore = useCloudPlanStore();
}, const userStore = useUsersStore();
logo(): string {
const { releaseChannel } = useSettingsStore().settings; const appliedTheme = computed(() => {
const suffix = this.appliedTheme === 'dark' ? '-dark.svg' : '.svg'; return theme.value === 'system' ? getPreferredTheme() : theme.value;
});
const logo = computed(() => {
const { releaseChannel } = settingsStore.settings;
const suffix = appliedTheme.value === 'dark' ? '-dark.svg' : '.svg';
return `static/logo/${ return `static/logo/${
releaseChannel === 'stable' ? 'expanded' : `channel/${releaseChannel}` releaseChannel === 'stable' ? 'expanded' : `channel/${releaseChannel}`
}${suffix}`; }${suffix}`;
}, });
contextBasedTranslationKeys() {
const settingsStore = useSettingsStore(); const contextBasedTranslationKeys = computed(() => {
const deploymentType = settingsStore.deploymentType; const deploymentType = settingsStore.deploymentType;
let contextKey: '' | '.cloud' | '.desktop' = ''; let contextKey: '' | '.cloud' | '.desktop' = '';
@@ -268,291 +274,283 @@ export const useUIStore = defineStore(STORES.UI, {
}, },
}, },
} as const; } as const;
}, });
getLastSelectedNode(): INodeUi | null {
const workflowsStore = useWorkflowsStore(); const getLastSelectedNode = computed(() => {
if (this.lastSelectedNode) { if (lastSelectedNode.value) {
return workflowsStore.getNodeByName(this.lastSelectedNode); return workflowsStore.getNodeByName(lastSelectedNode.value);
} }
return null; return null;
}, });
areExpressionsDisabled(): boolean {
return this.currentView === VIEWS.DEMO; const isVersionsOpen = computed(() => {
}, return modalsById.value[VERSIONS_MODAL_KEY].open;
isVersionsOpen(): boolean { });
return this.modals[VERSIONS_MODAL_KEY].open;
}, const isModalActiveById = computed(() =>
isModalOpen() { Object.keys(modalsById.value).reduce((acc: { [key: string]: boolean }, name) => {
return (name: ModalKey) => this.modals[name].open; acc[name] = name === modalStack.value[0];
}, return acc;
isModalActive() { }, {}),
return (name: ModalKey) => this.modalStack.length > 0 && name === this.modalStack[0];
},
getModalActiveId() {
return (name: ModalKey) => this.modals[name].activeId;
},
getModalMode() {
return (name: ModalKey) => this.modals[name].mode;
},
getModalData() {
return (name: ModalKey) => this.modals[name].data;
},
getFakeDoorByLocation() {
return (location: IFakeDoorLocation) =>
this.fakeDoorFeatures.filter((fakeDoor) => fakeDoor.uiLocations.includes(location));
},
getFakeDoorById() {
return (id: string) =>
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
},
isReadOnlyView(): boolean {
return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, VIEWS.EXECUTION_DEBUG].includes(
this.currentView as VIEWS,
); );
},
isNodeView(): boolean { const fakeDoorsByLocation = computed(() =>
return [ fakeDoorFeatures.value.reduce((acc: { [uiLocation: string]: IFakeDoor }, fakeDoor) => {
VIEWS.NEW_WORKFLOW.toString(), fakeDoor.uiLocations.forEach((uiLocation: IFakeDoorLocation) => {
acc[uiLocation] = fakeDoor;
});
return acc;
}, {}),
);
const fakeDoorsById = computed(() =>
fakeDoorFeatures.value.reduce((acc: { [id: string]: IFakeDoor }, fakeDoor) => {
acc[fakeDoor.id.toString()] = fakeDoor;
return acc;
}, {}),
);
const isReadOnlyView = computed(() => {
return ![
VIEWS.WORKFLOW.toString(), VIEWS.WORKFLOW.toString(),
VIEWS.WORKFLOW_EXECUTIONS.toString(), VIEWS.NEW_WORKFLOW.toString(),
].includes(this.currentView); VIEWS.EXECUTION_DEBUG.toString(),
}, ].includes(currentView.value);
isActionActive() { });
return (action: string) => this.activeActions.includes(action);
}, const isActionActive = computed(() =>
getSelectedNodes(): INodeUi[] { activeActions.value.reduce((acc: { [action: string]: boolean }, action) => {
acc[action] = true;
return acc;
}, {}),
);
const getSelectedNodes = computed(() => {
const seen = new Set(); const seen = new Set();
return this.selectedNodes.filter((node: INodeUi) => { return selectedNodes.value.filter((node) => {
// dedupe for instances when same node is selected in different ways // dedupe for instances when same node is selected in different ways
if (!seen.has(node.id)) { if (!seen.has(node)) {
seen.add(node.id); seen.add(node);
return true; return true;
} }
return false; return false;
}); });
}, });
isNodeSelected() {
return (nodeName: string): boolean => {
let index;
for (index in this.selectedNodes) {
if (this.selectedNodes[index].name === nodeName) {
return true;
}
}
return false;
};
},
upgradeLinkUrl() {
return async (source: string, utm_campaign: string, deploymentType: string) => {
let linkUrl = '';
const searchParams = new URLSearchParams(); const isNodeSelected = computed(() =>
selectedNodes.value.reduce((acc: { [nodeName: string]: true }, node) => {
acc[node.name] = true;
return acc;
}, {}),
);
if (deploymentType === 'cloud' && hasPermission(['instanceOwner'])) { const headerHeight = computed(() => {
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
const { code } = await useCloudPlanStore().getAutoLoginCode();
linkUrl = `https://${adminPanelHost}/login`;
searchParams.set('code', code);
searchParams.set('returnPath', '/account/change-plan');
} else {
linkUrl = N8N_PRICING_PAGE_URL;
}
if (utm_campaign) {
searchParams.set('utm_campaign', utm_campaign);
}
if (source) {
searchParams.set('source', source);
}
return `${linkUrl}?${searchParams.toString()}`;
};
},
headerHeight() {
const style = getComputedStyle(document.body); const style = getComputedStyle(document.body);
return Number(style.getPropertyValue('--header-height')); return Number(style.getPropertyValue('--header-height'));
}, });
isAnyModalOpen(): boolean {
return this.modalStack.length > 0; const isAnyModalOpen = computed(() => {
}, return modalStack.value.length > 0;
}, });
actions: {
setTheme(theme: ThemeOption): void { // Methods
this.theme = theme;
updateTheme(theme); const setTheme = (newTheme: ThemeOption): void => {
}, theme.value = newTheme;
setMode(name: keyof Modals, mode: string): void { updateTheme(newTheme);
this.modals[name] = { };
...this.modals[name],
const setMode = (name: keyof Modals, mode: string): void => {
modalsById.value[name] = {
...modalsById.value[name],
mode, mode,
}; };
}, };
setActiveId(name: keyof Modals, activeId: string): void {
this.modals[name] = { const setActiveId = (name: keyof Modals, activeId: string | null): void => {
...this.modals[name], modalsById.value[name] = {
...modalsById.value[name],
activeId, activeId,
}; };
}, };
setShowAuthSelector(name: keyof Modals, showAuthSelector: boolean) {
this.modals[name] = { const setShowAuthSelector = (name: keyof Modals, showAuthSelector: boolean): void => {
...this.modals[name], modalsById.value[name] = {
...modalsById.value[name],
showAuthSelector, showAuthSelector,
} as NewCredentialsModal; } as NewCredentialsModal;
}, };
setModalData(payload: { name: keyof Modals; data: Record<string, unknown> }) {
this.modals[payload.name] = { const setModalData = (payload: { name: keyof Modals; data: Record<string, unknown> }) => {
...this.modals[payload.name], modalsById.value[payload.name] = {
...modalsById.value[payload.name],
data: payload.data, data: payload.data,
}; };
}, };
openModal(name: keyof Modals): void {
this.modals[name] = { const openModal = (name: ModalKey) => {
...this.modals[name], modalsById.value[name] = {
...modalsById.value[name],
open: true, open: true,
}; };
this.modalStack = [name].concat(this.modalStack) as string[]; modalStack.value = [name].concat(modalStack.value) as string[];
}, };
openModalWithData(payload: { name: keyof Modals; data: Record<string, unknown> }): void {
this.setModalData(payload); const openModalWithData = (payload: { name: ModalKey; data: Record<string, unknown> }) => {
this.openModal(payload.name); setModalData(payload);
}, openModal(payload.name);
closeModal(name: keyof Modals): void { };
this.modals[name] = {
...this.modals[name], const closeModal = (name: ModalKey) => {
modalsById.value[name] = {
...modalsById.value[name],
open: false, open: false,
}; };
this.modalStack = this.modalStack.filter((openModalName: string) => { modalStack.value = modalStack.value.filter((openModalName) => name !== openModalName);
return name !== openModalName; };
});
}, const draggableStartDragging = (type: string, data: string) => {
draggableStartDragging(type: string, data: string): void { draggable.value = {
this.draggable = {
isDragging: true, isDragging: true,
type, type,
data, data,
canDrop: false, canDrop: false,
stickyPosition: null, stickyPosition: null,
}; };
}, };
draggableStopDragging(): void {
this.draggable = { const draggableStopDragging = () => {
draggable.value = {
isDragging: false, isDragging: false,
type: '', type: '',
data: '', data: '',
canDrop: false, canDrop: false,
stickyPosition: null, stickyPosition: null,
}; };
}, };
setDraggableStickyPos(position: XYPosition): void {
this.draggable = { const setDraggableStickyPos = (position: XYPosition) => {
...this.draggable, draggable.value = {
...draggable.value,
stickyPosition: position, stickyPosition: position,
}; };
}, };
setDraggableCanDrop(canDrop: boolean): void {
this.draggable = { const setDraggableCanDrop = (canDrop: boolean) => {
...this.draggable, draggable.value = {
...draggable.value,
canDrop, canDrop,
}; };
}, };
openDeleteUserModal(id: string): void {
this.setActiveId(DELETE_USER_MODAL_KEY, id); const openDeleteUserModal = (id: string) => {
this.openModal(DELETE_USER_MODAL_KEY); setActiveId(DELETE_USER_MODAL_KEY, id);
}, openModal(DELETE_USER_MODAL_KEY);
openExistingCredential(id: string): void { };
this.setActiveId(CREDENTIAL_EDIT_MODAL_KEY, id);
this.setMode(CREDENTIAL_EDIT_MODAL_KEY, 'edit'); const openExistingCredential = (id: string) => {
this.openModal(CREDENTIAL_EDIT_MODAL_KEY); setActiveId(CREDENTIAL_EDIT_MODAL_KEY, id);
}, setMode(CREDENTIAL_EDIT_MODAL_KEY, 'edit');
openNewCredential(type: string, showAuthOptions = false): void { openModal(CREDENTIAL_EDIT_MODAL_KEY);
this.setActiveId(CREDENTIAL_EDIT_MODAL_KEY, type); };
this.setShowAuthSelector(CREDENTIAL_EDIT_MODAL_KEY, showAuthOptions);
this.setMode(CREDENTIAL_EDIT_MODAL_KEY, 'new'); const openNewCredential = (type: string, showAuthOptions = false) => {
this.openModal(CREDENTIAL_EDIT_MODAL_KEY); setActiveId(CREDENTIAL_EDIT_MODAL_KEY, type);
}, setShowAuthSelector(CREDENTIAL_EDIT_MODAL_KEY, showAuthOptions);
async getNextOnboardingPrompt(): Promise<IOnboardingCallPrompt | null> { setMode(CREDENTIAL_EDIT_MODAL_KEY, 'new');
const rootStore = useRootStore(); openModal(CREDENTIAL_EDIT_MODAL_KEY);
};
const getNextOnboardingPrompt = async () => {
const instanceId = rootStore.instanceId; const instanceId = rootStore.instanceId;
const { currentUser } = useUsersStore(); const { currentUser } = userStore;
if (currentUser) { if (currentUser) {
return await fetchNextOnboardingPrompt(instanceId, currentUser); return await onboardingApi.fetchNextOnboardingPrompt(instanceId, currentUser);
} }
return null; return null;
}, };
async applyForOnboardingCall(email: string): Promise<string | null> {
const rootStore = useRootStore(); const applyForOnboardingCall = async (email: string) => {
const instanceId = rootStore.instanceId; const instanceId = rootStore.instanceId;
const { currentUser } = useUsersStore(); const { currentUser } = userStore;
if (currentUser) { if (currentUser) {
return await applyForOnboardingCall(instanceId, currentUser, email); return await onboardingApi.applyForOnboardingCall(instanceId, currentUser, email);
} }
return null; return null;
}, };
async submitContactEmail(email: string, agree: boolean): Promise<string | null> {
const rootStore = useRootStore(); const submitContactEmail = async (email: string, agree: boolean) => {
const instanceId = rootStore.instanceId; const instanceId = rootStore.instanceId;
const { currentUser } = useUsersStore(); const { currentUser } = userStore;
if (currentUser) { if (currentUser) {
return await submitEmailOnSignup( return await onboardingApi.submitEmailOnSignup(
instanceId, instanceId,
currentUser, currentUser,
email ?? currentUser?.email, email ?? currentUser.email,
agree, agree,
); );
} }
return null; return null;
}, };
openCommunityPackageUninstallConfirmModal(packageName: string) {
this.setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName); const openCommunityPackageUninstallConfirmModal = (packageName: string) => {
this.setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL); setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName);
this.openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY); setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL);
}, openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
openCommunityPackageUpdateConfirmModal(packageName: string) { };
this.setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName);
this.setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE); const openCommunityPackageUpdateConfirmModal = (packageName: string) => {
this.openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY); setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName);
}, setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE);
addActiveAction(action: string): void { openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
if (!this.activeActions.includes(action)) { };
this.activeActions.push(action);
const addActiveAction = (action: string) => {
if (!activeActions.value.includes(action)) {
activeActions.value.push(action);
} }
}, };
removeActiveAction(action: string): void {
const actionIndex = this.activeActions.indexOf(action); const removeActiveAction = (action: string) => {
const actionIndex = activeActions.value.indexOf(action);
if (actionIndex !== -1) { if (actionIndex !== -1) {
this.activeActions.splice(actionIndex, 1); activeActions.value.splice(actionIndex, 1);
} }
}, };
addSelectedNode(node: INodeUi): void {
const isAlreadySelected = this.selectedNodes.some((n) => n.name === node.name); const addSelectedNode = (node: INodeUi) => {
const isAlreadySelected = selectedNodes.value.some((n) => n.name === node.name);
if (!isAlreadySelected) { if (!isAlreadySelected) {
this.selectedNodes.push(node); selectedNodes.value.push(node);
} }
}, };
removeNodeFromSelection(node: INodeUi): void {
let index; const removeNodeFromSelection = (node: INodeUi) => {
for (index in this.selectedNodes) { for (const [index] of selectedNodes.value.entries()) {
if (this.selectedNodes[index].name === node.name) { if (selectedNodes.value[index].name === node.name) {
this.selectedNodes.splice(parseInt(index, 10), 1); selectedNodes.value.splice(Number(index), 1);
break; break;
} }
} }
}, };
resetSelectedNodes(): void {
this.selectedNodes = []; const resetSelectedNodes = () => {
}, selectedNodes.value = [];
setCurlCommand(payload: { name: string; command: string }): void { };
this.modals[payload.name] = {
...this.modals[payload.name], const setCurlCommand = (payload: { name: string; command: string }) => {
modalsById.value[payload.name] = {
...modalsById.value[payload.name],
curlCommand: payload.command, curlCommand: payload.command,
}; };
}, };
toggleSidebarMenuCollapse(): void {
this.sidebarMenuCollapsed = !this.sidebarMenuCollapsed; const toggleSidebarMenuCollapse = () => {
}, sidebarMenuCollapsed.value = !sidebarMenuCollapsed.value;
async getCurlToJson(curlCommand: string): Promise<CurlToJSONResponse> { };
const rootStore = useRootStore();
const parameters = await getCurlToJson(rootStore.restApiContext, curlCommand); const getCurlToJson = async (curlCommand: string) => {
const parameters = await curlParserApi.getCurlToJson(rootStore.restApiContext, curlCommand);
// Normalize placeholder values // Normalize placeholder values
if (parameters['parameters.url']) { if (parameters['parameters.url']) {
@@ -562,17 +560,18 @@ export const useUIStore = defineStore(STORES.UI, {
} }
return parameters; return parameters;
}, };
async goToUpgrade(
const goToUpgrade = async (
source: CloudUpdateLinkSourceType, source: CloudUpdateLinkSourceType,
utm_campaign: UTMCampaign, utm_campaign: UTMCampaign,
mode: 'open' | 'redirect' = 'open', mode: 'open' | 'redirect' = 'open',
): Promise<void> { ) => {
const { usageLeft, trialDaysLeft, userIsTrialing } = useCloudPlanStore(); const { usageLeft, trialDaysLeft, userIsTrialing } = cloudPlanStore;
const { executionsLeft, workflowsLeft } = usageLeft; const { executionsLeft, workflowsLeft } = usageLeft;
const deploymentType = useSettingsStore().deploymentType; const deploymentType = settingsStore.deploymentType;
useTelemetryStore().track('User clicked upgrade CTA', { telemetryStore.track('User clicked upgrade CTA', {
source, source,
isTrial: userIsTrialing, isTrial: userIsTrialing,
deploymentType, deploymentType,
@@ -581,51 +580,124 @@ export const useUIStore = defineStore(STORES.UI, {
workflowsLeft, workflowsLeft,
}); });
const upgradeLink = await this.upgradeLinkUrl(source, utm_campaign, deploymentType); const upgradeLink = await generateUpgradeLinkUrl(source, utm_campaign, deploymentType);
if (mode === 'open') { if (mode === 'open') {
window.open(upgradeLink, '_blank'); window.open(upgradeLink, '_blank');
} else { } else {
location.href = upgradeLink; location.href = upgradeLink;
} }
}, };
async dismissBanner(
name: BannerName, const removeBannerFromStack = (name: BannerName) => {
type: 'temporary' | 'permanent' = 'temporary', bannerStack.value = bannerStack.value.filter((bannerName) => bannerName !== name);
): Promise<void> { };
const dismissBanner = async (name: BannerName, type: 'temporary' | 'permanent' = 'temporary') => {
if (type === 'permanent') { if (type === 'permanent') {
await dismissBannerPermanently(useRootStore().restApiContext, { await dismissBannerPermanently(rootStore.restApiContext, {
bannerName: name, bannerName: name,
dismissedBanners: useSettingsStore().permanentlyDismissedBanners, dismissedBanners: settingsStore.permanentlyDismissedBanners,
}); });
this.removeBannerFromStack(name); removeBannerFromStack(name);
return; return;
} }
this.removeBannerFromStack(name); removeBannerFromStack(name);
}, };
updateBannersHeight(newHeight: number): void {
this.bannersHeight = newHeight; const updateBannersHeight = (newHeight: number) => {
}, bannersHeight.value = newHeight;
pushBannerToStack(name: BannerName) { };
if (this.bannerStack.includes(name)) return;
this.bannerStack.push(name); const pushBannerToStack = (name: BannerName) => {
}, if (bannerStack.value.includes(name)) return;
removeBannerFromStack(name: BannerName) { bannerStack.value.push(name);
this.bannerStack = this.bannerStack.filter((bannerName) => bannerName !== name); };
},
clearBannerStack() { const clearBannerStack = () => {
this.bannerStack = []; bannerStack.value = [];
}, };
getNotificationsForView(view: VIEWS): NotificationOptions[] {
return this.pendingNotificationsForViews[view] ?? []; const setNotificationsForView = (view: VIEWS, notifications: NotificationOptions[]) => {
}, pendingNotificationsForViews.value[view] = notifications;
setNotificationsForView(view: VIEWS, notifications: NotificationOptions[]) { };
this.pendingNotificationsForViews[view] = notifications;
}, const deleteNotificationsForView = (view: VIEWS) => {
deleteNotificationsForView(view: VIEWS) { delete pendingNotificationsForViews.value[view];
delete this.pendingNotificationsForViews[view]; };
},
}, return {
appliedTheme,
logo,
contextBasedTranslationKeys,
getLastSelectedNode,
isVersionsOpen,
isModalActiveById,
fakeDoorsByLocation,
isReadOnlyView,
isActionActive,
activeActions,
getSelectedNodes,
isNodeSelected,
headerHeight,
stateIsDirty,
lastSelectedNodeOutputIndex,
activeCredentialType,
lastSelectedNode,
selectedNodes,
bannersHeight,
lastSelectedNodeEndpointUuid,
nodeViewOffsetPosition,
nodeViewMoveInProgress,
nodeViewInitialized,
addFirstStepOnLoad,
isCreateNodeActive,
sidebarMenuCollapsed,
fakeDoorFeatures,
bannerStack,
theme,
modalsById,
currentView,
isAnyModalOpen,
fakeDoorsById,
pendingNotificationsForViews,
setTheme,
setMode,
setActiveId,
setShowAuthSelector,
setModalData,
openModalWithData,
openModal,
closeModal,
draggableStartDragging,
draggableStopDragging,
setDraggableStickyPos,
setDraggableCanDrop,
openDeleteUserModal,
openExistingCredential,
openNewCredential,
getNextOnboardingPrompt,
applyForOnboardingCall,
submitContactEmail,
openCommunityPackageUninstallConfirmModal,
openCommunityPackageUpdateConfirmModal,
addActiveAction,
removeActiveAction,
addSelectedNode,
removeNodeFromSelection,
resetSelectedNodes,
setCurlCommand,
toggleSidebarMenuCollapse,
getCurlToJson,
goToUpgrade,
removeBannerFromStack,
dismissBanner,
updateBannersHeight,
pushBannerToStack,
clearBannerStack,
setNotificationsForView,
deleteNotificationsForView,
};
}); });
/** /**
@@ -668,3 +740,34 @@ export const listenForModalChanges = (opts: {
}); });
}); });
}; };
export const generateUpgradeLinkUrl = async (
source: string,
utm_campaign: string,
deploymentType: string,
) => {
let linkUrl = '';
const searchParams = new URLSearchParams();
const cloudPlanStore = useCloudPlanStore();
if (deploymentType === 'cloud' && hasPermission(['instanceOwner'])) {
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
const { code } = await cloudPlanStore.getAutoLoginCode();
linkUrl = `https://${adminPanelHost}/login`;
searchParams.set('code', code);
searchParams.set('returnPath', '/account/change-plan');
} else {
linkUrl = N8N_PRICING_PAGE_URL;
}
if (utm_campaign) {
searchParams.set('utm_campaign', utm_campaign);
}
if (source) {
searchParams.set('source', source);
}
return `${linkUrl}?${searchParams.toString()}`;
};

View File

@@ -678,7 +678,7 @@ export default defineComponent({
return this.workflowsStore.getWorkflowExecution; return this.workflowsStore.getWorkflowExecution;
}, },
workflowRunning(): boolean { workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning'); return this.uiStore.isActionActive['workflowRunning'];
}, },
currentWorkflow(): string { currentWorkflow(): string {
return this.$route.params.name?.toString() || this.workflowsStore.workflowId; return this.$route.params.name?.toString() || this.workflowsStore.workflowId;

View File

@@ -23,7 +23,7 @@ export default defineComponent({
computed: { computed: {
...mapStores(useUIStore), ...mapStores(useUIStore),
featureInfo(): IFakeDoor | undefined { featureInfo(): IFakeDoor | undefined {
return this.uiStore.getFakeDoorById(this.featureId); return this.uiStore.fakeDoorsById[this.featureId];
}, },
}, },
methods: { methods: {