Notification (#337)

* rewrite notice module

* optimization: show some notification when export html or pdf

* optimization: style of open project button

* little bug fix

* style: uniform titlebar hight to remove some style error
This commit is contained in:
冉四夕 2018-06-15 21:30:10 +08:00 committed by GitHub
parent 311d7ddf1a
commit 4be72ade97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 448 additions and 226 deletions

View File

@ -20,16 +20,23 @@ const handleResponseForExport = (e, { type, content, filename, pathname }) => {
defaultPath
})
// If export PDF, the content will be undefined.
if (!content && type === 'pdf') {
win.webContents.printToPDF({ printBackground: true }, (err, data) => {
if (err) log(err)
else {
writeFile(filePath, data, extension)
.then(() => {
win.webContents.send('AGANI::export-success', { type, filePath })
})
.catch(log)
}
})
} else {
writeFile(filePath, content, extension)
.then(() => {
win.webContents.send('AGANI::export-success', { type, filePath })
})
.catch(log)
}
}

View File

@ -10,7 +10,7 @@ autoUpdater.autoDownload = false
autoUpdater.on('error', error => {
if (win) {
win.webContents.send('AGANI::UPDATE_ERROR', error === null ? 'Error: unknown' : (error.stack || error).toString())
win.webContents.send('AGANI::UPDATE_ERROR', error === null ? 'Error: unknown' : (error.message || error).toString())
}
})

View File

@ -1,7 +1,6 @@
<template>
<div
class="editor-container"
:class="[{ 'frameless': platform !== 'darwin' }]"
>
<title-bar
:pathname="pathname"
@ -130,6 +129,7 @@
dispatch('LINTEN_FOR_SET_LINE_ENDING')
dispatch('LISTEN_FOR_NEW_TAB')
dispatch('LISTEN_FOR_CLOSE_TAB')
dispatch('LINTEN_FOR_EXPORT_SUCCESS')
// module: notification
dispatch('LISTEN_FOR_NOTIFICATION')
}
@ -138,9 +138,6 @@
<style scoped>
.editor-container {
padding-top: 22px;
}
.editor-container.frameless {
padding-top: 25px;
}
.editor-container .hide {
@ -151,6 +148,7 @@
}
.editor-middle {
display: flex;
min-height: calc(100vh - 25px);
& > .editor {
flex: 1;
}

File diff suppressed because one or more lines are too long

View File

@ -4,18 +4,15 @@
v-if="!sourceCode"
:theme="theme"
></search>
<status></status>
</div>
</template>
<script>
import Search from './search'
import Status from './status'
export default {
components: {
Search,
Status
Search
},
props: {
sourceCode: Boolean,

View File

@ -477,7 +477,7 @@
<style>
.ag-dialog-table {
border-radius: 3px;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(230, 230, 230, .3);
}

View File

@ -1,7 +1,6 @@
<template>
<div
class="editor-with-tabs"
:class="[{ 'frameless': platform !== 'darwin' }]"
>
<tabs v-show="showTabBar"></tabs>
<editor
@ -22,7 +21,6 @@
import Tabs from './tabs.vue'
import Editor from './editor.vue'
import SourceCode from './sourceCode.vue'
import { mapState } from 'vuex'
export default {
props: {
@ -49,11 +47,6 @@
required: true
}
},
computed: {
...mapState([
'platform'
])
},
components: {
Tabs,
Editor,
@ -67,10 +60,7 @@
flex: 1;
display: flex;
flex-direction: column;
height: calc(100vh - 22px);
height: calc(100vh - 25px);
overflow: hidden;
}
.editor-with-tabs.frameless {
height: calc(100vh - 25px);
}
</style>

View File

@ -2,7 +2,6 @@
<div
class="source-code"
ref="sourceCode"
:class="[theme, { 'frameless': platform !== 'darwin' }]"
>
</div>
</template>
@ -12,7 +11,6 @@
import { wordCount as getWordCount } from '../../../editor/utils'
import { adjustCursor } from '../../util'
import bus from '../../bus'
import { mapState } from 'vuex'
export default {
props: {
@ -24,12 +22,6 @@
cursor: Object
},
computed: {
...mapState([
'platform'
])
},
data () {
return {
contentState: null,
@ -129,13 +121,10 @@
<style>
.source-code {
height: calc(100vh - 22px);
height: calc(100vh - 25px);
box-sizing: border-box;
overflow: auto;
}
.source-code.frameless {
height: calc(100vh - 25px);
}
.source-code .CodeMirror {
margin: 50px auto;
max-width: 860px;

View File

@ -2,9 +2,8 @@
<div
class="recent-files-projects"
:class="theme"
@click="newFile"
>
<h1>Mark Text</h1>
<div>Make you fall in love with writing</div>
</div>
</template>
@ -16,6 +15,11 @@
...mapState({
'theme': state => state.preferences.theme
})
},
methods: {
newFile () {
this.$store.dispatch('NEW_BLANK_FILE')
}
}
}
</script>
@ -23,29 +27,11 @@
<style scoped>
.recent-files-projects {
flex: 1;
& h1 {
text-align: center;
font-weight: 100;
font-size: 4rem;
margin-top: 200px;
font-family: monospace;
color: var(--primaryColor);
border-bottom: none;
}
& > div {
font-size: 2rem;
font-family: cursive;
text-align: center;
color: var(--regularColor);
}
}
.dark.recent-files-projects {
background: var(--darkBgColor);
& > h1 {
color: var(--baseBorder);
}
& > div {
color: var(--placeholerColor);
color: var(--baseBorder);
}
}
</style>

View File

@ -2,7 +2,7 @@
<div
v-show="showSideBar"
class="side-bar"
:class="[theme, { 'frameless': platform !== 'darwin' }]"
:class="[theme]"
ref="sideBar"
:style="{ 'width': `${finalSideBarWidth}px` }"
>
@ -81,9 +81,6 @@
'sideBarWidth': state => state.project.sideBarWidth,
'tabs': state => state.editor.tabs
}),
...mapState([
'platform'
]),
...mapGetters(['fileList']),
finalSideBarWidth () {
const { showSideBar, rightColumn, sideBarViewWidth } = this
@ -145,13 +142,10 @@
<style scoped>
.side-bar {
display: flex;
height: calc(100vh - 22px);
height: calc(100vh - 25px);
position: relative;
color: var(--secondaryColor);
}
.side-bar.frameless {
height: calc(100vh - 25px);
}
.side-bar.dark {
background: var(--darkBgColor);
}

View File

@ -90,7 +90,11 @@
</div>
</div>
<div v-else class="open-project">
<a href="javascript:;" @click="openProject">Open Project</a>
<a href="javascript:;" @click="openProject" title="Open Project">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-create-project"></use>
</svg>
</a>
</div>
</div>
</template>
@ -295,8 +299,23 @@
align-items: center;
margin-top: -100px;
& > a {
width: 35px;
height: 35px;
border-radius: 50%;
text-decoration: none;
color: var(--activeColor);
background: rgba(31, 116, 255, .5);
transition: all .3s ease;
display: flex;
justify-content: space-around;
align-items: center;
& > svg {
width: 18px;
height: 18px;
color: #fff;
}
&:hover {
background: var(--primary);
}
}
}
.new-input {

View File

@ -1,108 +0,0 @@
<template>
<div
class="bottom-status"
:class="{'error': error}"
v-show="showStatus"
>
<div class="status-wrapper">
<span class="message" :title="message">{{ message }}</span>
<span class="yes" v-show="showYes" @click="handleYesClick">[ Y ]</span>
<span class="no" @click="close(true)">[ X ]</span>
</div>
</div>
</template>
<script>
import bus from '../bus'
export default {
data () {
return {
error: false,
showStatus: false,
message: '',
showYes: false,
eventId: ''
}
},
created () {
this.$nextTick(() => {
bus.$on('status-error', msg => {
this.showStatus = true
this.error = true
this.message = msg
})
bus.$on('status-message', (msg, timeout) => {
this.showStatus = true
this.error = false
this.message = msg
if (timeout) {
setTimeout(() => {
this.close(true)
}, timeout)
}
})
bus.$on('status-promote', (msg, eventId) => {
this.showStatus = true
this.error = false
this.eventId = eventId
this.showYes = true
this.message = msg
})
})
},
methods: {
close (isEmit = false) {
const { eventId } = this
this.error = false
this.showStatus = false
this.message = ''
this.showYes = false
this.eventId = ''
if (isEmit && eventId) {
bus.$emit(eventId, false)
}
},
handleYesClick () {
const { eventId } = this
if (eventId) {
bus.$emit(eventId, true)
}
this.close()
}
}
}
</script>
<style scoped>
.bottom-status {
width: 100%;
height: 25px;
background-color: #2196F3;
color: #fff;
}
.bottom-status.error {
background-color: var(--dangerColor);
}
.status-wrapper {
text-align: center;
line-height: 25px;
font-size: 13px;
color: #fff;
}
.message {
max-width: 70%;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.message, .yes {
margin-right: 5px;
}
.yes, .no {
vertical-align: top;
color: #fff;
cursor: pointer;
}
</style>

View File

@ -1,5 +1,6 @@
<template>
<div class="title-bar"
<div
class="title-bar"
:class="[{ 'active': active }, theme, { 'frameless': platform !== 'darwin' }]"
>
<div class="title">
@ -166,7 +167,7 @@
-webkit-app-region: drag;
user-select: none;
width: 100%;
height: 22px;
height: 25px;
box-sizing: border-box;
color: #F2F6FC;
position: fixed;
@ -177,9 +178,6 @@
transition: color .4s ease-in-out;
cursor: default;
}
.title-bar.frameless {
height: 25px;
}
.active {
color: #909399;
}
@ -191,10 +189,17 @@
.title {
padding: 0 100px;
height: 100%;
line-height: 22px;
font-size: 12px;
line-height: 25px;
font-size: 14px;
text-align: center;
transition: all .25s ease-in-out;
& .filename {
transition: all .25s ease-in-out;
}
}
.title-bar:not(.frameless) .title .filename:hover {
color: var(--primary);
}
.active .save-dot {
@ -237,10 +242,10 @@
}
.word-count {
cursor: pointer;
font-size: 12px;
font-size: 14px;
color: #F2F6FC;
height: 15px;
line-height: 15px;
height: 17px;
line-height: 17px;
margin-top: 4px;
padding: 1px 5px;
border-radius: 1px;

View File

@ -189,11 +189,15 @@
box-sizing: border-box;
display: inline-block;
background: #eee;
cursor: not-allowed;
}
.button a.active, .button a.twitter:hover {
background: var(--activeColor);
.button a.active {
background: var(--primary);
color: #fff;
}
.button a.active {
cursor: pointer;
}
.button a.github {
color: var(--secondaryColor);
text-decoration: none;
@ -211,4 +215,8 @@
border-color: transparent;
color: var(--darkInputColor);
}
.tweet-dialog.light .el-dialog__header {
background: var(--primary);
color: #fff;
}
</style>

View File

@ -1,4 +1,8 @@
:root {
--primary: #409eff;
--info: #909399;
--warning: rgb(255, 130, 0);
--error: rgb(242, 19, 93);
--lightBarColor: rgb(245, 245, 245);
--lightTabColor: rgb(243, 243, 243);
--darkBgColor: rgb(45, 45, 45);

View File

@ -8,6 +8,9 @@ import store from './store'
import './assets/symbolIcon'
import './index.css'
import { Dialog, Form, FormItem, InputNumber, Button, Tooltip, Upload, Slider, ColorPicker, Col, Row } from 'element-ui'
import services from './services'
// import notice from './services/notification'
// In the renderer process:
// var webFrame = require('electron').webFrame
// var SpellCheckProvider = require('electron-spell-check-provider')
@ -55,6 +58,10 @@ if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.http = Vue.prototype.$http = axios
Vue.config.productionTip = false
services.forEach(s => {
Vue.prototype['$' + s.name] = s[s.name]
})
/* eslint-disable no-new */
new Vue({
components: { App },

View File

@ -1,5 +1,4 @@
import { getFileStateFromData } from '../store/help.js'
import { message } from '../notice'
export const tabsMixins = {
methods: {
@ -31,7 +30,13 @@ export const fileMixins = {
this.$store.dispatch('UPDATE_CURRENT_FILE', fileState)
if (isMixed && !isOpened) {
message(`${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`, 20000)
this.$notify({
title: 'Line Ending',
message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
type: 'primary',
time: 20000,
showConfirm: false
})
}
}
}

View File

@ -1,21 +0,0 @@
import bus from '../bus'
import { getUniqueId } from '../../editor/utils'
export const error = msg => {
bus.$emit('status-error', msg)
}
export const message = (msg, timeout) => {
bus.$emit('status-message', msg, timeout)
}
export const promote = msg => {
const eventId = getUniqueId()
bus.$emit('status-promote', msg, eventId)
return new Promise((resolve, reject) => {
bus.$on(eventId, bool => {
bool ? resolve() : reject(bool) // reject bool just for fix the esint error: `prefer-promise-reject-errors`
})
})
}

View File

@ -0,0 +1,5 @@
import notification from './notification'
export default [
notification
]

View File

@ -0,0 +1,116 @@
.mt-notification {
position: fixed;
z-index: 10000;
overflow: hidden;
border-radius: 5px;
max-width: 350px;
min-width: 280px;
transition: all .2s ease;
box-shadow: 0px 12px 0px 0px rgba(0, 0, 0, 0.0);
backface-visibility: hidden;
right: 15px;
bottom: 15px;
& > .notice-bg {
width: 0;
height: 0;
position: absolute;
transition: all .8s ease,left .4s ease,top .4s ease;
border-radius: 50%;
z-index: 10;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
}
& .content {
z-index: 100;
color: #fff;
position: relative;
& .icon-wrapper {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
justify-content: space-around;
align-items: center;
background: rgba(255, 255, 255, .2);
}
& .title {
display: flex;
align-items: center;
padding: 5px 5px;
& svg.icon {
width: .85em;
height: .85em;
}
& span {
flex: 1;
margin-left: 10px;
}
& .close {
margin-right: 10px;
opacity: 0;
transition: all .2s ease;
transform: scale(0);
cursor: pointer;
transform-origin: center;
}
}
& .body {
display: flex;
align-items: center;
font-size: 14px;
padding-bottom: 5px;
& .left-text {
overflow: hidden;
word-wrap: break-word;
padding: 5px;
box-sizing: border-box;
width: 100%;
}
& .confirm {
display: none;
margin-left: 7px;
width: 30px;
height: 30px;
cursor: pointer;
}
}
}
& .fluent-container {
width: 100%;
height: 100%;
position: absolute;
z-index: 40;
display: block;
top: 0px;
left: 0px;
}
& .fluent {
position: absolute;
z-index: 50;
display: block;
backface-visibility: hidden;
transform: translate(-50%, -50%);
border-radius: 50%;
transition: opacity 1s ease,width .4s ease,height .4s ease;
background: rgba(255, 255, 255, .2);
opacity: 0;
filter: blur(22px);
}
&:hover .content .title .close {
opacity: 1;
transform: scale(1);
}
}
.mt-notification.mt-confirm .content .body {
& .left-text {
width: calc(100% - 45px);
}
& .confirm {
display: flex;
}
}

View File

@ -0,0 +1,27 @@
<div class="mt-notification">
<div class="notice-bg"></div>
<div class="fluent-container">
<div class="fluent"></div>
</div>
<div class="content">
<div class="title">
<div class="icon-wrapper">
<svg class="icon" aria-hidden="true">
<use xlink:href="#{{icon}}"></use>
</svg>
</div>
<span>{{title}}</span>
<svg class="icon close" aria-hidden="true">
<use xlink:href="#icon-close"></use>
</svg>
</div>
<div class="body">
<div class="left-text">{{message}}</div>
<div class="confirm icon-wrapper">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-confirm"></use>
</svg>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,145 @@
import template from './index.html'
import './index.css'
const INON_HASH = {
primary: 'icon-message',
error: 'icon-error',
warning: 'icon-warn',
info: 'icon-info'
}
const notification = {
name: 'notify',
notify ({
time = 10000,
title = '',
message = '',
type = 'primary',
showConfirm = false
}) {
let rs
let rj
let timer = null
const fragment = document.createElement('div')
fragment.innerHTML = template
.replace(/\{\{icon\}\}/, INON_HASH[type])
.replace(/\{\{title\}\}/, title)
.replace(/\{\{message\}\}/, message)
const noticeContainer = fragment.querySelector('.mt-notification')
const bgNotice = noticeContainer.querySelector('.notice-bg')
const fluent = noticeContainer.querySelector('.fluent')
const close = noticeContainer.querySelector('.close')
const { offsetHeight } = noticeContainer
let target = noticeContainer
noticeContainer.classList.add(`mt-${type}`)
if (showConfirm) {
noticeContainer.classList.add(`mt-confirm`)
target = noticeContainer.querySelector('.confirm')
}
bgNotice.style.backgroundColor = `var(--${type})`
fluent.style.height = offsetHeight * 2 + 'px'
fluent.style.width = offsetHeight * 2 + 'px'
const setCloseTimer = () => {
if (typeof time === 'number' && time > 0) {
timer = setTimeout(() => {
remove()
}, time)
}
}
const mousemoveHandler = event => {
const { left, top } = noticeContainer.getBoundingClientRect()
const x = event.pageX
const y = event.pageY
fluent.style.left = x - left + 'px'
fluent.style.top = y - top + 'px'
fluent.style.opacity = '1'
fluent.style.height = noticeContainer.offsetHeight * 2 + 'px'
fluent.style.width = noticeContainer.offsetHeight * 2 + 'px'
if (timer) clearTimeout(timer)
}
const mouseleaveHandler = event => {
fluent.style.opacity = '0'
fluent.style.height = noticeContainer.offsetHeight * 4 + 'px'
fluent.style.width = noticeContainer.offsetHeight * 4 + 'px'
if (timer) clearTimeout(timer)
setCloseTimer()
}
const clickHandler = event => {
event.preventDefault()
event.stopPropagation()
remove()
rs && rs()
}
const closeHandler = event => {
event.preventDefault()
event.stopPropagation()
remove()
rj && rj()
}
const rePositionNotices = () => {
const notices = document.querySelectorAll('.mt-notification')
let i
let hx = 0
let len = notices.length
for (i = 0; i < len; i++) {
notices[i].style.transform = `translate(0, -${hx}px)`
notices[i].style.zIndex = 10000 - i
hx += notices[i].offsetHeight + 10
}
}
const remove = () => {
fluent.style.filter = 'blur(10px)'
fluent.style.opacity = '0'
fluent.style.height = noticeContainer.offsetHeight * 5 + 'px'
fluent.style.width = noticeContainer.offsetHeight * 5 + 'px'
noticeContainer.style.opacity = '0'
noticeContainer.style.right = '-400px'
setTimeout(() => {
noticeContainer.removeEventListener('mousemove', mousemoveHandler)
noticeContainer.removeEventListener('mouseleave', mouseleaveHandler)
target.removeEventListener('click', clickHandler)
close.removeEventListener('click', closeHandler)
noticeContainer.remove()
rePositionNotices()
}, 100)
}
noticeContainer.addEventListener('mousemove', mousemoveHandler)
noticeContainer.addEventListener('mouseleave', mouseleaveHandler)
target.addEventListener('click', clickHandler)
close.addEventListener('click', closeHandler)
setTimeout(() => {
bgNotice.style.width = noticeContainer.offsetWidth * 3.5 + 'px'
bgNotice.style.height = noticeContainer.offsetWidth * 3.5 + 'px'
rePositionNotices()
}, 50)
setCloseTimer()
document.body.prepend(noticeContainer, document.body.firstChild)
return new Promise((resolve, reject) => {
rs = resolve
rj = reject
})
}
}
export default notification

View File

@ -1,5 +1,5 @@
import { ipcRenderer } from 'electron'
import { error, message, promote } from '../notice'
import notice from '../services/notification'
const state = {}
@ -10,17 +10,35 @@ const mutations = {}
// AGANI::UPDATE_DOWNLOADED
const actions = {
LISTEN_FOR_UPDATE ({ commit }) {
ipcRenderer.on('AGANI::UPDATE_ERROR', (e, msg) => {
error(msg)
ipcRenderer.on('AGANI::UPDATE_ERROR', (e, message) => {
notice.notify({
title: 'Update',
type: 'error',
time: 10000,
message
})
})
ipcRenderer.on('AGANI::UPDATE_NOT_AVAILABLE', (e, msg) => {
message(msg)
ipcRenderer.on('AGANI::UPDATE_NOT_AVAILABLE', (e, message) => {
notice.notify({
title: 'Update not Available',
type: 'warning',
message
})
})
ipcRenderer.on('AGANI::UPDATE_DOWNLOADED', (e, msg) => {
message(msg)
ipcRenderer.on('AGANI::UPDATE_DOWNLOADED', (e, message) => {
notice.notify({
title: 'Update Downloaded',
type: 'info',
message
})
})
ipcRenderer.on('AGANI::UPDATE_AVAILABLE', (e, msg) => {
promote(msg)
ipcRenderer.on('AGANI::UPDATE_AVAILABLE', (e, message) => {
notice.notify({
title: 'Update Available',
type: 'primary',
message,
showConfirm: true
})
.then(() => {
const needUpdate = true
ipcRenderer.send('AGANI::NEED_UPDATE', { needUpdate })

View File

@ -1,8 +1,9 @@
import { ipcRenderer } from 'electron'
import { ipcRenderer, shell } from 'electron'
import path from 'path'
import bus from '../bus'
import { hasKeys } from '../util'
import { getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
import notice from '../services/notification'
const toc = require('markdown-toc')
@ -434,6 +435,19 @@ const actions = {
ipcRenderer.send('AGANI::response-export', { type, content, filename, pathname })
},
LINTEN_FOR_EXPORT_SUCCESS ({ commit }) {
ipcRenderer.on('AGANI::export-success', (e, { type, filePath }) => {
notice.notify({
title: 'Export',
message: `Export ${path.basename(filePath)} successfully`,
showConfirm: true
})
.then(() => {
shell.showItemInFolder(filePath)
})
})
},
LISTEN_FOR_INSERT_IMAGE ({ commit, state }) {
ipcRenderer.on('AGANI::INSERT_IMAGE', (e, { filename: imagePath, type }) => {
if (!hasKeys(state.currentFile)) return

View File

@ -1,5 +1,5 @@
import { ipcRenderer } from 'electron'
import { error, message } from '../notice'
import notice from '../services/notification'
const state = {}
@ -10,11 +10,20 @@ const mutations = {
const actions = {
LISTEN_FOR_NOTIFICATION ({ commit }) {
ipcRenderer.on('AGANI::show-error-notification', (e, msg) => {
error(msg)
ipcRenderer.on('AGANI::show-error-notification', (e, message) => {
notice.notify({
title: 'Error',
type: 'error',
message
})
})
ipcRenderer.on('AGANI::show-info-notification', (e, { msg, timeout }) => {
message(msg, timeout)
ipcRenderer.on('AGANI::show-info-notification', (e, { message, timeout }) => {
notice.notify({
title: 'Infomation',
type: 'info',
time: timeout,
message
})
})
}
}

View File

@ -3,7 +3,7 @@ import { ipcRenderer, shell } from 'electron'
import { addFile, unlinkFile, changeFile, addDirectory, unlinkDirectory } from './treeCtrl'
import bus from '../bus'
import { create, paste, rename } from '../util/fileSystem'
import { error } from '../notice'
import notice from '../services/notification'
const width = localStorage.getItem('side-bar-width')
const sideBarWidth = typeof +width === 'number' ? Math.max(+width, 180) : 280
@ -164,7 +164,11 @@ const actions = {
commit('SET_CLIPBOARD', null)
})
.catch(err => {
error(err.message)
notice.notify({
title: 'Paste Error',
type: 'error',
message: err.message
})
})
}
})
@ -182,7 +186,11 @@ const actions = {
commit('CREATE_PATH', {})
})
.catch(err => {
error(err.message)
notice.notify({
title: 'Error in Side Bar',
type: 'error',
message: err.message
})
})
},