diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index dbc613bd64..8ce3bc4080 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -15,7 +15,7 @@ import { TRELLO_NODE_NAME, } from '../constants'; import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages'; -import { successToast } from '../pages/notifications'; +import { errorToast, successToast } from '../pages/notifications'; import { getVisibleSelect } from '../utils'; const credentialsPage = new CredentialsPage(); @@ -278,4 +278,25 @@ describe('Credentials', () => { credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); nodeDetailsView.getters.copyInput().should('not.exist'); }); + + it('ADO-2583 should show notifications above credential modal overlay', () => { + // check error notifications because they are sticky + cy.intercept('POST', '/rest/credentials', { forceNetworkError: true }); + credentialsPage.getters.createCredentialButton().click(); + + credentialsModal.getters.newCredentialModal().should('be.visible'); + credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); + credentialsModal.getters.newCredentialTypeOption('Notion API').click(); + + credentialsModal.getters.newCredentialTypeButton().click(); + credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); + + credentialsModal.actions.setName('My awesome Notion account'); + credentialsModal.getters.saveButton().click({ force: true }); + errorToast().should('have.length', 1); + errorToast().should('be.visible'); + + errorToast().should('have.css', 'z-index', '2100'); + cy.get('.el-overlay').should('have.css', 'z-index', '2001'); + }); }); diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index 4c849fb5d9..77d371855c 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -16,6 +16,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useHistoryHelper } from '@/composables/useHistoryHelper'; +import { useStyles } from './composables/useStyles'; const route = useRoute(); const rootStore = useRootStore(); @@ -24,6 +25,8 @@ const uiStore = useUIStore(); const usersStore = useUsersStore(); const settingsStore = useSettingsStore(); +const { setAppZIndexes } = useStyles(); + // Initialize undo/redo useHistoryHelper(route); @@ -41,6 +44,7 @@ watch(defaultLocale, (newLocale) => { }); onMounted(async () => { + setAppZIndexes(); logHiringBanner(); void useExternalHooks().run('app.mount'); loading.value = false; @@ -134,7 +138,7 @@ const updateGridWidth = async () => { .banners { grid-area: banners; - z-index: 999; + z-index: var(--z-index-top-banners); } .content { @@ -154,13 +158,13 @@ const updateGridWidth = async () => { .header { grid-area: header; - z-index: 99; + z-index: var(--z-index-app-header); } .sidebar { grid-area: sidebar; height: 100%; - z-index: 999; + z-index: var(--z-index-app-sidebar); } .modals { diff --git a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue index bef68236f8..db4d8ee105 100644 --- a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue +++ b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue @@ -106,7 +106,7 @@ function onClose() { .container { height: 100%; flex-basis: content; - z-index: 300; + z-index: var(--z-index-ask-assistant-chat); } .wrapper { diff --git a/packages/editor-ui/src/components/AskAssistant/AskAssistantFloatingButton.vue b/packages/editor-ui/src/components/AskAssistant/AskAssistantFloatingButton.vue index b147ecca9f..70f6ed84fd 100644 --- a/packages/editor-ui/src/components/AskAssistant/AskAssistantFloatingButton.vue +++ b/packages/editor-ui/src/components/AskAssistant/AskAssistantFloatingButton.vue @@ -1,5 +1,6 @@ - - - $emit('click')" - @mouseover="showTooltip = true" - @mouseleave="showTooltip = false" - > - - - - - - - - - - - - - - - - {{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }} - - - - - {{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }} - - - - - diff --git a/packages/editor-ui/src/components/Modal.vue b/packages/editor-ui/src/components/Modal.vue index 3877b8dc53..3f98e2a023 100644 --- a/packages/editor-ui/src/components/Modal.vue +++ b/packages/editor-ui/src/components/Modal.vue @@ -5,6 +5,7 @@ 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'; +import { useStyles } from '@/composables/useStyles'; const props = withDefaults( defineProps<{ @@ -50,6 +51,8 @@ const props = withDefaults( const emit = defineEmits<{ enter: [] }>(); +const { APP_Z_INDEXES } = useStyles(); + const styles = computed(() => { const styles: { [prop: string]: string } = {}; if (props.height) { @@ -143,7 +146,7 @@ function getCustomClass() { :append-to-body="appendToBody" :data-test-id="`${name}-modal`" :modal-class="center ? $style.center : ''" - :z-index="2000" + :z-index="APP_Z_INDEXES.MODALS" > diff --git a/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue b/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue index d93e83ebfa..82767f964d 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue @@ -173,7 +173,7 @@ onBeforeUnmount(() => { top: $header-height; bottom: 0; right: 0; - z-index: 200; + z-index: var(--z-index-node-creator); width: $node-creator-width; color: $node-creator-text-color; } diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index f66800e384..8294ff2f24 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -36,6 +36,7 @@ import { usePinnedData } from '@/composables/usePinnedData'; import { useTelemetry } from '@/composables/useTelemetry'; import { useI18n } from '@/composables/useI18n'; import { storeToRefs } from 'pinia'; +import { useStyles } from '@/composables/useStyles'; const emit = defineEmits<{ saveKeyboardShortcut: [event: KeyboardEvent]; @@ -73,6 +74,7 @@ const deviceSupport = useDeviceSupport(); const telemetry = useTelemetry(); const i18n = useI18n(); const message = useMessage(); +const { APP_Z_INDEXES } = useStyles(); const settingsEventBus = createEventBus(); const redrawRequired = ref(false); @@ -668,7 +670,7 @@ onBeforeUnmount(() => { width="auto" :append-to="`#${APP_MODALS_ELEMENT_ID}`" data-test-id="ndv" - :z-index="1800" + :z-index="APP_Z_INDEXES.NDV" :data-has-output-connection="hasOutputConnection" > (false); const isTouchActive = ref(false); @@ -136,7 +138,9 @@ const stickySize = computed(() => ({ const stickyPosition = computed(() => ({ left: position.value[0] + 'px', top: position.value[1] + 'px', - zIndex: props.isActive ? 9999999 : -1 * Math.floor((height.value * width.value) / 1000), + zIndex: props.isActive + ? APP_Z_INDEXES.ACTIVE_STICKY + : -1 * Math.floor((height.value * width.value) / 1000), })); const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning); diff --git a/packages/editor-ui/src/components/WorkflowPreview.vue b/packages/editor-ui/src/components/WorkflowPreview.vue index 4f295b924d..ff617499ff 100644 --- a/packages/editor-ui/src/components/WorkflowPreview.vue +++ b/packages/editor-ui/src/components/WorkflowPreview.vue @@ -242,7 +242,7 @@ watch( left: 0; height: 100%; width: 100%; - z-index: 9999999; + z-index: var(--z-index-workflow-preview-ndv); } .spinner { diff --git a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts index 6beaf33d60..83c1edbe42 100644 --- a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts +++ b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts @@ -7,6 +7,7 @@ import { getMousePosition, getRelativePosition } from '@/utils/nodeViewUtils'; import { ref, computed } from 'vue'; import { useCanvasStore } from '@/stores/canvas.store'; import { useContextMenu } from './useContextMenu'; +import { useStyles } from './useStyles'; interface ExtendedHTMLSpanElement extends HTMLSpanElement { x: number; @@ -22,6 +23,7 @@ export default function useCanvasMouseSelect() { const canvasStore = useCanvasStore(); const workflowsStore = useWorkflowsStore(); const { isOpen: isContextMenuOpen } = useContextMenu(); + const { APP_Z_INDEXES } = useStyles(); function _setSelectBoxStyle(styles: Record) { Object.assign(selectBox.value.style, styles); @@ -106,7 +108,7 @@ export default function useCanvasMouseSelect() { border: '2px dotted #FF0000', // Positioned absolutely within #node-view. This is consistent with how nodes are positioned. position: 'absolute', - zIndex: '100', + zIndex: `${APP_Z_INDEXES.SELECT_BOX}`, visibility: 'hidden', }); diff --git a/packages/editor-ui/src/composables/useStyles.spec.ts b/packages/editor-ui/src/composables/useStyles.spec.ts new file mode 100644 index 0000000000..2e27979b3e --- /dev/null +++ b/packages/editor-ui/src/composables/useStyles.spec.ts @@ -0,0 +1,25 @@ +import { useStyles } from './useStyles'; + +describe('useStyles', () => { + it('sets z-index as css variables', () => { + vi.spyOn(global.document.documentElement.style, 'setProperty'); + + const { setAppZIndexes } = useStyles(); + + setAppZIndexes(); + + expect(global.document.documentElement.style.setProperty).toHaveBeenNthCalledWith( + 1, + '--z-index-app-header', + '99', + ); + expect(global.document.documentElement.style.setProperty).toHaveBeenCalledWith( + '--z-index-canvas-add-button', + '101', + ); + expect(global.document.documentElement.style.setProperty).toHaveBeenLastCalledWith( + '--z-index-workflow-preview-ndv', + '9999999', + ); + }); +}); diff --git a/packages/editor-ui/src/composables/useStyles.ts b/packages/editor-ui/src/composables/useStyles.ts new file mode 100644 index 0000000000..dc1db3ade3 --- /dev/null +++ b/packages/editor-ui/src/composables/useStyles.ts @@ -0,0 +1,31 @@ +const APP_Z_INDEXES = { + APP_HEADER: 99, + SELECT_BOX: 100, + CANVAS_ADD_BUTTON: 101, + NODE_CREATOR: 200, + ASK_ASSISTANT_CHAT: 300, + APP_SIDEBAR: 999, + CANVAS_SELECT_BOX: 100, + TOP_BANNERS: 999, + NDV: 1800, + MODALS: 2000, + TOASTS: 2100, + ASK_ASSISTANT_FLOATING_BUTTON: 3000, + ASK_ASSISTANT_FLOATING_BUTTON_TOOLTIP: 3000, + DRAGGABLE: 9999999, + ACTIVE_STICKY: 9999999, + WORKFLOW_PREVIEW_NDV: 9999999, +} as const; + +const setAppZIndexes = () => { + Object.keys(APP_Z_INDEXES).forEach((key) => { + const variableName = `--z-index-${key.toLowerCase().replaceAll('_', '-')}`; + const value = APP_Z_INDEXES[key as keyof typeof APP_Z_INDEXES]; + document.documentElement.style.setProperty(variableName, `${value}`); + }); +}; + +export const useStyles = () => ({ + APP_Z_INDEXES, + setAppZIndexes, +}); diff --git a/packages/editor-ui/src/composables/useToast.ts b/packages/editor-ui/src/composables/useToast.ts index 1ee8f4a2b4..7d07cdf249 100644 --- a/packages/editor-ui/src/composables/useToast.ts +++ b/packages/editor-ui/src/composables/useToast.ts @@ -9,6 +9,7 @@ import { useI18n } from './useI18n'; import { useExternalHooks } from './useExternalHooks'; import { VIEWS } from '@/constants'; import type { ApplicationError } from 'n8n-workflow'; +import { useStyles } from './useStyles'; export interface NotificationErrorWithNodeAndDescription extends ApplicationError { node: { @@ -17,15 +18,6 @@ export interface NotificationErrorWithNodeAndDescription extends ApplicationErro description: string; } -const messageDefaults: Partial> = { - dangerouslyUseHTMLString: false, - position: 'bottom-right', - zIndex: 1900, // above NDV and below the modals - offset: 64, - appendTo: '#app-grid', - customClass: 'content-toast', -}; - const stickyNotificationQueue: NotificationHandle[] = []; export function useToast() { @@ -34,6 +26,16 @@ export function useToast() { const uiStore = useUIStore(); const externalHooks = useExternalHooks(); const i18n = useI18n(); + const { APP_Z_INDEXES } = useStyles(); + + const messageDefaults: Partial> = { + dangerouslyUseHTMLString: false, + position: 'bottom-right', + zIndex: APP_Z_INDEXES.TOASTS, // above NDV and modal overlays + offset: 64, + appendTo: '#app-grid', + customClass: 'content-toast', + }; function showMessage(messageData: Partial, track = true) { const { message, title } = messageData; diff --git a/packages/editor-ui/src/views/CanvasAddButton.vue b/packages/editor-ui/src/views/CanvasAddButton.vue index ff7b37b4e3..108cf321e8 100644 --- a/packages/editor-ui/src/views/CanvasAddButton.vue +++ b/packages/editor-ui/src/views/CanvasAddButton.vue @@ -55,7 +55,7 @@ const containerCssVars = computed(() => ({ left: var(--trigger-placeholder-left-position); // We have to increase z-index to make sure it's higher than selecting box in NodeView // otherwise the clicks wouldn't register - z-index: 101; + z-index: var(--z-index-canvas-add-button); &:hover .button svg path { fill: var(--color-primary);