Merge branches 'node-1921-multi-step-form-duplicate-page-displayed' and 'master' of https://github.com/n8n-io/n8n into node-1921-multi-step-form-duplicate-page-displayed

This commit is contained in:
Michael Kret 2024-10-26 07:46:43 +03:00
commit 463ecb5cff
22 changed files with 633 additions and 220 deletions

View file

@ -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');
});
});

View file

@ -125,12 +125,14 @@ export class TaskRunnerServer {
const { app } = this;
// Augment errors sent to Sentry
if (this.globalConfig.sentry.backendDsn) {
const {
Handlers: { requestHandler, errorHandler },
} = await import('@sentry/node');
app.use(requestHandler());
app.use(errorHandler());
}
}
private setupCommonMiddlewares() {
// Compress the response data

View file

@ -226,6 +226,16 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
data-test-id="chat-message-system"
>
<span> {{ message.content }}</span>
<n8n-button
v-if="message.retry"
type="secondary"
size="mini"
:class="$style.retryButton"
data-test-id="error-retry-button"
@click="() => message.retry?.()"
>
{{ t('generic.retry') }}
</n8n-button>
</div>
<div v-else-if="message.type === 'code-diff'">
<CodeDiff
@ -553,6 +563,13 @@ code[class^='language-'] {
.error {
color: var(--color-danger);
display: flex;
flex-direction: column;
align-items: start;
}
.retryButton {
margin-top: var(--spacing-3xs);
}
.assistantText {

View file

@ -182,4 +182,58 @@ describe('AskAssistantChat', () => {
});
expect(container).toMatchSnapshot();
});
it('renders error message correctly with retry button', () => {
const wrapper = render(AskAssistantChat, {
global: {
directives: {
n8nHtml,
},
stubs,
},
props: {
user: { firstName: 'Kobi', lastName: 'Dog' },
messages: [
{
id: '1',
role: 'assistant',
type: 'error',
content: 'This is an error message.',
read: false,
// Button is not shown without a retry function
retry: async () => {},
},
],
},
});
expect(wrapper.container).toMatchSnapshot();
expect(wrapper.getByTestId('error-retry-button')).toBeInTheDocument();
});
it('does not render retry button if no error is present', () => {
const wrapper = render(AskAssistantChat, {
global: {
directives: {
n8nHtml,
},
stubs,
},
props: {
user: { firstName: 'Kobi', lastName: 'Dog' },
messages: [
{
id: '1',
type: 'text',
role: 'assistant',
content:
'Hi Max! Here is my top solution to fix the error in your **Transform data** node👇',
read: false,
},
],
},
});
expect(wrapper.container).toMatchSnapshot();
expect(wrapper.queryByTestId('error-retry-button')).not.toBeInTheDocument();
});
});

View file

@ -1,5 +1,179 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AskAssistantChat > does not render retry button if no error is present 1`] = `
<div>
<div
class="container"
>
<div
class="header"
>
<div
class="chatTitle"
>
<div
class="headerText"
>
<svg
fill="none"
height="18"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.9658 14.0171C19.9679 14.3549 19.8654 14.6851 19.6722 14.9622C19.479 15.2393 19.2046 15.4497 18.8869 15.5645L13.5109 17.5451L11.5303 22.9211C11.4137 23.2376 11.2028 23.5107 10.9261 23.7037C10.6494 23.8966 10.3202 24 9.9829 24C9.64559 24 9.3164 23.8966 9.0397 23.7037C8.76301 23.5107 8.55212 23.2376 8.43549 22.9211L6.45487 17.5451L1.07888 15.5645C0.762384 15.4479 0.489262 15.237 0.296347 14.9603C0.103431 14.6836 0 14.3544 0 14.0171C0 13.6798 0.103431 13.3506 0.296347 13.0739C0.489262 12.7972 0.762384 12.5863 1.07888 12.4697L6.45487 10.4891L8.43549 5.11309C8.55212 4.79659 8.76301 4.52347 9.0397 4.33055C9.3164 4.13764 9.64559 4.0342 9.9829 4.0342C10.3202 4.0342 10.6494 4.13764 10.9261 4.33055C11.2028 4.52347 11.4137 4.79659 11.5303 5.11309L13.5109 10.4891L18.8869 12.4697C19.2046 12.5845 19.479 12.7949 19.6722 13.072C19.8654 13.3491 19.9679 13.6793 19.9658 14.0171ZM14.1056 4.12268H15.7546V5.77175C15.7546 5.99043 15.8415 6.20015 15.9961 6.35478C16.1508 6.50941 16.3605 6.59628 16.5792 6.59628C16.7979 6.59628 17.0076 6.50941 17.1622 6.35478C17.3168 6.20015 17.4037 5.99043 17.4037 5.77175V4.12268H19.0528C19.2715 4.12268 19.4812 4.03581 19.6358 3.88118C19.7905 3.72655 19.8773 3.51682 19.8773 3.29814C19.8773 3.07946 19.7905 2.86974 19.6358 2.71511C19.4812 2.56048 19.2715 2.47361 19.0528 2.47361H17.4037V0.824535C17.4037 0.605855 17.3168 0.396131 17.1622 0.241501C17.0076 0.0868704 16.7979 0 16.5792 0C16.3605 0 16.1508 0.0868704 15.9961 0.241501C15.8415 0.396131 15.7546 0.605855 15.7546 0.824535V2.47361H14.1056C13.8869 2.47361 13.6772 2.56048 13.5225 2.71511C13.3679 2.86974 13.281 3.07946 13.281 3.29814C13.281 3.51682 13.3679 3.72655 13.5225 3.88118C13.6772 4.03581 13.8869 4.12268 14.1056 4.12268ZM23.1755 7.42082H22.3509V6.59628C22.3509 6.3776 22.2641 6.16788 22.1094 6.01325C21.9548 5.85862 21.7451 5.77175 21.5264 5.77175C21.3077 5.77175 21.098 5.85862 20.9434 6.01325C20.7887 6.16788 20.7019 6.3776 20.7019 6.59628V7.42082H19.8773C19.6586 7.42082 19.4489 7.50769 19.2943 7.66232C19.1397 7.81695 19.0528 8.02667 19.0528 8.24535C19.0528 8.46404 19.1397 8.67376 19.2943 8.82839C19.4489 8.98302 19.6586 9.06989 19.8773 9.06989H20.7019V9.89443C20.7019 10.1131 20.7887 10.3228 20.9434 10.4775C21.098 10.6321 21.3077 10.719 21.5264 10.719C21.7451 10.719 21.9548 10.6321 22.1094 10.4775C22.2641 10.3228 22.3509 10.1131 22.3509 9.89443V9.06989H23.1755C23.3941 9.06989 23.6039 8.98302 23.7585 8.82839C23.9131 8.67376 24 8.46404 24 8.24535C24 8.02667 23.9131 7.81695 23.7585 7.66232C23.6039 7.50769 23.3941 7.42082 23.1755 7.42082Z"
fill="url(#paint0_linear_173_12825)"
/>
<defs>
<lineargradient
gradientUnits="userSpaceOnUse"
id="paint0_linear_173_12825"
x1="-3.67094e-07"
x2="28.8315"
y1="-0.000120994"
y2="9.82667"
>
<stop
stop-color="var(--color-assistant-highlight-1)"
/>
<stop
offset="0.495"
stop-color="var(--color-assistant-highlight-2)"
/>
<stop
offset="1"
stop-color="var(--color-assistant-highlight-3)"
/>
</lineargradient>
</defs>
</svg>
<span
class="text large"
>
AI Assistant
</span>
</div>
<div
class="beta"
>
beta
</div>
</div>
<div
class="back"
data-test-id="close-chat-button"
>
<n8n-icon-stub
color="text-base"
icon="arrow-right"
/>
</div>
</div>
<div
class="body"
>
<div
class="messages"
>
<div
class="message"
data-test-id="chat-message-assistant"
>
<div
class="roleName"
>
<div
class="container small"
>
<svg
fill="none"
height="10"
viewBox="0 0 24 24"
width="10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.9658 14.0171C19.9679 14.3549 19.8654 14.6851 19.6722 14.9622C19.479 15.2393 19.2046 15.4497 18.8869 15.5645L13.5109 17.5451L11.5303 22.9211C11.4137 23.2376 11.2028 23.5107 10.9261 23.7037C10.6494 23.8966 10.3202 24 9.9829 24C9.64559 24 9.3164 23.8966 9.0397 23.7037C8.76301 23.5107 8.55212 23.2376 8.43549 22.9211L6.45487 17.5451L1.07888 15.5645C0.762384 15.4479 0.489262 15.237 0.296347 14.9603C0.103431 14.6836 0 14.3544 0 14.0171C0 13.6798 0.103431 13.3506 0.296347 13.0739C0.489262 12.7972 0.762384 12.5863 1.07888 12.4697L6.45487 10.4891L8.43549 5.11309C8.55212 4.79659 8.76301 4.52347 9.0397 4.33055C9.3164 4.13764 9.64559 4.0342 9.9829 4.0342C10.3202 4.0342 10.6494 4.13764 10.9261 4.33055C11.2028 4.52347 11.4137 4.79659 11.5303 5.11309L13.5109 10.4891L18.8869 12.4697C19.2046 12.5845 19.479 12.7949 19.6722 13.072C19.8654 13.3491 19.9679 13.6793 19.9658 14.0171ZM14.1056 4.12268H15.7546V5.77175C15.7546 5.99043 15.8415 6.20015 15.9961 6.35478C16.1508 6.50941 16.3605 6.59628 16.5792 6.59628C16.7979 6.59628 17.0076 6.50941 17.1622 6.35478C17.3168 6.20015 17.4037 5.99043 17.4037 5.77175V4.12268H19.0528C19.2715 4.12268 19.4812 4.03581 19.6358 3.88118C19.7905 3.72655 19.8773 3.51682 19.8773 3.29814C19.8773 3.07946 19.7905 2.86974 19.6358 2.71511C19.4812 2.56048 19.2715 2.47361 19.0528 2.47361H17.4037V0.824535C17.4037 0.605855 17.3168 0.396131 17.1622 0.241501C17.0076 0.0868704 16.7979 0 16.5792 0C16.3605 0 16.1508 0.0868704 15.9961 0.241501C15.8415 0.396131 15.7546 0.605855 15.7546 0.824535V2.47361H14.1056C13.8869 2.47361 13.6772 2.56048 13.5225 2.71511C13.3679 2.86974 13.281 3.07946 13.281 3.29814C13.281 3.51682 13.3679 3.72655 13.5225 3.88118C13.6772 4.03581 13.8869 4.12268 14.1056 4.12268ZM23.1755 7.42082H22.3509V6.59628C22.3509 6.3776 22.2641 6.16788 22.1094 6.01325C21.9548 5.85862 21.7451 5.77175 21.5264 5.77175C21.3077 5.77175 21.098 5.85862 20.9434 6.01325C20.7887 6.16788 20.7019 6.3776 20.7019 6.59628V7.42082H19.8773C19.6586 7.42082 19.4489 7.50769 19.2943 7.66232C19.1397 7.81695 19.0528 8.02667 19.0528 8.24535C19.0528 8.46404 19.1397 8.67376 19.2943 8.82839C19.4489 8.98302 19.6586 9.06989 19.8773 9.06989H20.7019V9.89443C20.7019 10.1131 20.7887 10.3228 20.9434 10.4775C21.098 10.6321 21.3077 10.719 21.5264 10.719C21.7451 10.719 21.9548 10.6321 22.1094 10.4775C22.2641 10.3228 22.3509 10.1131 22.3509 9.89443V9.06989H23.1755C23.3941 9.06989 23.6039 8.98302 23.7585 8.82839C23.9131 8.67376 24 8.46404 24 8.24535C24 8.02667 23.9131 7.81695 23.7585 7.66232C23.6039 7.50769 23.3941 7.42082 23.1755 7.42082Z"
fill="white"
/>
<defs>
<lineargradient
gradientUnits="userSpaceOnUse"
id="paint0_linear_173_12825"
x1="-3.67094e-07"
x2="28.8315"
y1="-0.000120994"
y2="9.82667"
>
<stop
stop-color="var(--color-assistant-highlight-1)"
/>
<stop
offset="0.495"
stop-color="var(--color-assistant-highlight-2)"
/>
<stop
offset="1"
stop-color="var(--color-assistant-highlight-3)"
/>
</lineargradient>
</defs>
</svg>
</div>
<span>
Assistant
</span>
</div>
<div
class="textMessage"
>
<div
class="assistantText rendered-content"
>
<p>
Hi Max! Here is my top solution to fix the error in your
<strong>
Transform data
</strong>
node👇
</p>
</div>
<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
<!--v-if-->
</div>
<div
class="inputWrapper"
data-test-id="chat-input-wrapper"
>
<textarea
data-test-id="chat-input"
placeholder="Enter your response..."
rows="1"
wrap="hard"
/>
<n8n-icon-button-stub
class="sendButton"
data-test-id="send-message-button"
disabled="true"
icon="paper-plane"
size="large"
type="text"
/>
</div>
</div>
</div>
`;
exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<div>
<div
@ -1168,6 +1342,175 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
</div>
`;
exports[`AskAssistantChat > renders error message correctly with retry button 1`] = `
<div>
<div
class="container"
>
<div
class="header"
>
<div
class="chatTitle"
>
<div
class="headerText"
>
<svg
fill="none"
height="18"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.9658 14.0171C19.9679 14.3549 19.8654 14.6851 19.6722 14.9622C19.479 15.2393 19.2046 15.4497 18.8869 15.5645L13.5109 17.5451L11.5303 22.9211C11.4137 23.2376 11.2028 23.5107 10.9261 23.7037C10.6494 23.8966 10.3202 24 9.9829 24C9.64559 24 9.3164 23.8966 9.0397 23.7037C8.76301 23.5107 8.55212 23.2376 8.43549 22.9211L6.45487 17.5451L1.07888 15.5645C0.762384 15.4479 0.489262 15.237 0.296347 14.9603C0.103431 14.6836 0 14.3544 0 14.0171C0 13.6798 0.103431 13.3506 0.296347 13.0739C0.489262 12.7972 0.762384 12.5863 1.07888 12.4697L6.45487 10.4891L8.43549 5.11309C8.55212 4.79659 8.76301 4.52347 9.0397 4.33055C9.3164 4.13764 9.64559 4.0342 9.9829 4.0342C10.3202 4.0342 10.6494 4.13764 10.9261 4.33055C11.2028 4.52347 11.4137 4.79659 11.5303 5.11309L13.5109 10.4891L18.8869 12.4697C19.2046 12.5845 19.479 12.7949 19.6722 13.072C19.8654 13.3491 19.9679 13.6793 19.9658 14.0171ZM14.1056 4.12268H15.7546V5.77175C15.7546 5.99043 15.8415 6.20015 15.9961 6.35478C16.1508 6.50941 16.3605 6.59628 16.5792 6.59628C16.7979 6.59628 17.0076 6.50941 17.1622 6.35478C17.3168 6.20015 17.4037 5.99043 17.4037 5.77175V4.12268H19.0528C19.2715 4.12268 19.4812 4.03581 19.6358 3.88118C19.7905 3.72655 19.8773 3.51682 19.8773 3.29814C19.8773 3.07946 19.7905 2.86974 19.6358 2.71511C19.4812 2.56048 19.2715 2.47361 19.0528 2.47361H17.4037V0.824535C17.4037 0.605855 17.3168 0.396131 17.1622 0.241501C17.0076 0.0868704 16.7979 0 16.5792 0C16.3605 0 16.1508 0.0868704 15.9961 0.241501C15.8415 0.396131 15.7546 0.605855 15.7546 0.824535V2.47361H14.1056C13.8869 2.47361 13.6772 2.56048 13.5225 2.71511C13.3679 2.86974 13.281 3.07946 13.281 3.29814C13.281 3.51682 13.3679 3.72655 13.5225 3.88118C13.6772 4.03581 13.8869 4.12268 14.1056 4.12268ZM23.1755 7.42082H22.3509V6.59628C22.3509 6.3776 22.2641 6.16788 22.1094 6.01325C21.9548 5.85862 21.7451 5.77175 21.5264 5.77175C21.3077 5.77175 21.098 5.85862 20.9434 6.01325C20.7887 6.16788 20.7019 6.3776 20.7019 6.59628V7.42082H19.8773C19.6586 7.42082 19.4489 7.50769 19.2943 7.66232C19.1397 7.81695 19.0528 8.02667 19.0528 8.24535C19.0528 8.46404 19.1397 8.67376 19.2943 8.82839C19.4489 8.98302 19.6586 9.06989 19.8773 9.06989H20.7019V9.89443C20.7019 10.1131 20.7887 10.3228 20.9434 10.4775C21.098 10.6321 21.3077 10.719 21.5264 10.719C21.7451 10.719 21.9548 10.6321 22.1094 10.4775C22.2641 10.3228 22.3509 10.1131 22.3509 9.89443V9.06989H23.1755C23.3941 9.06989 23.6039 8.98302 23.7585 8.82839C23.9131 8.67376 24 8.46404 24 8.24535C24 8.02667 23.9131 7.81695 23.7585 7.66232C23.6039 7.50769 23.3941 7.42082 23.1755 7.42082Z"
fill="url(#paint0_linear_173_12825)"
/>
<defs>
<lineargradient
gradientUnits="userSpaceOnUse"
id="paint0_linear_173_12825"
x1="-3.67094e-07"
x2="28.8315"
y1="-0.000120994"
y2="9.82667"
>
<stop
stop-color="var(--color-assistant-highlight-1)"
/>
<stop
offset="0.495"
stop-color="var(--color-assistant-highlight-2)"
/>
<stop
offset="1"
stop-color="var(--color-assistant-highlight-3)"
/>
</lineargradient>
</defs>
</svg>
<span
class="text large"
>
AI Assistant
</span>
</div>
<div
class="beta"
>
beta
</div>
</div>
<div
class="back"
data-test-id="close-chat-button"
>
<n8n-icon-stub
color="text-base"
icon="arrow-right"
/>
</div>
</div>
<div
class="body"
>
<div
class="messages"
>
<div
class="message"
data-test-id="chat-message-assistant"
>
<div
class="roleName"
>
<div
class="container small"
>
<svg
fill="none"
height="10"
viewBox="0 0 24 24"
width="10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.9658 14.0171C19.9679 14.3549 19.8654 14.6851 19.6722 14.9622C19.479 15.2393 19.2046 15.4497 18.8869 15.5645L13.5109 17.5451L11.5303 22.9211C11.4137 23.2376 11.2028 23.5107 10.9261 23.7037C10.6494 23.8966 10.3202 24 9.9829 24C9.64559 24 9.3164 23.8966 9.0397 23.7037C8.76301 23.5107 8.55212 23.2376 8.43549 22.9211L6.45487 17.5451L1.07888 15.5645C0.762384 15.4479 0.489262 15.237 0.296347 14.9603C0.103431 14.6836 0 14.3544 0 14.0171C0 13.6798 0.103431 13.3506 0.296347 13.0739C0.489262 12.7972 0.762384 12.5863 1.07888 12.4697L6.45487 10.4891L8.43549 5.11309C8.55212 4.79659 8.76301 4.52347 9.0397 4.33055C9.3164 4.13764 9.64559 4.0342 9.9829 4.0342C10.3202 4.0342 10.6494 4.13764 10.9261 4.33055C11.2028 4.52347 11.4137 4.79659 11.5303 5.11309L13.5109 10.4891L18.8869 12.4697C19.2046 12.5845 19.479 12.7949 19.6722 13.072C19.8654 13.3491 19.9679 13.6793 19.9658 14.0171ZM14.1056 4.12268H15.7546V5.77175C15.7546 5.99043 15.8415 6.20015 15.9961 6.35478C16.1508 6.50941 16.3605 6.59628 16.5792 6.59628C16.7979 6.59628 17.0076 6.50941 17.1622 6.35478C17.3168 6.20015 17.4037 5.99043 17.4037 5.77175V4.12268H19.0528C19.2715 4.12268 19.4812 4.03581 19.6358 3.88118C19.7905 3.72655 19.8773 3.51682 19.8773 3.29814C19.8773 3.07946 19.7905 2.86974 19.6358 2.71511C19.4812 2.56048 19.2715 2.47361 19.0528 2.47361H17.4037V0.824535C17.4037 0.605855 17.3168 0.396131 17.1622 0.241501C17.0076 0.0868704 16.7979 0 16.5792 0C16.3605 0 16.1508 0.0868704 15.9961 0.241501C15.8415 0.396131 15.7546 0.605855 15.7546 0.824535V2.47361H14.1056C13.8869 2.47361 13.6772 2.56048 13.5225 2.71511C13.3679 2.86974 13.281 3.07946 13.281 3.29814C13.281 3.51682 13.3679 3.72655 13.5225 3.88118C13.6772 4.03581 13.8869 4.12268 14.1056 4.12268ZM23.1755 7.42082H22.3509V6.59628C22.3509 6.3776 22.2641 6.16788 22.1094 6.01325C21.9548 5.85862 21.7451 5.77175 21.5264 5.77175C21.3077 5.77175 21.098 5.85862 20.9434 6.01325C20.7887 6.16788 20.7019 6.3776 20.7019 6.59628V7.42082H19.8773C19.6586 7.42082 19.4489 7.50769 19.2943 7.66232C19.1397 7.81695 19.0528 8.02667 19.0528 8.24535C19.0528 8.46404 19.1397 8.67376 19.2943 8.82839C19.4489 8.98302 19.6586 9.06989 19.8773 9.06989H20.7019V9.89443C20.7019 10.1131 20.7887 10.3228 20.9434 10.4775C21.098 10.6321 21.3077 10.719 21.5264 10.719C21.7451 10.719 21.9548 10.6321 22.1094 10.4775C22.2641 10.3228 22.3509 10.1131 22.3509 9.89443V9.06989H23.1755C23.3941 9.06989 23.6039 8.98302 23.7585 8.82839C23.9131 8.67376 24 8.46404 24 8.24535C24 8.02667 23.9131 7.81695 23.7585 7.66232C23.6039 7.50769 23.3941 7.42082 23.1755 7.42082Z"
fill="white"
/>
<defs>
<lineargradient
gradientUnits="userSpaceOnUse"
id="paint0_linear_173_12825"
x1="-3.67094e-07"
x2="28.8315"
y1="-0.000120994"
y2="9.82667"
>
<stop
stop-color="var(--color-assistant-highlight-1)"
/>
<stop
offset="0.495"
stop-color="var(--color-assistant-highlight-2)"
/>
<stop
offset="1"
stop-color="var(--color-assistant-highlight-3)"
/>
</lineargradient>
</defs>
</svg>
</div>
<span>
Assistant
</span>
</div>
<div
class="error"
data-test-id="chat-message-system"
>
<span>
⚠️ This is an error message.
</span>
<n8n-button-stub
class="retryButton"
data-test-id="error-retry-button"
size="mini"
type="secondary"
/>
</div>
<!--v-if-->
</div>
</div>
<!--v-if-->
</div>
<div
class="inputWrapper"
data-test-id="chat-input-wrapper"
>
<textarea
data-test-id="chat-input"
placeholder="Enter your response..."
rows="1"
wrap="hard"
/>
<n8n-icon-button-stub
class="sendButton"
data-test-id="send-message-button"
disabled="true"
icon="paper-plane"
size="large"
type="text"
/>
</div>
</div>
</div>
`;
exports[`AskAssistantChat > renders message with code snippet 1`] = `
<div>
<div

View file

@ -40,6 +40,7 @@ export namespace ChatUI {
role: 'assistant';
type: 'error';
content: string;
retry?: () => Promise<void>;
}
export interface AgentSuggestionMessage {

View file

@ -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 {

View file

@ -106,7 +106,7 @@ function onClose() {
.container {
height: 100%;
flex-basis: content;
z-index: 300;
z-index: var(--z-index-ask-assistant-chat);
}
.wrapper {

View file

@ -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 {

View file

@ -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>

View file

@ -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" />

View file

@ -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;
}

View file

@ -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

View file

@ -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);

View file

@ -242,7 +242,7 @@ watch(
left: 0;
height: 100%;
width: 100%;
z-index: 9999999;
z-index: var(--z-index-workflow-preview-ndv);
}
.spinner {

View file

@ -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',
});

View 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',
);
});
});

View 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,
});

View file

@ -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;

View file

@ -18,6 +18,7 @@ import { reactive } from 'vue';
import * as chatAPI from '@/api/ai';
import * as telemetryModule from '@/composables/useTelemetry';
import type { Telemetry } from '@/plugins/telemetry';
import type { ChatUI } from 'n8n-design-system/types/assistant';
let settingsStore: ReturnType<typeof useSettingsStore>;
let posthogStore: ReturnType<typeof usePostHog>;
@ -417,4 +418,74 @@ describe('AI Assistant store', () => {
workflow_id: '__EMPTY__',
});
});
it('should call the function again if retry is called after handleServiceError', async () => {
const mockFn = vi.fn();
const assistantStore = useAssistantStore();
assistantStore.handleServiceError(new Error('test error'), '125', mockFn);
expect(assistantStore.chatMessages.length).toBe(1);
const message = assistantStore.chatMessages[0];
expect(message.type).toBe('error');
const errorMessage = message as ChatUI.ErrorMessage;
expect(errorMessage.retry).toBeDefined();
// This simulates the button click from the UI
await errorMessage.retry?.();
expect(mockFn).toHaveBeenCalled();
});
it('should properly clear messages on retry in a chat session', async () => {
const assistantStore = useAssistantStore();
const mockSessionId = 'mockSessionId';
const message: ChatRequest.MessageResponse = {
type: 'message',
role: 'assistant',
text: 'Hello!',
quickReplies: [
{ text: 'Yes', type: 'text' },
{ text: 'No', type: 'text' },
],
};
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
onMessage({ messages: [message], sessionId: mockSessionId });
onDone();
});
apiSpy.mockImplementationOnce((_ctx, _payload, _onMessage, _onDone, onError) =>
onError(new Error('test error')),
);
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
onMessage({ messages: [message], sessionId: mockSessionId });
onDone();
});
await assistantStore.initSupportChat('hello');
expect(assistantStore.chatMessages.length).toBe(2);
expect(assistantStore.chatMessages[0].type).toBe('text');
expect(assistantStore.chatMessages[1].type).toBe('text');
await assistantStore.sendMessage({ text: 'test' });
expect(assistantStore.chatMessages.length).toBe(4);
expect(assistantStore.chatMessages[0].type).toBe('text');
expect(assistantStore.chatMessages[1].type).toBe('text');
expect(assistantStore.chatMessages[2].type).toBe('text');
expect(assistantStore.chatMessages[3].type).toBe('error');
expect(assistantStore.chatMessages[3]).toHaveProperty('retry');
// This simulates the functionality triggered from the consumer (e.g. UI Button)
await (assistantStore.chatMessages[3] as ChatUI.ErrorMessage).retry?.();
expect(assistantStore.chatMessages.length).toBe(4);
expect(assistantStore.chatMessages[0].type).toBe('text');
expect(assistantStore.chatMessages[1].type).toBe('text');
expect(assistantStore.chatMessages[2].type).toBe('text');
expect(assistantStore.chatMessages[3].type).toBe('text');
});
});

View file

@ -251,13 +251,14 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
streaming.value = false;
}
function addAssistantError(content: string, id: string) {
function addAssistantError(content: string, id: string, retry?: () => Promise<void>) {
chatMessages.value.push({
id,
role: 'assistant',
type: 'error',
content,
read: true,
retry,
});
}
@ -275,11 +276,15 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
});
}
function handleServiceError(e: unknown, id: string) {
function handleServiceError(e: unknown, id: string, retry?: () => Promise<void>) {
assert(e instanceof Error);
stopStreaming();
assistantThinkingMessage.value = undefined;
addAssistantError(`${locale.baseText('aiAssistant.serviceError.message')}: (${e.message})`, id);
addAssistantError(
`${locale.baseText('aiAssistant.serviceError.message')}: (${e.message})`,
id,
retry,
);
}
function onEachStreamingMessage(response: ChatRequest.ResponsePayload, id: string) {
@ -447,7 +452,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
(e) => handleServiceError(e, id),
(e) =>
handleServiceError(e, id, async () => await initSupportChat(userMessage, credentialType)),
);
}
@ -498,7 +504,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
(e) => handleServiceError(e, id),
(e) => handleServiceError(e, id, async () => await initErrorHelper(context)),
);
}
@ -527,7 +533,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
(e) => handleServiceError(e, id),
(e) => handleServiceError(e, id, async () => await sendEvent(eventName, error)),
);
}
async function onNodeExecution(pushEvent: PushPayload<'nodeExecuteAfter'>) {
@ -564,6 +570,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}
const id = getRandomId();
const retry = async () => {
chatMessages.value = chatMessages.value.filter((msg) => msg.id !== id);
await sendMessage(chatMessage);
};
try {
addUserMessage(chatMessage.text, id);
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
@ -594,12 +606,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
(e) => handleServiceError(e, id),
(e) => handleServiceError(e, id, retry),
);
trackUserMessage(chatMessage.text, !!chatMessage.quickReplyType);
} catch (e: unknown) {
// in case of assert
handleServiceError(e, id);
handleServiceError(e, id, retry);
}
}
@ -824,5 +836,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
chatSessionTask,
initCredHelp,
isCredTypeActive,
handleServiceError,
};
});

View file

@ -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);