mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-12 15:44:06 -08:00
fix(editor): Ensure toasts show above modal overlays (#11410)
Some checks failed
Test Master / install-and-build (push) Has been cancelled
Test Master / Unit tests (18.x) (push) Has been cancelled
Test Master / Unit tests (20.x) (push) Has been cancelled
Test Master / Unit tests (22.4) (push) Has been cancelled
Test Master / Lint (push) Has been cancelled
Test Master / Notify Slack on failure (push) Has been cancelled
Some checks failed
Test Master / install-and-build (push) Has been cancelled
Test Master / Unit tests (18.x) (push) Has been cancelled
Test Master / Unit tests (20.x) (push) Has been cancelled
Test Master / Unit tests (22.4) (push) Has been cancelled
Test Master / Lint (push) Has been cancelled
Test Master / Notify Slack on failure (push) Has been cancelled
This commit is contained in:
parent
d6aaeea2ab
commit
351134f786
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -106,7 +106,7 @@ function onClose() {
|
|||
.container {
|
||||
height: 100%;
|
||||
flex-basis: content;
|
||||
z-index: 300;
|
||||
z-index: var(--z-index-ask-assistant-chat);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useStyles } from '@/composables/useStyles';
|
||||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
import AssistantAvatar from 'n8n-design-system/components/AskAssistantAvatar/AssistantAvatar.vue';
|
||||
import AskAssistantButton from 'n8n-design-system/components/AskAssistantButton/AskAssistantButton.vue';
|
||||
|
@ -7,6 +8,7 @@ import { computed } from 'vue';
|
|||
|
||||
const assistantStore = useAssistantStore();
|
||||
const i18n = useI18n();
|
||||
const { APP_Z_INDEXES } = useStyles();
|
||||
|
||||
const lastUnread = computed(() => {
|
||||
const msg = assistantStore.lastUnread;
|
||||
|
@ -39,7 +41,7 @@ const onClick = () => {
|
|||
data-test-id="ask-assistant-floating-button"
|
||||
>
|
||||
<n8n-tooltip
|
||||
:z-index="4000"
|
||||
:z-index="APP_Z_INDEXES.ASK_ASSISTANT_FLOATING_BUTTON_TOOLTIP"
|
||||
placement="top"
|
||||
:visible="!!lastUnread"
|
||||
:popper-class="$style.tooltip"
|
||||
|
@ -61,7 +63,7 @@ const onClick = () => {
|
|||
position: absolute;
|
||||
bottom: var(--spacing-s);
|
||||
right: var(--spacing-s);
|
||||
z-index: 3000;
|
||||
z-index: var(--z-index-ask-assistant-floating-button);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { type StyleValue, defineComponent, type PropType } from 'vue';
|
||||
|
||||
import type { ITemplatesNode } from '@/Interface';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useRootStore } from '@/stores/root.store';
|
||||
|
||||
interface NodeIconData {
|
||||
type: string;
|
||||
path?: string;
|
||||
icon?: string;
|
||||
fileExtension?: string;
|
||||
fileBuffer?: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HoverableNodeIcon',
|
||||
props: {
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clickButton: {
|
||||
type: Function,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nodeType: {
|
||||
type: Object as PropType<INodeTypeDescription>,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore),
|
||||
fontStyleData(): object {
|
||||
return {
|
||||
'max-width': this.size + 'px',
|
||||
};
|
||||
},
|
||||
iconStyleData(): StyleValue {
|
||||
const nodeType = this.nodeType;
|
||||
const nodeTypeColor = nodeType?.defaults?.color;
|
||||
const color = typeof nodeTypeColor === 'string' ? nodeTypeColor : '';
|
||||
|
||||
if (!this.size) {
|
||||
return { color };
|
||||
}
|
||||
|
||||
return {
|
||||
color,
|
||||
width: this.size + 'px',
|
||||
height: this.size + 'px',
|
||||
'font-size': this.size + 'px',
|
||||
'line-height': this.size + 'px',
|
||||
'border-radius': this.circle ? '50%' : '2px',
|
||||
...(this.disabled && {
|
||||
color: 'var(--color-text-light)',
|
||||
'-webkit-filter': 'contrast(40%) brightness(1.5) grayscale(100%)',
|
||||
filter: 'contrast(40%) brightness(1.5) grayscale(100%)',
|
||||
}),
|
||||
};
|
||||
},
|
||||
imageStyleData(): StyleValue {
|
||||
return {
|
||||
width: '100%',
|
||||
'max-width': '100%',
|
||||
'max-height': '100%',
|
||||
};
|
||||
},
|
||||
nodeIconData(): null | NodeIconData {
|
||||
const nodeType = this.nodeType as INodeTypeDescription | ITemplatesNode | null;
|
||||
if (nodeType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((nodeType as ITemplatesNode).iconData) {
|
||||
return (nodeType as ITemplatesNode).iconData;
|
||||
}
|
||||
|
||||
const restUrl = this.rootStore.restUrl;
|
||||
|
||||
if (typeof nodeType.icon === 'string') {
|
||||
const [type, path] = nodeType.icon.split(':');
|
||||
const returnData: NodeIconData = {
|
||||
type,
|
||||
path,
|
||||
};
|
||||
|
||||
if (type === 'file') {
|
||||
returnData.path = restUrl + '/node-icon/' + nodeType.name;
|
||||
returnData.fileExtension = path.split('.').slice(-1).join();
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTooltip: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="$style.wrapper"
|
||||
:style="iconStyleData"
|
||||
@click="() => $emit('click')"
|
||||
@mouseover="showTooltip = true"
|
||||
@mouseleave="showTooltip = false"
|
||||
>
|
||||
<div :class="$style.tooltip">
|
||||
<n8n-tooltip placement="top" :visible="showTooltip">
|
||||
<template #content>
|
||||
<div v-text="nodeType.displayName"></div>
|
||||
</template>
|
||||
<span />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<div v-if="nodeIconData !== null" :class="$style.icon" title="">
|
||||
<div :class="$style.iconWrapper" :style="iconStyleData">
|
||||
<div v-if="nodeIconData !== null" :class="$style.icon">
|
||||
<img
|
||||
v-if="nodeIconData.type === 'file'"
|
||||
:src="nodeIconData.fileBuffer || nodeIconData.path"
|
||||
:style="imageStyleData"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
:icon="nodeIconData.icon || nodeIconData.path"
|
||||
:style="fontStyleData"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="node-icon-placeholder">
|
||||
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.placeholder">
|
||||
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
cursor: pointer;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
left: 10px;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
|
@ -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"
|
||||
>
|
||||
<template v-if="$slots.header" #header>
|
||||
<slot v-if="!loading" name="header" />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
<n8n-tooltip
|
||||
|
|
|
@ -20,6 +20,7 @@ import { assert } from '@/utils/assert';
|
|||
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||
import { useNodeBase } from '@/composables/useNodeBase';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useStyles } from '@/composables/useStyles';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -54,6 +55,7 @@ const ndvStore = useNDVStore();
|
|||
const nodeTypesStore = useNodeTypesStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const { APP_Z_INDEXES } = useStyles();
|
||||
|
||||
const isResizing = ref<boolean>(false);
|
||||
const isTouchActive = ref<boolean>(false);
|
||||
|
@ -136,7 +138,9 @@ const stickySize = computed<StyleValue>(() => ({
|
|||
const stickyPosition = computed<StyleValue>(() => ({
|
||||
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);
|
||||
|
|
|
@ -242,7 +242,7 @@ watch(
|
|||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 9999999;
|
||||
z-index: var(--z-index-workflow-preview-ndv);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
|
|
|
@ -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<string, string>) {
|
||||
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',
|
||||
});
|
||||
|
||||
|
|
25
packages/editor-ui/src/composables/useStyles.spec.ts
Normal file
25
packages/editor-ui/src/composables/useStyles.spec.ts
Normal file
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
31
packages/editor-ui/src/composables/useStyles.ts
Normal file
31
packages/editor-ui/src/composables/useStyles.ts
Normal file
|
@ -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,
|
||||
});
|
|
@ -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<Omit<NotificationOptions, 'message'>> = {
|
||||
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<Omit<NotificationOptions, 'message'>> = {
|
||||
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<NotificationOptions>, track = true) {
|
||||
const { message, title } = messageData;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue