mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Implement new app layout (#10548)
This commit is contained in:
parent
d7241cfc3a
commit
95a9cd2c73
|
@ -7,7 +7,7 @@ import {
|
||||||
WorkflowSharingModal,
|
WorkflowSharingModal,
|
||||||
WorkflowsPage,
|
WorkflowsPage,
|
||||||
} from '../pages';
|
} from '../pages';
|
||||||
import { getVisibleDropdown, getVisibleSelect } from '../utils';
|
import { getVisibleDropdown, getVisiblePopper, getVisibleSelect } from '../utils';
|
||||||
import * as projects from '../composables/projects';
|
import * as projects from '../composables/projects';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -180,7 +180,8 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
||||||
).should('be.visible');
|
).should('be.visible');
|
||||||
|
|
||||||
credentialsModal.getters.usersSelect().click();
|
credentialsModal.getters.usersSelect().click();
|
||||||
cy.getByTestId('project-sharing-info')
|
getVisiblePopper()
|
||||||
|
.find('[data-test-id="project-sharing-info"]')
|
||||||
.filter(':visible')
|
.filter(':visible')
|
||||||
.should('have.length', 3)
|
.should('have.length', 3)
|
||||||
.contains(INSTANCE_ADMIN.email)
|
.contains(INSTANCE_ADMIN.email)
|
||||||
|
|
|
@ -34,7 +34,7 @@ describe('AI Assistant::enabled', () => {
|
||||||
aiAssistant.getters.chatInputWrapper().should('not.exist');
|
aiAssistant.getters.chatInputWrapper().should('not.exist');
|
||||||
aiAssistant.getters.closeChatButton().should('be.visible');
|
aiAssistant.getters.closeChatButton().should('be.visible');
|
||||||
aiAssistant.getters.closeChatButton().click();
|
aiAssistant.getters.closeChatButton().click();
|
||||||
aiAssistant.getters.askAssistantChat().should('not.exist');
|
aiAssistant.getters.askAssistantChat().should('not.be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resize assistant chat up', () => {
|
it('should resize assistant chat up', () => {
|
||||||
|
@ -162,13 +162,13 @@ describe('AI Assistant::enabled', () => {
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||||
wf.actions.openNode('Edit Fields');
|
wf.actions.openNode('Edit Fields');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||||
cy.wait('@chatRequest');
|
cy.wait('@chatRequest');
|
||||||
aiAssistant.getters.closeChatButton().click();
|
aiAssistant.getters.closeChatButton().click();
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
wf.actions.openNode('Stop and Error');
|
wf.actions.openNode('Stop and Error');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||||
// Since we already have an active session, a warning should be shown
|
// Since we already have an active session, a warning should be shown
|
||||||
aiAssistant.getters.newAssistantSessionModal().should('be.visible');
|
aiAssistant.getters.newAssistantSessionModal().should('be.visible');
|
||||||
aiAssistant.getters
|
aiAssistant.getters
|
||||||
|
|
|
@ -199,7 +199,7 @@ describe('NDV', () => {
|
||||||
.contains(key)
|
.contains(key)
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
getObjectValueItem().find('label').click();
|
getObjectValueItem().find('label').click({ force: true });
|
||||||
expandedObjectProps.forEach((key) => {
|
expandedObjectProps.forEach((key) => {
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.outputPanel()
|
.outputPanel()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted } from 'vue';
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import LoadingView from '@/views/LoadingView.vue';
|
import LoadingView from '@/views/LoadingView.vue';
|
||||||
import BannerStack from '@/components/banners/BannerStack.vue';
|
import BannerStack from '@/components/banners/BannerStack.vue';
|
||||||
|
@ -9,7 +9,7 @@ import Telemetry from '@/components/Telemetry.vue';
|
||||||
import AskAssistantFloatingButton from '@/components/AskAssistant/AskAssistantFloatingButton.vue';
|
import AskAssistantFloatingButton from '@/components/AskAssistant/AskAssistantFloatingButton.vue';
|
||||||
import { loadLanguage } from '@/plugins/i18n';
|
import { loadLanguage } from '@/plugins/i18n';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { HIRING_BANNER, VIEWS } from '@/constants';
|
import { APP_MODALS_ELEMENT_ID, HIRING_BANNER, VIEWS } from '@/constants';
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
import { useAssistantStore } from './stores/assistant.store';
|
import { useAssistantStore } from './stores/assistant.store';
|
||||||
import { useUIStore } from './stores/ui.store';
|
import { useUIStore } from './stores/ui.store';
|
||||||
|
@ -32,34 +32,56 @@ const defaultLocale = computed(() => rootStore.defaultLocale);
|
||||||
const isDemoMode = computed(() => route.name === VIEWS.DEMO);
|
const isDemoMode = computed(() => route.name === VIEWS.DEMO);
|
||||||
const showAssistantButton = computed(() => assistantStore.canShowAssistantButtons);
|
const showAssistantButton = computed(() => assistantStore.canShowAssistantButtons);
|
||||||
|
|
||||||
|
const appGrid = ref<Element | null>(null);
|
||||||
|
|
||||||
|
const assistantSidebarWidth = computed(() => assistantStore.chatWidth);
|
||||||
|
|
||||||
watch(defaultLocale, (newLocale) => {
|
watch(defaultLocale, (newLocale) => {
|
||||||
void loadLanguage(newLocale);
|
void loadLanguage(newLocale);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
logHiringBanner();
|
||||||
|
void useExternalHooks().run('app.mount');
|
||||||
|
loading.value = false;
|
||||||
|
window.addEventListener('resize', updateGridWidth);
|
||||||
|
await updateGridWidth();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateGridWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
// As assistant sidebar width changes, recalculate the total width regularly
|
||||||
|
watch(assistantSidebarWidth, async () => {
|
||||||
|
await updateGridWidth();
|
||||||
|
});
|
||||||
|
|
||||||
const logHiringBanner = () => {
|
const logHiringBanner = () => {
|
||||||
if (settingsStore.isHiringBannerEnabled && !isDemoMode.value) {
|
if (settingsStore.isHiringBannerEnabled && !isDemoMode.value) {
|
||||||
console.log(HIRING_BANNER);
|
console.log(HIRING_BANNER);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
const updateGridWidth = async () => {
|
||||||
logHiringBanner();
|
await nextTick();
|
||||||
void useExternalHooks().run('app.mount');
|
if (appGrid.value) {
|
||||||
loading.value = false;
|
uiStore.appGridWidth = appGrid.value.clientWidth;
|
||||||
});
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.app, 'root-container']">
|
|
||||||
<LoadingView v-if="loading" />
|
<LoadingView v-if="loading" />
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
id="app"
|
id="n8n-app"
|
||||||
:class="{
|
:class="{
|
||||||
[$style.container]: true,
|
[$style.container]: true,
|
||||||
[$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed,
|
[$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<div id="app-grid" ref="appGrid" :class="$style['app-grid']">
|
||||||
<div id="banners" :class="$style.banners">
|
<div id="banners" :class="$style.banners">
|
||||||
<BannerStack v-if="!isDemoMode" />
|
<BannerStack v-if="!isDemoMode" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,29 +99,37 @@ onMounted(async () => {
|
||||||
<component :is="Component" v-else />
|
<component :is="Component" v-else />
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
<AskAssistantChat />
|
<div :id="APP_MODALS_ELEMENT_ID" :class="$style.modals">
|
||||||
<Modals />
|
<Modals />
|
||||||
|
</div>
|
||||||
<Telemetry />
|
<Telemetry />
|
||||||
<AskAssistantFloatingButton v-if="showAssistantButton" />
|
<AskAssistantFloatingButton v-if="showAssistantButton" />
|
||||||
</div>
|
</div>
|
||||||
|
<AskAssistantChat />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.app {
|
// On the root level, whole app is a flex container
|
||||||
|
// with app grid and assistant sidebar as children
|
||||||
|
.container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
// App grid is the main app layout including modals and other absolute positioned elements
|
||||||
|
.app-grid {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
|
||||||
'banners banners rightsidebar'
|
|
||||||
'sidebar header rightsidebar'
|
|
||||||
'sidebar content rightsidebar';
|
|
||||||
grid-auto-columns: minmax(0, max-content) minmax(100px, auto) minmax(0, max-content);
|
|
||||||
grid-template-rows: auto fit-content($header-height) 1fr;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
flex-basis: 100%;
|
||||||
|
grid-template-areas:
|
||||||
|
'banners banners'
|
||||||
|
'sidebar header'
|
||||||
|
'sidebar content';
|
||||||
|
grid-auto-columns: minmax(0, max-content) 1fr;
|
||||||
|
grid-template-rows: auto fit-content($header-height) 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banners {
|
.banners {
|
||||||
|
@ -132,4 +162,8 @@ onMounted(async () => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modals {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
||||||
import type { ISettingsState } from '@/Interface';
|
import type { ISettingsState } from '@/Interface';
|
||||||
import { UserManagementAuthenticationMethod } from '@/Interface';
|
import { UserManagementAuthenticationMethod } from '@/Interface';
|
||||||
import { defaultSettings } from './defaults';
|
import { defaultSettings } from './defaults';
|
||||||
|
import { APP_MODALS_ELEMENT_ID } from '@/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retries the given assertion until it passes or the timeout is reached
|
* Retries the given assertion until it passes or the timeout is reached
|
||||||
|
@ -90,3 +91,20 @@ export const getSelectedDropdownValue = async (items: NodeListOf<Element>) => {
|
||||||
expect(selectedItem).toBeInTheDocument();
|
expect(selectedItem).toBeInTheDocument();
|
||||||
return selectedItem?.querySelector('p')?.textContent?.trim();
|
return selectedItem?.querySelector('p')?.textContent?.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a container for teleported modals
|
||||||
|
*
|
||||||
|
* More info: https://test-utils.vuejs.org/guide/advanced/teleport#Mounting-the-Component
|
||||||
|
* @returns {HTMLElement} appModals
|
||||||
|
*/
|
||||||
|
export const createAppModals = () => {
|
||||||
|
const appModals = document.createElement('div');
|
||||||
|
appModals.id = APP_MODALS_ELEMENT_ID;
|
||||||
|
document.body.appendChild(appModals);
|
||||||
|
return appModals;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanupAppModals = () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
};
|
||||||
|
|
|
@ -67,7 +67,7 @@ function onClose() {
|
||||||
<template>
|
<template>
|
||||||
<SlideTransition>
|
<SlideTransition>
|
||||||
<n8n-resize-wrapper
|
<n8n-resize-wrapper
|
||||||
v-if="assistantStore.isAssistantOpen"
|
v-show="assistantStore.isAssistantOpen"
|
||||||
:supported-directions="['left']"
|
:supported-directions="['left']"
|
||||||
:width="assistantStore.chatWidth"
|
:width="assistantStore.chatWidth"
|
||||||
:class="$style.container"
|
:class="$style.container"
|
||||||
|
@ -95,9 +95,9 @@ function onClose() {
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
.container {
|
.container {
|
||||||
grid-area: rightsidebar;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 99999; /* Needs to be high enough so it doesn't get covered by element-ui dialogs */
|
flex-basis: content;
|
||||||
|
z-index: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
|
|
|
@ -46,9 +46,10 @@ const startNewSession = async () => {
|
||||||
<Modal
|
<Modal
|
||||||
width="460px"
|
width="460px"
|
||||||
height="250px"
|
height="250px"
|
||||||
|
data-test-id="new-assistant-session-modal"
|
||||||
:name="NEW_ASSISTANT_SESSION_MODAL"
|
:name="NEW_ASSISTANT_SESSION_MODAL"
|
||||||
:center="true"
|
:center="true"
|
||||||
data-test-id="new-assistant-session-modal"
|
:append-to-body="true"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ i18n.baseText('aiAssistant.newSessionModal.title.part1') }}
|
{{ i18n.baseText('aiAssistant.newSessionModal.title.part1') }}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Assignment from '../Assignment.vue';
|
||||||
import { defaultSettings } from '@/__tests__/defaults';
|
import { defaultSettings } from '@/__tests__/defaults';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
const DEFAULT_SETUP = {
|
const DEFAULT_SETUP = {
|
||||||
pinia: createTestingPinia({
|
pinia: createTestingPinia({
|
||||||
|
@ -24,7 +25,12 @@ const DEFAULT_SETUP = {
|
||||||
const renderComponent = createComponentRenderer(Assignment, DEFAULT_SETUP);
|
const renderComponent = createComponentRenderer(Assignment, DEFAULT_SETUP);
|
||||||
|
|
||||||
describe('Assignment.vue', () => {
|
describe('Assignment.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { fireEvent, within } from '@testing-library/vue';
|
||||||
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
||||||
import AssignmentCollection from '../AssignmentCollection.vue';
|
import AssignmentCollection from '../AssignmentCollection.vue';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
|
|
||||||
const DEFAULT_SETUP = {
|
const DEFAULT_SETUP = {
|
||||||
pinia: createTestingPinia({
|
pinia: createTestingPinia({
|
||||||
|
@ -64,8 +64,13 @@ async function dropAssignment({
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('AssignmentCollection.vue', () => {
|
describe('AssignmentCollection.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
cleanupAppModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders empty state properly', async () => {
|
it('renders empty state properly', async () => {
|
||||||
|
|
|
@ -20,6 +20,8 @@ import { useDebounce } from '@/composables/useDebounce';
|
||||||
import DraggableTarget from './DraggableTarget.vue';
|
import DraggableTarget from './DraggableTarget.vue';
|
||||||
import { dropInEditor } from '@/plugins/codemirror/dragAndDrop';
|
import { dropInEditor } from '@/plugins/codemirror/dragAndDrop';
|
||||||
|
|
||||||
|
import { APP_MODALS_ELEMENT_ID } from '@/constants';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -122,8 +124,8 @@ async function onDrop(expression: string, event: MouseEvent) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
width="calc(100vw - var(--spacing-3xl))"
|
width="calc(100% - var(--spacing-3xl))"
|
||||||
append-to-body
|
:append-to="`#${APP_MODALS_ELEMENT_ID}`"
|
||||||
:class="$style.modal"
|
:class="$style.modal"
|
||||||
:model-value="dialogVisible"
|
:model-value="dialogVisible"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue';
|
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
@ -35,8 +35,13 @@ const DEFAULT_SETUP = {
|
||||||
const renderComponent = createComponentRenderer(FilterConditions, DEFAULT_SETUP);
|
const renderComponent = createComponentRenderer(FilterConditions, DEFAULT_SETUP);
|
||||||
|
|
||||||
describe('FilterConditions.vue', () => {
|
describe('FilterConditions.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
cleanupAppModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders default state properly', async () => {
|
it('renders default state properly', async () => {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { mapStores } from 'pinia';
|
||||||
import type { EventBus } from 'n8n-design-system';
|
import type { EventBus } from 'n8n-design-system';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import type { ModalKey } from '@/Interface';
|
import type { ModalKey } from '@/Interface';
|
||||||
|
import { APP_MODALS_ELEMENT_ID } from '@/constants';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Modal',
|
name: 'Modal',
|
||||||
|
@ -13,15 +14,19 @@ export default defineComponent({
|
||||||
...ElDialog.props,
|
...ElDialog.props,
|
||||||
name: {
|
name: {
|
||||||
type: String as PropType<ModalKey>,
|
type: String as PropType<ModalKey>,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
type: String,
|
type: String,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
eventBus: {
|
eventBus: {
|
||||||
type: Object as PropType<EventBus>,
|
type: Object as PropType<EventBus>,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
showClose: {
|
showClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -35,31 +40,39 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
beforeClose: {
|
beforeClose: {
|
||||||
type: Function,
|
type: Function,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
customClass: {
|
customClass: {
|
||||||
type: String,
|
type: String,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
center: {
|
center: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '50%',
|
default: '50%',
|
||||||
},
|
},
|
||||||
minWidth: {
|
minWidth: {
|
||||||
type: String,
|
type: [String, null] as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
type: String,
|
type: [String, null] as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: [String, null] as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
type: String,
|
type: [String, null] as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
maxHeight: {
|
maxHeight: {
|
||||||
type: String,
|
type: [String, null] as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
scrollable: {
|
scrollable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -79,23 +92,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
appendToBody: {
|
appendToBody: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
emits: { enter: null },
|
||||||
window.addEventListener('keydown', this.onWindowKeydown);
|
|
||||||
|
|
||||||
this.eventBus?.on('close', this.closeDialog);
|
|
||||||
|
|
||||||
const activeElement = document.activeElement as HTMLElement;
|
|
||||||
if (activeElement) {
|
|
||||||
activeElement.blur();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
this.eventBus?.off('close', this.closeDialog);
|
|
||||||
window.removeEventListener('keydown', this.onWindowKeydown);
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useUIStore),
|
...mapStores(useUIStore),
|
||||||
styles() {
|
styles() {
|
||||||
|
@ -117,6 +117,23 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
return styles;
|
return styles;
|
||||||
},
|
},
|
||||||
|
appModalsId() {
|
||||||
|
return `#${APP_MODALS_ELEMENT_ID}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('keydown', this.onWindowKeydown);
|
||||||
|
|
||||||
|
this.eventBus?.on('close', this.closeDialog);
|
||||||
|
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.blur();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.eventBus?.off('close', this.closeDialog);
|
||||||
|
window.removeEventListener('keydown', this.onWindowKeydown);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onWindowKeydown(event: KeyboardEvent) {
|
onWindowKeydown(event: KeyboardEvent) {
|
||||||
|
@ -175,9 +192,11 @@ export default defineComponent({
|
||||||
:close-on-click-modal="closeOnClickModal"
|
:close-on-click-modal="closeOnClickModal"
|
||||||
:close-on-press-escape="closeOnPressEscape"
|
:close-on-press-escape="closeOnPressEscape"
|
||||||
:style="styles"
|
:style="styles"
|
||||||
|
:append-to="appendToBody ? undefined : appModalsId"
|
||||||
:append-to-body="appendToBody"
|
:append-to-body="appendToBody"
|
||||||
:data-test-id="`${name}-modal`"
|
:data-test-id="`${name}-modal`"
|
||||||
:modal-class="center ? $style.center : ''"
|
:modal-class="center ? $style.center : ''"
|
||||||
|
z-index="2000"
|
||||||
>
|
>
|
||||||
<template v-if="$slots.header" #header>
|
<template v-if="$slots.header" #header>
|
||||||
<slot v-if="!loading" name="header" />
|
<slot v-if="!loading" name="header" />
|
||||||
|
|
|
@ -11,11 +11,12 @@ import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||||
import { useDebounce } from '@/composables/useDebounce';
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
import type { XYPosition } from '@/Interface';
|
import type { XYPosition } from '@/Interface';
|
||||||
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
const SIDE_MARGIN = 24;
|
const SIDE_MARGIN = 24;
|
||||||
const SIDE_PANELS_MARGIN = 80;
|
const SIDE_PANELS_MARGIN = 80;
|
||||||
const MIN_PANEL_WIDTH = 280;
|
const MIN_PANEL_WIDTH = 310;
|
||||||
const PANEL_WIDTH = 320;
|
const PANEL_WIDTH = 350;
|
||||||
const PANEL_WIDTH_LARGE = 420;
|
const PANEL_WIDTH_LARGE = 420;
|
||||||
const MIN_WINDOW_WIDTH = 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
|
const MIN_WINDOW_WIDTH = 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
|
||||||
|
|
||||||
|
@ -35,10 +36,10 @@ interface Props {
|
||||||
|
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const windowWidth = ref<number>(1);
|
|
||||||
const isDragging = ref<boolean>(false);
|
const isDragging = ref<boolean>(false);
|
||||||
const initialized = ref<boolean>(false);
|
const initialized = ref<boolean>(false);
|
||||||
|
|
||||||
|
@ -57,8 +58,6 @@ const slots = defineSlots<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTotalWidth();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Only set(or restore) initial position if `mainPanelDimensions`
|
Only set(or restore) initial position if `mainPanelDimensions`
|
||||||
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
|
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
|
||||||
|
@ -72,7 +71,6 @@ onMounted(() => {
|
||||||
restorePositionData();
|
restorePositionData();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', setTotalWidth);
|
|
||||||
emit('init', { position: mainPanelDimensions.value.relativeLeft });
|
emit('init', { position: mainPanelDimensions.value.relativeLeft });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initialized.value = true;
|
initialized.value = true;
|
||||||
|
@ -82,11 +80,12 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('resize', setTotalWidth);
|
|
||||||
ndvEventBus.off('setPositionByName', setPositionByName);
|
ndvEventBus.off('setPositionByName', setPositionByName);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(windowWidth, (width) => {
|
const containerWidth = computed(() => uiStore.appGridWidth);
|
||||||
|
|
||||||
|
watch(containerWidth, (width) => {
|
||||||
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
|
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
|
||||||
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
|
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
|
||||||
|
|
||||||
|
@ -161,14 +160,14 @@ const hasInputSlot = computed((): boolean => {
|
||||||
const inputPanelMargin = computed(() => pxToRelativeWidth(SIDE_PANELS_MARGIN));
|
const inputPanelMargin = computed(() => pxToRelativeWidth(SIDE_PANELS_MARGIN));
|
||||||
|
|
||||||
const minimumLeftPosition = computed((): number => {
|
const minimumLeftPosition = computed((): number => {
|
||||||
if (windowWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
|
if (containerWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
|
||||||
|
|
||||||
if (!hasInputSlot.value) return pxToRelativeWidth(SIDE_MARGIN);
|
if (!hasInputSlot.value) return pxToRelativeWidth(SIDE_MARGIN);
|
||||||
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
|
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const maximumRightPosition = computed((): number => {
|
const maximumRightPosition = computed((): number => {
|
||||||
if (windowWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
|
if (containerWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
|
||||||
|
|
||||||
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
|
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
|
||||||
});
|
});
|
||||||
|
@ -208,7 +207,7 @@ const hasDoubleWidth = computed((): boolean => {
|
||||||
const fixedPanelWidth = computed((): number => {
|
const fixedPanelWidth = computed((): number => {
|
||||||
const multiplier = hasDoubleWidth.value ? 2 : 1;
|
const multiplier = hasDoubleWidth.value ? 2 : 1;
|
||||||
|
|
||||||
if (windowWidth.value > 1700) {
|
if (containerWidth.value > 1700) {
|
||||||
return PANEL_WIDTH_LARGE * multiplier;
|
return PANEL_WIDTH_LARGE * multiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,15 +287,11 @@ function setPositionByName(position: 'minLeft' | 'maxRight' | 'initial') {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pxToRelativeWidth(px: number): number {
|
function pxToRelativeWidth(px: number): number {
|
||||||
return px / windowWidth.value;
|
return px / containerWidth.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function relativeWidthToPx(relativeWidth: number) {
|
function relativeWidthToPx(relativeWidth: number) {
|
||||||
return relativeWidth * windowWidth.value;
|
return relativeWidth * containerWidth.value;
|
||||||
}
|
|
||||||
|
|
||||||
function onResizeStart() {
|
|
||||||
setTotalWidth();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResizeEnd() {
|
function onResizeEnd() {
|
||||||
|
@ -357,16 +352,12 @@ function onDragEnd() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
emit('dragend', {
|
emit('dragend', {
|
||||||
windowWidth: windowWidth.value,
|
windowWidth: containerWidth.value,
|
||||||
position: mainPanelDimensions.value.relativeLeft,
|
position: mainPanelDimensions.value.relativeLeft,
|
||||||
});
|
});
|
||||||
}, 0);
|
}, 0);
|
||||||
storePositionData();
|
storePositionData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTotalWidth() {
|
|
||||||
windowWidth.value = window.innerWidth;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -390,7 +381,6 @@ function setTotalWidth() {
|
||||||
:grid-size="20"
|
:grid-size="20"
|
||||||
:supported-directions="supportedResizeDirections"
|
:supported-directions="supportedResizeDirections"
|
||||||
@resize="onResizeDebounced"
|
@resize="onResizeDebounced"
|
||||||
@resizestart="onResizeStart"
|
|
||||||
@resizeend="onResizeEnd"
|
@resizeend="onResizeEnd"
|
||||||
>
|
>
|
||||||
<div :class="$style.dragButtonContainer">
|
<div :class="$style.dragButtonContainer">
|
||||||
|
|
|
@ -12,6 +12,7 @@ import OutputPanel from './OutputPanel.vue';
|
||||||
import InputPanel from './InputPanel.vue';
|
import InputPanel from './InputPanel.vue';
|
||||||
import TriggerPanel from './TriggerPanel.vue';
|
import TriggerPanel from './TriggerPanel.vue';
|
||||||
import {
|
import {
|
||||||
|
APP_MODALS_ELEMENT_ID,
|
||||||
BASE_NODE_SURVEY_URL,
|
BASE_NODE_SURVEY_URL,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
EXECUTABLE_TRIGGER_NODE_TYPES,
|
EXECUTABLE_TRIGGER_NODE_TYPES,
|
||||||
|
@ -664,8 +665,9 @@ onBeforeUnmount(() => {
|
||||||
class="data-display-wrapper ndv-wrapper"
|
class="data-display-wrapper ndv-wrapper"
|
||||||
overlay-class="data-display-overlay"
|
overlay-class="data-display-overlay"
|
||||||
width="auto"
|
width="auto"
|
||||||
append-to-body
|
:append-to="`#${APP_MODALS_ELEMENT_ID}`"
|
||||||
data-test-id="ndv"
|
data-test-id="ndv"
|
||||||
|
z-index="1800"
|
||||||
:data-has-output-connection="hasOutputConnection"
|
:data-has-output-connection="hasOutputConnection"
|
||||||
>
|
>
|
||||||
<n8n-tooltip
|
<n8n-tooltip
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils'
|
||||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
APP_MODALS_ELEMENT_ID,
|
||||||
CORE_NODES_CATEGORY,
|
CORE_NODES_CATEGORY,
|
||||||
CUSTOM_API_CALL_KEY,
|
CUSTOM_API_CALL_KEY,
|
||||||
HTML_NODE_TYPE,
|
HTML_NODE_TYPE,
|
||||||
|
@ -1037,7 +1038,7 @@ onUpdated(async () => {
|
||||||
>
|
>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="codeEditDialogVisible"
|
:model-value="codeEditDialogVisible"
|
||||||
append-to-body
|
:append-to="`#${APP_MODALS_ELEMENT_ID}`"
|
||||||
width="80%"
|
width="80%"
|
||||||
:title="`${i18n.baseText('codeEdit.edit')} ${$locale
|
:title="`${i18n.baseText('codeEdit.edit')} ${$locale
|
||||||
.nodeText()
|
.nodeText()
|
||||||
|
|
|
@ -2,9 +2,16 @@ import { renderComponent } from '@/__tests__/render';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import ParameterInputWrapper from './ParameterInputWrapper.vue';
|
import ParameterInputWrapper from './ParameterInputWrapper.vue';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
|
|
||||||
describe('ParameterInputWrapper.vue', () => {
|
describe('ParameterInputWrapper.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
});
|
||||||
test('should resolve expression', async () => {
|
test('should resolve expression', async () => {
|
||||||
const { getByTestId } = renderComponent(ParameterInputWrapper, {
|
const { getByTestId } = renderComponent(ParameterInputWrapper, {
|
||||||
pinia: createTestingPinia({
|
pinia: createTestingPinia({
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, nextTick } from 'vue';
|
import { ref, watch, onMounted, nextTick } from 'vue';
|
||||||
import type { INodeProperties } from 'n8n-workflow';
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
import { APP_MODALS_ELEMENT_ID } from '@/constants';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dialogVisible: boolean;
|
dialogVisible: boolean;
|
||||||
|
@ -60,7 +61,7 @@ const closeDialog = () => {
|
||||||
<div v-if="dialogVisible">
|
<div v-if="dialogVisible">
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="dialogVisible"
|
:model-value="dialogVisible"
|
||||||
append-to-body
|
:append-to="`#${APP_MODALS_ELEMENT_ID}`"
|
||||||
width="80%"
|
width="80%"
|
||||||
:title="`${$locale.baseText('textEdit.edit')} ${$locale
|
:title="`${$locale.baseText('textEdit.edit')} ${$locale
|
||||||
.nodeText()
|
.nodeText()
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { getDropdownItems } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, getDropdownItems } from '@/__tests__/utils';
|
||||||
import { EnterpriseEditionFeature, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
import { EnterpriseEditionFeature, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
@ -36,6 +36,8 @@ describe('WorkflowSettingsVue', () => {
|
||||||
pinia = createPinia();
|
pinia = createPinia();
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
createAppModals();
|
||||||
|
|
||||||
workflowsStore = useWorkflowsStore();
|
workflowsStore = useWorkflowsStore();
|
||||||
settingsStore = useSettingsStore();
|
settingsStore = useSettingsStore();
|
||||||
uiStore = useUIStore();
|
uiStore = useUIStore();
|
||||||
|
@ -65,6 +67,10 @@ describe('WorkflowSettingsVue', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
server.shutdown();
|
server.shutdown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||||
import ChangePasswordModal from '@/components/ChangePasswordModal.vue';
|
import ChangePasswordModal from '@/components/ChangePasswordModal.vue';
|
||||||
import type { createPinia } from 'pinia';
|
import type { createPinia } from 'pinia';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(ChangePasswordModal);
|
const renderComponent = createComponentRenderer(ChangePasswordModal);
|
||||||
|
|
||||||
|
@ -9,9 +10,14 @@ describe('ChangePasswordModal', () => {
|
||||||
let pinia: ReturnType<typeof createPinia>;
|
let pinia: ReturnType<typeof createPinia>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
pinia = createTestingPinia({});
|
pinia = createTestingPinia({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
const wrapper = renderComponent({ pinia });
|
const wrapper = renderComponent({ pinia });
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||||
import { CHAT_EMBED_MODAL_KEY, STORES, WEBHOOK_NODE_TYPE } from '@/constants';
|
import { CHAT_EMBED_MODAL_KEY, STORES, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(ChatEmbedModal, {
|
const renderComponent = createComponentRenderer(ChatEmbedModal, {
|
||||||
props: {
|
props: {
|
||||||
|
@ -26,16 +27,22 @@ const renderComponent = createComponentRenderer(ChatEmbedModal, {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ChatEmbedModal', () => {
|
describe('ChatEmbedModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
});
|
||||||
it('should render correctly', async () => {
|
it('should render correctly', async () => {
|
||||||
const wrapper = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getByTestId('chatEmbed-modal')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabs = wrapper.container.querySelectorAll('.n8n-tabs .tab');
|
const modalContainer = getByTestId('chatEmbed-modal');
|
||||||
const activeTab = wrapper.container.querySelector('.n8n-tabs .tab.activeTab');
|
const tabs = modalContainer.querySelectorAll('.n8n-tabs .tab');
|
||||||
const editor = wrapper.container.querySelector('.cm-editor');
|
const activeTab = modalContainer.querySelector('.n8n-tabs .tab.activeTab');
|
||||||
|
const editor = modalContainer.querySelector('.cm-editor');
|
||||||
|
|
||||||
expect(tabs).toHaveLength(4);
|
expect(tabs).toHaveLength(4);
|
||||||
expect(activeTab).toBeVisible();
|
expect(activeTab).toBeVisible();
|
||||||
|
|
|
@ -3,7 +3,7 @@ import CommunityPackageInstallModal from '../CommunityPackageInstallModal.vue';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, STORES } from '@/constants';
|
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, STORES } from '@/constants';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { retry } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, retry } from '@/__tests__/utils';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CommunityPackageInstallModal, {
|
const renderComponent = createComponentRenderer(CommunityPackageInstallModal, {
|
||||||
props: {
|
props: {
|
||||||
|
@ -33,22 +33,27 @@ const renderComponent = createComponentRenderer(CommunityPackageInstallModal, {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CommunityPackageInstallModal', () => {
|
describe('CommunityPackageInstallModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
});
|
||||||
it('should disable install button until user agrees', async () => {
|
it('should disable install button until user agrees', async () => {
|
||||||
const wrapper = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
await retry(() =>
|
await retry(() => expect(getByTestId('communityPackageInstall-modal')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const installButton = wrapper.getByTestId('install-community-package-button');
|
const installButton = getByTestId('install-community-package-button');
|
||||||
|
|
||||||
expect(installButton).toBeDisabled();
|
expect(installButton).toBeDisabled();
|
||||||
|
|
||||||
await userEvent.click(wrapper.getByTestId('user-agreement-checkbox'));
|
await userEvent.click(getByTestId('user-agreement-checkbox'));
|
||||||
|
|
||||||
expect(installButton).toBeEnabled();
|
expect(installButton).toBeEnabled();
|
||||||
|
|
||||||
await userEvent.click(wrapper.getByTestId('user-agreement-checkbox'));
|
await userEvent.click(getByTestId('user-agreement-checkbox'));
|
||||||
|
|
||||||
expect(installButton).toBeDisabled();
|
expect(installButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { setupServer } from '@/__tests__/server';
|
import { setupServer } from '@/__tests__/server';
|
||||||
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
||||||
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
async function createPiniaWithActiveNode() {
|
async function createPiniaWithActiveNode() {
|
||||||
const node = mockNodes[0];
|
const node = mockNodes[0];
|
||||||
|
@ -47,7 +48,12 @@ describe('NodeDetailsView', () => {
|
||||||
server = setupServer();
|
server = setupServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -73,12 +79,10 @@ describe('NodeDetailsView', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
pinia,
|
pinia,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.ndv-wrapper')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { faker } from '@faker-js/faker';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import type { useNodeTypesStore } from '../../stores/nodeTypes.store';
|
import type { useNodeTypesStore } from '../../stores/nodeTypes.store';
|
||||||
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
let mockNdvState: Partial<ReturnType<typeof useNDVStore>>;
|
let mockNdvState: Partial<ReturnType<typeof useNDVStore>>;
|
||||||
let mockNodeTypesState: Partial<ReturnType<typeof useNodeTypesStore>>;
|
let mockNodeTypesState: Partial<ReturnType<typeof useNodeTypesStore>>;
|
||||||
|
@ -57,6 +58,11 @@ describe('ParameterInput.vue', () => {
|
||||||
mockNodeTypesState = {
|
mockNodeTypesState = {
|
||||||
allNodeTypes: [],
|
allNodeTypes: [],
|
||||||
};
|
};
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render an options parameter (select)', async () => {
|
test('should render an options parameter (select)', async () => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PersonalizationModal from '@/components/PersonalizationModal.vue';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { PERSONALIZATION_MODAL_KEY, ROLE, STORES, VIEWS } from '@/constants';
|
import { PERSONALIZATION_MODAL_KEY, ROLE, STORES, VIEWS } from '@/constants';
|
||||||
import { retry } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, retry } from '@/__tests__/utils';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { fireEvent } from '@testing-library/vue';
|
import { fireEvent } from '@testing-library/vue';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
@ -55,26 +55,32 @@ const renderComponent = createComponentRenderer(PersonalizationModal, {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PersonalizationModal.vue', () => {
|
describe('PersonalizationModal.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
it('should render correctly', async () => {
|
it('should render correctly', async () => {
|
||||||
const wrapper = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
await retry(() =>
|
await retry(() => expect(getByTestId('personalization-form')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper.container.querySelectorAll('.n8n-select').length).toEqual(5);
|
const modalContent = getByTestId('personalization-form');
|
||||||
|
expect(modalContent.querySelectorAll('.n8n-select').length).toEqual(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display new option when role is "Devops", "Engineering", "IT", or "Sales and marketing"', async () => {
|
it('should display new option when role is "Devops", "Engineering", "IT", or "Sales and marketing"', async () => {
|
||||||
const wrapper = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
await retry(() =>
|
await retry(() => expect(getByTestId('personalization-form')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const index of [3, 4, 5, 6]) {
|
for (const index of [3, 4, 5, 6]) {
|
||||||
|
const modalContent = getByTestId('personalization-form');
|
||||||
const expectFn = expect; // So we don't break @typescript-eslint/no-loop-func
|
const expectFn = expect; // So we don't break @typescript-eslint/no-loop-func
|
||||||
const select = wrapper.container.querySelectorAll('.n8n-select')[1];
|
const select = modalContent.querySelectorAll('.n8n-select')[1];
|
||||||
|
|
||||||
await fireEvent.click(select);
|
await fireEvent.click(select);
|
||||||
|
|
||||||
|
@ -83,27 +89,27 @@ describe('PersonalizationModal.vue', () => {
|
||||||
await fireEvent.click(item);
|
await fireEvent.click(item);
|
||||||
|
|
||||||
await retry(() => {
|
await retry(() => {
|
||||||
expectFn(wrapper.container.querySelectorAll('.n8n-select').length).toEqual(6);
|
expectFn(modalContent.querySelectorAll('.n8n-select').length).toEqual(6);
|
||||||
expectFn(wrapper.container.querySelector('[name^="automationGoal"]')).toBeInTheDocument();
|
expectFn(modalContent.querySelector('[name^="automationGoal"]')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display self serve trial option when company size is larger than 500', async () => {
|
it('should display self serve trial option when company size is larger than 500', async () => {
|
||||||
const wrapper = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
await retry(() =>
|
await retry(() => expect(getByTestId('personalization-form')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const select = wrapper.container.querySelectorAll('.n8n-select')[3];
|
const modalContent = getByTestId('personalization-form');
|
||||||
|
|
||||||
|
const select = modalContent.querySelectorAll('.n8n-select')[3];
|
||||||
await fireEvent.click(select);
|
await fireEvent.click(select);
|
||||||
|
|
||||||
const item = select.querySelectorAll('.el-select-dropdown__item')[3];
|
const item = select.querySelectorAll('.el-select-dropdown__item')[3];
|
||||||
await fireEvent.click(item);
|
await fireEvent.click(item);
|
||||||
|
|
||||||
await retry(() => {
|
await retry(() => {
|
||||||
expect(wrapper.container.querySelector('.card')).not.toBeNull();
|
expect(modalContent.querySelector('.card')).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -114,23 +120,23 @@ describe('PersonalizationModal.vue', () => {
|
||||||
const usageStore = useUsageStore(pinia);
|
const usageStore = useUsageStore(pinia);
|
||||||
const spyLicenseTrial = vi.spyOn(usageStore, 'requestEnterpriseLicenseTrial');
|
const spyLicenseTrial = vi.spyOn(usageStore, 'requestEnterpriseLicenseTrial');
|
||||||
|
|
||||||
const wrapper = renderComponent();
|
const { getByTestId, getByRole } = renderComponent();
|
||||||
|
|
||||||
await retry(() =>
|
await retry(() => expect(getByTestId('personalization-form')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const select = wrapper.container.querySelectorAll('.n8n-select')[3];
|
const modalContent = getByTestId('personalization-form');
|
||||||
|
|
||||||
|
const select = modalContent.querySelectorAll('.n8n-select')[3];
|
||||||
await fireEvent.click(select);
|
await fireEvent.click(select);
|
||||||
|
|
||||||
const item = select.querySelectorAll('.el-select-dropdown__item')[3];
|
const item = select.querySelectorAll('.el-select-dropdown__item')[3];
|
||||||
await fireEvent.click(item);
|
await fireEvent.click(item);
|
||||||
|
|
||||||
const agreeCheckbox = wrapper.container.querySelector('.n8n-checkbox');
|
const agreeCheckbox = modalContent.querySelector('.n8n-checkbox');
|
||||||
assert(agreeCheckbox);
|
assert(agreeCheckbox);
|
||||||
await fireEvent.click(agreeCheckbox);
|
await fireEvent.click(agreeCheckbox);
|
||||||
|
|
||||||
const submitButton = wrapper.getByRole('button');
|
const submitButton = getByRole('button');
|
||||||
await userEvent.click(submitButton);
|
await userEvent.click(submitButton);
|
||||||
|
|
||||||
await retry(() => expect(spyLicenseTrial).toHaveBeenCalled());
|
await retry(() => expect(spyLicenseTrial).toHaveBeenCalled());
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
UPDATED_SCHEMA,
|
UPDATED_SCHEMA,
|
||||||
} from './utils/ResourceMapper.utils';
|
} from './utils/ResourceMapper.utils';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { waitAllPromises } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, waitAllPromises } from '@/__tests__/utils';
|
||||||
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
@ -23,8 +23,13 @@ describe('ResourceMapper.vue', () => {
|
||||||
.mockResolvedValue(MAPPING_COLUMNS_RESPONSE);
|
.mockResolvedValue(MAPPING_COLUMNS_RESPONSE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
cleanupAppModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders default configuration properly', async () => {
|
it('renders default configuration properly', async () => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { setupServer } from '@/__tests__/server';
|
import { setupServer } from '@/__tests__/server';
|
||||||
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
||||||
|
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
const connections: IConnections = {
|
const connections: IConnections = {
|
||||||
'Chat Trigger': {
|
'Chat Trigger': {
|
||||||
|
@ -74,7 +75,12 @@ describe('WorkflowLMChatModal', () => {
|
||||||
server = setupServer();
|
server = setupServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,31 +89,27 @@ describe('WorkflowLMChatModal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render correctly', async () => {
|
it('should render correctly', async () => {
|
||||||
const wrapper = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
pinia: await createPiniaWithAINodes(),
|
pinia: await createPiniaWithAINodes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper.getByTestId('workflow-lm-chat-dialog')).toBeInTheDocument();
|
expect(getByTestId('workflow-lm-chat-dialog')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send and display chat message', async () => {
|
it('should send and display chat message', async () => {
|
||||||
const wrapper = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
pinia: await createPiniaWithAINodes({
|
pinia: await createPiniaWithAINodes({
|
||||||
withConnections: true,
|
withConnections: true,
|
||||||
withAgentNode: true,
|
withAgentNode: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
|
||||||
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const chatDialog = wrapper.getByTestId('workflow-lm-chat-dialog');
|
const chatDialog = getByTestId('workflow-lm-chat-dialog');
|
||||||
const chatInputsContainer = wrapper.getByTestId('lm-chat-inputs');
|
const chatInputsContainer = getByTestId('lm-chat-inputs');
|
||||||
const chatSendButton = chatInputsContainer.querySelector('.chat-input-send-button');
|
const chatSendButton = chatInputsContainer.querySelector('.chat-input-send-button');
|
||||||
const chatInput = chatInputsContainer.querySelector('textarea');
|
const chatInput = chatInputsContainer.querySelector('textarea');
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ export interface NotificationErrorWithNodeAndDescription extends ApplicationErro
|
||||||
const messageDefaults: Partial<Omit<NotificationOptions, 'message'>> = {
|
const messageDefaults: Partial<Omit<NotificationOptions, 'message'>> = {
|
||||||
dangerouslyUseHTMLString: false,
|
dangerouslyUseHTMLString: false,
|
||||||
position: 'bottom-right',
|
position: 'bottom-right',
|
||||||
zIndex: 3000, // above NDV and chat window
|
zIndex: 1900, // above NDV and below the modals
|
||||||
offset: 64,
|
offset: 64,
|
||||||
appendTo: '#node-view-root',
|
appendTo: '#node-view-root',
|
||||||
customClass: 'content-toast',
|
customClass: 'content-toast',
|
||||||
|
|
|
@ -575,7 +575,7 @@ export const EnterpriseEditionFeature: Record<
|
||||||
AdvancedPermissions: 'advancedPermissions',
|
AdvancedPermissions: 'advancedPermissions',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
export const MAIN_NODE_PANEL_WIDTH = 390;
|
||||||
|
|
||||||
export const enum MAIN_HEADER_TABS {
|
export const enum MAIN_HEADER_TABS {
|
||||||
WORKFLOW = 'workflow',
|
WORKFLOW = 'workflow',
|
||||||
|
@ -867,3 +867,5 @@ export const CanvasNodeHandleKey =
|
||||||
|
|
||||||
/** Auth */
|
/** Auth */
|
||||||
export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
|
export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
|
||||||
|
|
||||||
|
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
background-color: var(--color-dialog-overlay-background-dark);
|
background-color: var(--color-dialog-overlay-background-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#app-modals .el-overlay {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
box-shadow: 0px 6px 16px rgb(68 28 23 / 6%);
|
box-shadow: 0px 6px 16px rgb(68 28 23 / 6%);
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { usePostHog } from './posthog.store';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useUIStore } from './ui.store';
|
||||||
|
|
||||||
export const MAX_CHAT_WIDTH = 425;
|
export const MAX_CHAT_WIDTH = 425;
|
||||||
export const MIN_CHAT_WIDTH = 250;
|
export const MIN_CHAT_WIDTH = 250;
|
||||||
|
@ -42,6 +43,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
const chatMessages = ref<ChatUI.AssistantMessage[]>([]);
|
const chatMessages = ref<ChatUI.AssistantMessage[]>([]);
|
||||||
const chatWindowOpen = ref<boolean>(false);
|
const chatWindowOpen = ref<boolean>(false);
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const streaming = ref<boolean>();
|
const streaming = ref<boolean>();
|
||||||
|
@ -117,13 +119,20 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
suggestions.value = {};
|
suggestions.value = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeChat() {
|
// As assistant sidebar opens and closes, use window width to calculate the container width
|
||||||
chatWindowOpen.value = false;
|
// This will prevent animation race conditions from making ndv twitchy
|
||||||
}
|
|
||||||
|
|
||||||
function openChat() {
|
function openChat() {
|
||||||
chatWindowOpen.value = true;
|
chatWindowOpen.value = true;
|
||||||
chatMessages.value = chatMessages.value.map((msg) => ({ ...msg, read: true }));
|
chatMessages.value = chatMessages.value.map((msg) => ({ ...msg, read: true }));
|
||||||
|
uiStore.appGridWidth = window.innerWidth - chatWidth.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeChat() {
|
||||||
|
chatWindowOpen.value = false;
|
||||||
|
// Looks smoother if we wait for slide animation to finish before updating the grid width
|
||||||
|
setTimeout(() => {
|
||||||
|
uiStore.appGridWidth = window.innerWidth;
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAssistantMessages(assistantMessages: ChatRequest.MessageResponse[], id: string) {
|
function addAssistantMessages(assistantMessages: ChatRequest.MessageResponse[], id: string) {
|
||||||
|
|
|
@ -192,6 +192,8 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||||
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
|
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
|
||||||
const isCreateNodeActive = ref<boolean>(false);
|
const isCreateNodeActive = ref<boolean>(false);
|
||||||
|
|
||||||
|
const appGridWidth = ref<number>(0);
|
||||||
|
|
||||||
// Last interacted with - Canvas v2 specific
|
// Last interacted with - Canvas v2 specific
|
||||||
const lastInteractedWithNodeConnection = ref<Connection | null>(null);
|
const lastInteractedWithNodeConnection = ref<Connection | null>(null);
|
||||||
const lastInteractedWithNodeHandle = ref<string | null>(null);
|
const lastInteractedWithNodeHandle = ref<string | null>(null);
|
||||||
|
@ -623,6 +625,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
appGridWidth,
|
||||||
appliedTheme,
|
appliedTheme,
|
||||||
logo,
|
logo,
|
||||||
contextBasedTranslationKeys,
|
contextBasedTranslationKeys,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { within } from '@testing-library/vue';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { getDropdownItems } from '@/__tests__/utils';
|
import { cleanupAppModals, createAppModals, getDropdownItems } from '@/__tests__/utils';
|
||||||
import ModalRoot from '@/components/ModalRoot.vue';
|
import ModalRoot from '@/components/ModalRoot.vue';
|
||||||
import DeleteUserModal from '@/components/DeleteUserModal.vue';
|
import DeleteUserModal from '@/components/DeleteUserModal.vue';
|
||||||
import SettingsUsersView from '@/views/SettingsUsersView.vue';
|
import SettingsUsersView from '@/views/SettingsUsersView.vue';
|
||||||
|
@ -55,6 +55,8 @@ describe('SettingsUsersView', () => {
|
||||||
usersStore = useUsersStore();
|
usersStore = useUsersStore();
|
||||||
rbacStore = useRBACStore();
|
rbacStore = useRBACStore();
|
||||||
|
|
||||||
|
createAppModals();
|
||||||
|
|
||||||
useSettingsStore().settings.enterprise = {
|
useSettingsStore().settings.enterprise = {
|
||||||
...defaultSettings.enterprise,
|
...defaultSettings.enterprise,
|
||||||
[EnterpriseEditionFeature.AdvancedExecutionFilters]: true,
|
[EnterpriseEditionFeature.AdvancedExecutionFilters]: true,
|
||||||
|
@ -71,6 +73,10 @@ describe('SettingsUsersView', () => {
|
||||||
usersStore.currentUserId = loggedInUser.id;
|
usersStore.currentUserId = loggedInUser.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupAppModals();
|
||||||
|
});
|
||||||
|
|
||||||
it('should show confirmation modal before deleting user and delete with transfer', async () => {
|
it('should show confirmation modal before deleting user and delete with transfer', async () => {
|
||||||
const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {});
|
const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue