// @ts-ignore import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types/notification'; import mixins from 'vue-typed-mixins'; import { externalHooks } from '@/mixins/externalHooks'; import { 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'; let stickyNotificationQueue: ElNotificationComponent[] = []; export const showMessage = mixins(externalHooks).extend({ computed: { ...mapStores(useWorkflowsStore), }, methods: { $showMessage( messageData: Omit & { 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:
"${receivedError}"`; } } return errorMessage; }, $showError(e: Error | unknown, title: string, message?: string) { const error = e as Error; const messageLine = message ? `${message}
` : ''; this.$showMessage( { title, message: ` ${messageLine} ${error.message} ${this.collapsableDetails(error)}`, type: 'error', duration: 0, }, false, ); 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 { 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 { 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 `

${this.$locale.baseText('showMessage.showDetails')}

${node.name}: ${errorDescription}

`; }, /** * 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'); }, }, });