refactor(editor): Turn showMessage mixin to composable (#6081)

* refactor(editor): move $getExecutionError from showMessages mixin to pushConnection (it is used there only)

* refactor(editor): resolve showMessage mixin methods

* fix(editor): use composable instead of mixin

* fix(editor): resolve conflicts

* fix(editor): replace clearAllStickyNotifications

* fix(editor): replace confirmMessage

* fix(editor): replace confirmMessage

* fix(editor): replace confirmMessage

* fix(editor): remove last confirmMessage usage

* fix(editor): remove $prompt usage

* fix(editor): remove $show methods

* fix(editor): lint fix

* fix(editor): lint fix

* fix(editor): fixes after review
This commit is contained in:
Csaba Tuncsik
2023-05-12 10:13:42 +02:00
committed by GitHub
parent 0666377ef8
commit b95fcd7323
75 changed files with 990 additions and 862 deletions

View File

@@ -1,10 +1,15 @@
import mixins from 'vue-typed-mixins';
import { defineComponent } from 'vue';
import dateformat from 'dateformat';
import { VIEWS } from '@/constants';
import { showMessage } from '@/mixins/showMessage';
import { useToast } from '@/composables';
export const genericHelpers = mixins(showMessage).extend({
export const genericHelpers = defineComponent({
setup() {
return {
...useToast(),
};
},
data() {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -46,7 +51,7 @@ export const genericHelpers = mixins(showMessage).extend({
},
editAllowedCheck(): boolean {
if (this.isReadOnly) {
this.$showMessage({
this.showMessage({
// title: 'Workflow can not be changed!',
title: this.$locale.baseText('genericHelpers.showMessage.title'),
message: this.$locale.baseText('genericHelpers.showMessage.message'),

View File

@@ -1,11 +1,16 @@
import mixins from 'vue-typed-mixins';
import { showMessage } from './showMessage';
import { defineComponent } from 'vue';
import { useToast } from '@/composables';
import { VERSIONS_MODAL_KEY } from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store';
import { useVersionsStore } from '@/stores/versions.store';
export const newVersions = mixins(showMessage).extend({
export const newVersions = defineComponent({
setup() {
return {
...useToast(),
};
},
computed: {
...mapStores(useUIStore, useVersionsStore),
},
@@ -28,7 +33,7 @@ export const newVersions = mixins(showMessage).extend({
}
message = `${message} <a class="primary-color">More info</a>`;
this.$showToast({
this.showToast({
title: 'Critical update available',
message,
onClick: () => {

View File

@@ -5,6 +5,7 @@ import { stringSizeInBytes } from '@/utils';
import { MAX_WORKFLOW_PINNED_DATA_SIZE, PIN_DATA_NODE_TYPES_DENYLIST } from '@/constants';
import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useToast } from '@/composables';
export interface IPinDataContext {
node: INodeUi;
@@ -13,6 +14,11 @@ export interface IPinDataContext {
}
export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend({
setup() {
return {
...useToast(),
};
},
computed: {
...mapStores(useWorkflowsStore),
pinData(): IPinData[string] | undefined {
@@ -72,7 +78,7 @@ export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend
})} ${error.message}`;
}
this.$showError(error, title);
this.showError(error, title);
return false;
}
@@ -84,7 +90,7 @@ export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend
this.workflowsStore.pinDataSize + stringSizeInBytes(data) >
MAX_WORKFLOW_PINNED_DATA_SIZE
) {
this.$showError(
this.showError(
new Error(this.$locale.baseText('ndv.pinData.error.tooLarge.description')),
this.$locale.baseText('ndv.pinData.error.tooLarge.title'),
);

View File

@@ -7,8 +7,7 @@ import type {
import { externalHooks } from '@/mixins/externalHooks';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { showMessage } from '@/mixins/showMessage';
import { useTitleChange } from '@/composables/useTitleChange';
import { useTitleChange, useToast } from '@/composables';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import type {
@@ -19,6 +18,7 @@ import type {
IRunExecutionData,
IWorkflowBase,
SubworkflowOperationError,
IExecuteContextData,
} from 'n8n-workflow';
import { TelemetryHelpers } from 'n8n-workflow';
@@ -35,15 +35,11 @@ import { useSettingsStore } from '@/stores/settings.store';
import { parse } from 'flatted';
import { useSegment } from '@/stores/segment.store';
export const pushConnection = mixins(
externalHooks,
nodeHelpers,
showMessage,
workflowHelpers,
).extend({
export const pushConnection = mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
setup() {
return {
...useTitleChange(),
...useToast(),
};
},
data() {
@@ -324,7 +320,7 @@ export const pushConnection = mixins(
const runDataExecuted = pushData.data;
let runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data);
let runDataExecutedErrorMessage = this.getExecutionError(runDataExecuted.data);
if (pushData.data.status === 'crashed') {
runDataExecutedErrorMessage = this.$locale.baseText(
@@ -367,7 +363,7 @@ export const pushConnection = mixins(
// Workflow did start but had been put to wait
this.titleSet(workflow.name as string, 'IDLE');
this.$showToast({
this.showToast({
title: 'Workflow started waiting',
message: `${action} <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`,
type: 'success',
@@ -422,7 +418,7 @@ export const pushConnection = mixins(
this.workflowsStore.subWorkflowExecutionError = error;
this.$showMessage({
this.showMessage({
title: error.message,
message: error.description,
type: 'error',
@@ -436,7 +432,7 @@ export const pushConnection = mixins(
title = 'Problem executing workflow';
}
this.$showMessage({
this.showMessage({
title,
message: runDataExecutedErrorMessage,
type: 'error',
@@ -459,7 +455,7 @@ export const pushConnection = mixins(
execution.data.resultData.runData &&
execution.data.resultData.runData[execution.executedNode];
if (nodeType && nodeType.polling && !nodeOutput) {
this.$showMessage({
this.showMessage({
title: this.$locale.baseText('pushConnection.pollingNode.dataNotFound', {
interpolate: {
service: getTriggerNodeServiceName(nodeType),
@@ -473,13 +469,13 @@ export const pushConnection = mixins(
type: 'success',
});
} else {
this.$showMessage({
this.showMessage({
title: this.$locale.baseText('pushConnection.nodeExecutedSuccessfully'),
type: 'success',
});
}
} else {
this.$showMessage({
this.showMessage({
title: this.$locale.baseText('pushConnection.workflowExecutedSuccessfully'),
type: 'success',
});
@@ -582,5 +578,38 @@ export const pushConnection = mixins(
}
return true;
},
getExecutionError(data: IRunExecutionData | IExecuteContextData) {
const error = data.resultData.error;
let errorMessage: string;
if (data.resultData.lastNodeExecuted && error) {
errorMessage = error.message || error.description;
} else {
errorMessage = this.$locale.baseText('pushConnection.executionError', {
interpolate: { error: '!' },
});
if (error && error.message) {
let nodeName: string | undefined;
if ('node' in error) {
nodeName = typeof error.node === 'string' ? error.node : error.node!.name;
}
const receivedError = nodeName ? `${nodeName}: ${error.message}` : error.message;
errorMessage = this.$locale.baseText('pushConnection.executionError', {
interpolate: {
error: `.${this.$locale.baseText('pushConnection.executionError.details', {
interpolate: {
details: receivedError,
},
})}`,
},
});
}
}
return errorMessage;
},
},
});

View File

@@ -1,235 +0,0 @@
// @ts-ignore
import type { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types/notification';
import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/mixins/externalHooks';
import type { IExecuteContextData, IRunExecutionData } from 'n8n-workflow';
import type { ElMessageBoxOptions } from 'element-ui/types/message-box';
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
import { sanitizeHtml } from '@/utils';
import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store';
let stickyNotificationQueue: ElNotificationComponent[] = [];
export const showMessage = mixins(externalHooks).extend({
computed: {
...mapStores(useWorkflowsStore),
},
methods: {
$showMessage(
messageData: Omit<ElNotificationOptions, 'message'> & { message?: string },
track = true,
) {
messageData.dangerouslyUseHTMLString = true;
messageData.message = messageData.message
? sanitizeHtml(messageData.message)
: messageData.message;
if (messageData.position === undefined) {
messageData.position = 'bottom-right';
}
const notification = this.$notify(messageData as ElNotificationOptions);
if (messageData.duration === 0) {
stickyNotificationQueue.push(notification);
}
if (messageData.type === 'error' && track) {
this.$telemetry.track('Instance FE emitted error', {
error_title: messageData.title,
error_message: messageData.message,
caused_by_credential: this.causedByCredential(messageData.message),
workflow_id: this.workflowsStore.workflowId,
});
}
return notification;
},
$showToast(config: {
title: string;
message: string;
onClick?: () => void;
onClose?: () => void;
duration?: number;
customClass?: string;
closeOnClick?: boolean;
type?: MessageType;
}) {
// eslint-disable-next-line prefer-const
let notification: ElNotificationComponent;
if (config.closeOnClick) {
const cb = config.onClick;
config.onClick = () => {
if (notification) {
notification.close();
}
if (cb) {
cb();
}
};
}
notification = this.$showMessage({
title: config.title,
message: config.message,
onClick: config.onClick,
onClose: config.onClose,
duration: config.duration,
customClass: config.customClass,
type: config.type,
});
return notification;
},
$showAlert(config: ElMessageOptions): ElMessageComponent {
return this.$message(config);
},
$getExecutionError(data: IRunExecutionData | IExecuteContextData) {
const error = data.resultData.error;
let errorMessage: string;
if (data.resultData.lastNodeExecuted && error) {
errorMessage = error.message || error.description;
} else {
errorMessage = 'There was a problem executing the workflow!';
if (error && error.message) {
let nodeName: string | undefined;
if ('node' in error) {
nodeName = typeof error.node === 'string' ? error.node : error.node!.name;
}
const receivedError = nodeName ? `${nodeName}: ${error.message}` : error.message;
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
}
}
return errorMessage;
},
$showError(e: Error | unknown, title: string, message?: string) {
const error = e as Error;
const messageLine = message ? `${message}<br/>` : '';
this.$showMessage(
{
title,
message: `
${messageLine}
<i>${error.message}</i>
${this.collapsableDetails(error)}`,
type: 'error',
duration: 0,
},
false,
);
void this.$externalHooks().run('showMessage.showError', {
title,
message,
errorMessage: error.message,
});
this.$telemetry.track('Instance FE emitted error', {
error_title: title,
error_description: message,
error_message: error.message,
caused_by_credential: this.causedByCredential(error.message),
workflow_id: this.workflowsStore.workflowId,
});
},
async confirmMessage(
message: string,
headline: string,
type: MessageType | null = 'warning',
confirmButtonText?: string,
cancelButtonText?: string,
): Promise<boolean> {
try {
const options: ElMessageBoxOptions = {
confirmButtonText: confirmButtonText || this.$locale.baseText('showMessage.ok'),
cancelButtonText: cancelButtonText || this.$locale.baseText('showMessage.cancel'),
dangerouslyUseHTMLString: true,
...(type && { type }),
};
const sanitizedMessage = sanitizeHtml(message);
await this.$confirm(sanitizedMessage, headline, options);
return true;
} catch (e) {
return false;
}
},
async confirmModal(
message: string,
headline: string,
type: MessageType | null = 'warning',
confirmButtonText?: string,
cancelButtonText?: string,
showClose = false,
): Promise<string> {
try {
const options: ElMessageBoxOptions = {
confirmButtonText: confirmButtonText || this.$locale.baseText('showMessage.ok'),
cancelButtonText: cancelButtonText || this.$locale.baseText('showMessage.cancel'),
dangerouslyUseHTMLString: true,
showClose,
...(type && { type }),
};
const sanitizedMessage = sanitizeHtml(message);
await this.$confirm(sanitizedMessage, headline, options);
return 'confirmed';
} catch (e) {
return e as string;
}
},
clearAllStickyNotifications() {
stickyNotificationQueue.map((notification: ElNotificationComponent) => {
if (notification) {
notification.close();
}
});
stickyNotificationQueue = [];
},
// @ts-ignore
collapsableDetails({ description, node }: Error) {
if (!description) return '';
const errorDescription =
description.length > 500 ? `${description.slice(0, 500)}...` : description;
return `
<br>
<br>
<details>
<summary
style="color: #ff6d5a; font-weight: bold; cursor: pointer;"
>
${this.$locale.baseText('showMessage.showDetails')}
</summary>
<p>${node.name}: ${errorDescription}</p>
</details>
`;
},
/**
* Whether a workflow execution error was caused by a credential issue, as reflected by the error message.
*/
causedByCredential(message: string | undefined) {
if (!message) return false;
return message.includes('Credentials for') && message.includes('are not set');
},
},
});

View File

@@ -1,6 +1,6 @@
import { externalHooks } from '@/mixins/externalHooks';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { showMessage } from '@/mixins/showMessage';
import { useToast } from '@/composables';
import mixins from 'vue-typed-mixins';
import {
@@ -13,7 +13,12 @@ import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
export const workflowActivate = mixins(externalHooks, workflowHelpers, showMessage).extend({
export const workflowActivate = mixins(externalHooks, workflowHelpers).extend({
setup() {
return {
...useToast(),
};
},
data() {
return {
updatingWorkflowActivation: false,
@@ -60,7 +65,7 @@ export const workflowActivate = mixins(externalHooks, workflowHelpers, showMessa
try {
if (isWorkflowActive && newActiveState) {
this.$showMessage({
this.showMessage({
title: this.$locale.baseText('workflowActivator.workflowIsActive'),
type: 'success',
});
@@ -70,7 +75,7 @@ export const workflowActivate = mixins(externalHooks, workflowHelpers, showMessa
}
if (isCurrentWorkflow && nodesIssuesExist && newActiveState === true) {
this.$showMessage({
this.showMessage({
title: this.$locale.baseText(
'workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title',
),
@@ -87,7 +92,7 @@ export const workflowActivate = mixins(externalHooks, workflowHelpers, showMessa
await this.updateWorkflow({ workflowId: currWorkflowId, active: newActiveState });
} catch (error) {
const newStateName = newActiveState === true ? 'activated' : 'deactivated';
this.$showError(
this.showError(
error,
this.$locale.baseText('workflowActivator.showError.title', {
interpolate: { newStateName },

View File

@@ -4,6 +4,7 @@ import {
WEBHOOK_NODE_TYPE,
VIEWS,
EnterpriseEditionFeature,
MODAL_CONFIRM,
} from '@/constants';
import type {
@@ -40,7 +41,7 @@ import type {
import { externalHooks } from '@/mixins/externalHooks';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { showMessage } from '@/mixins/showMessage';
import { useToast, useMessage } from '@/composables';
import { isEqual } from 'lodash-es';
@@ -320,7 +321,13 @@ function executeData(
return executeData;
}
export const workflowHelpers = mixins(externalHooks, nodeHelpers, showMessage).extend({
export const workflowHelpers = mixins(externalHooks, nodeHelpers).extend({
setup() {
return {
...useToast(),
...useMessage(),
};
},
computed: {
...mapStores(
useNodeTypesStore,
@@ -741,26 +748,31 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, showMessage).e
params: { name: currentWorkflow },
}).href;
const overwrite = await this.confirmMessage(
const overwrite = await this.confirm(
this.$locale.baseText('workflows.concurrentChanges.confirmMessage.message', {
interpolate: {
url,
},
}),
this.$locale.baseText('workflows.concurrentChanges.confirmMessage.title'),
null,
this.$locale.baseText('workflows.concurrentChanges.confirmMessage.confirmButtonText'),
this.$locale.baseText('workflows.concurrentChanges.confirmMessage.cancelButtonText'),
{
confirmButtonText: this.$locale.baseText(
'workflows.concurrentChanges.confirmMessage.confirmButtonText',
),
cancelButtonText: this.$locale.baseText(
'workflows.concurrentChanges.confirmMessage.cancelButtonText',
),
},
);
if (overwrite) {
if (overwrite === MODAL_CONFIRM) {
return this.saveCurrentWorkflow({ id, name, tags }, redirect, true);
}
return false;
}
this.$showMessage({
this.showMessage({
title: this.$locale.baseText('workflowHelpers.showMessage.title'),
message: error.message,
type: 'error',
@@ -890,7 +902,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, showMessage).e
} catch (e) {
this.uiStore.removeActiveAction('workflowSaving');
this.$showMessage({
this.showMessage({
title: this.$locale.baseText('workflowHelpers.showMessage.title'),
message: (e as Error).message,
type: 'error',

View File

@@ -5,7 +5,7 @@ import { NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
import { externalHooks } from '@/mixins/externalHooks';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { showMessage } from '@/mixins/showMessage';
import { useToast } from '@/composables';
import mixins from 'vue-typed-mixins';
import { useTitleChange } from '@/composables/useTitleChange';
@@ -14,10 +14,11 @@ import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRootStore } from '@/stores/n8nRoot.store';
export const workflowRun = mixins(externalHooks, workflowHelpers, showMessage).extend({
export const workflowRun = mixins(externalHooks, workflowHelpers).extend({
setup() {
return {
...useTitleChange(),
...useToast(),
};
},
computed: {
@@ -106,7 +107,7 @@ export const workflowRun = mixins(externalHooks, workflowHelpers, showMessage).e
trackNodeIssues.push(trackNodeIssue);
}
this.$showMessage({
this.showMessage({
title: this.$locale.baseText('workflowRun.showMessage.title'),
message: errorMessages.join('<br />'),
type: 'error',
@@ -239,7 +240,7 @@ export const workflowRun = mixins(externalHooks, workflowHelpers, showMessage).e
return runWorkflowApiResponse;
} catch (error) {
this.titleSet(workflow.name as string, 'ERROR');
this.$showError(error, this.$locale.baseText('workflowRun.showError.title'));
this.showError(error, this.$locale.baseText('workflowRun.showError.title'));
return undefined;
}
},