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