Add per-tab notifications (#1377)

* Add per-tab notifications

* fix: file watcher depth on macOS

* Free array reference
This commit is contained in:
Felix Häusler 2019-09-27 19:25:42 +02:00 committed by GitHub
parent 1cac5dbe52
commit b386630d3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 253 additions and 54 deletions

View File

@ -100,16 +100,13 @@ class App {
// Prevent to load webview and opening links or new windows via HTML/JS.
app.on('web-contents-created', (event, contents) => {
contents.on('will-attach-webview', (event, webPreferences, params) => {
console.warn('Prevented webview creation.')
contents.on('will-attach-webview', event => {
event.preventDefault()
})
contents.on('will-navigate', event => {
console.warn('Prevented opening a link.')
event.preventDefault()
})
contents.on('new-window', (event, url) => {
console.warn('Prevented opening a new window.')
contents.on('new-window', event => {
event.preventDefault()
})
})

View File

@ -6,7 +6,7 @@ import { exists } from 'common/filesystem'
import { hasMarkdownExtension } from 'common/filesystem/paths'
import { getUniqueId } from '../utils'
import { loadMarkdownFile } from '../filesystem/markdown'
import { isLinux } from '../config'
import { isLinux, isOsx } from '../config'
// TODO(refactor): Please see GH#1035.
@ -159,7 +159,8 @@ class Watcher {
ignorePermissionErrors: true,
// Just to be sure when a file is replaced with a directory don't watch recursively.
depth: type === 'file' ? 0 : undefined,
depth: type === 'file'
? (isOsx ? 1 : 0) : undefined,
// Please see GH#1043
awaitWriteFinish: {

View File

@ -18,6 +18,7 @@
:text-direction="textDirection"
></source-code>
</div>
<tab-notifications></tab-notifications>
</div>
</template>
@ -25,6 +26,7 @@
import Tabs from './tabs.vue'
import Editor from './editor.vue'
import SourceCode from './sourceCode.vue'
import TabNotifications from './notifications.vue'
export default {
props: {
@ -61,7 +63,8 @@ export default {
components: {
Tabs,
Editor,
SourceCode
SourceCode,
TabNotifications
}
}
</script>

View File

@ -0,0 +1,123 @@
<template>
<div
v-if="currentNotification"
class="editor-notifications"
:class="currentNotification.style"
:style="{'max-width': showSideBar ? `calc(100vw - ${sideBarWidth}px` : '100vw' }"
>
<div class="msg">
{{ currentNotification.msg }}
</div>
<div class="controls">
<div>
<span
class="inline-button"
v-if="currentNotification.showConfirm"
@click.stop="handleClick(true)"
>
Ok
</span>
<span
class="inline-button"
@click.stop="handleClick(false)"
>
<svg class="close-icon icon" aria-hidden="true">
<use id="default-close-icon" xlink:href="#icon-close-small"></use>
</svg>
</span>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data () {
return {}
},
computed: {
...mapState({
currentFile: state => state.editor.currentFile,
showSideBar: state => state.layout.showSideBar,
sideBarWidth: state => state.layout.sideBarWidth
}),
currentNotification () {
const notifications = this.currentFile.notifications
if (!notifications || notifications.length === 0) {
return null
}
return notifications[0]
}
},
methods: {
handleClick (status) {
const notifications = this.currentFile.notifications
if (!notifications || notifications.length === 0) {
console.error('notifications::handleClick: Cannot find notification on stack.')
return
}
const item = notifications.shift()
const action = item.action
if (action) {
action(status)
}
}
}
}
</script>
<style scoped>
.editor-notifications {
position: relative;
display: flex;
flex-direction: row;
max-height: 100px;
margin-top: 4px;
background: var(--notificationPrimaryBg);
color: var(--notificationPrimaryColor);
padding: 8px 10px;
user-select: none;
overflow: hidden;
&.warn {
background: var(--notificationWarningBg);
color: var(--notificationWarningColor);
}
&.crit {
background: var(--notificationErrorBg);
color: var(--notificationErrorColor);
}
}
.msg {
font-size: 13px;
flex: 1;
}
.controls {
display: flex;
flex-direction: column;
justify-content: center;
& > div {
display: flex;
flex-direction: row;
}
& .inline-button:not(:last-child) {
margin-right: 3px;
}
& .inline-button {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
font-size: 12px;
cursor: pointer;
border: 1px solid rgba(255, 255, 255, 0.1);
}
& .inline-button:hover {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.6);
}
}
</style>

View File

@ -96,29 +96,59 @@ const mutations = {
const { data, pathname } = change
const { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, encoding, markdown, filename } = data
const options = { encoding, lineEnding, adjustLineEndingOnSave }
// Create a new document and update few entires later.
const newFileState = getSingleFileState({ markdown, filename, pathname, options })
if (isMixedLineEndings) {
notice.notify({
title: 'Line Ending',
message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
type: 'primary',
time: 20000,
showConfirm: false
})
}
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
if (!tab) {
// The tab may be closed in the meanwhile.
console.error('LOAD_CHANGE: Cannot find tab in tab list.')
notice.notify({
title: 'Error loading tab',
message: 'There was an error while loading the file change because the tab cannot be found.',
type: 'error',
time: 20000,
showConfirm: false
})
return
}
// Upate file content but not tab id.
// Backup few entries that we need to restore later.
const oldId = tab.id
const oldNotifications = tab.notifications
let oldHistory = null
if (tab.history.index >= 0 && tab.history.stack.length >= 1) {
// Allow to restore the old document.
oldHistory = {
stack: [tab.history.stack[tab.history.index]],
index: 0
}
// Free reference from array
tab.history.index--
tab.history.stack.pop()
}
// Update file content and restore some entries.
Object.assign(tab, newFileState)
tab.id = oldId
tab.notifications = oldNotifications
if (oldHistory) {
tab.history = oldHistory
}
if (isMixedLineEndings) {
tab.notifications.push({
msg: `"${filename}" has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
showConfirm: false,
style: 'info',
exclusiveType: '',
action: () => {}
})
}
// Reload the editor if the tab is currently opened.
if (pathname === currentFile.pathname) {
state.currentFile = tab
const { id, cursor, history } = tab
@ -236,6 +266,44 @@ const mutations = {
// TODO: Remove "SET_GLOBAL_LINE_ENDING" because nowhere used.
SET_GLOBAL_LINE_ENDING (state, ending) {
state.lineEnding = ending
},
// Push a tab specific notification on stack that never disappears.
PUSH_TAB_NOTIFICATION (state, data) {
const defaultAction = () => {}
const { tabId, msg } = data
const action = data.action || defaultAction
const showConfirm = data.showConfirm || false
const style = data.style || 'info'
// Whether only one notification should exist.
const exclusiveType = data.exclusiveType || ''
const { tabs } = state
const tab = tabs.find(t => t.id === tabId)
if (!tab) {
console.error('PUSH_TAB_NOTIFICATION: Cannot find tab in tab list.')
return
}
const { notifications } = tab
// Remove the old notification if only one should exist.
if (exclusiveType) {
const index = notifications.findIndex(n => n.exclusiveType === exclusiveType)
if (index >= 0) {
// Reorder current notification
notifications.splice(index, 1)
}
}
// Push new notification on stack.
notifications.push({
msg,
showConfirm,
style,
exclusiveType,
action: action
})
}
}
@ -388,6 +456,9 @@ const actions = {
})
ipcRenderer.on('mt::tab-save-failure', (e, tabId, msg) => {
const { tabs } = state
const tab = tabs.find(t => t.id === tabId)
if (!tab) {
notice.notify({
title: 'Save failure',
message: msg,
@ -395,6 +466,15 @@ const actions = {
time: 20000,
showConfirm: false
})
return
}
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
commit('PUSH_TAB_NOTIFICATION', {
tabId,
msg: `There was an error while saving: ${msg}`,
style: 'crit'
})
})
},
@ -703,14 +783,10 @@ const actions = {
}
if (isMixedLineEndings) {
// TODO(watcher): Show (this) notification(s) per tab.
const { filename, lineEnding } = markdownDocument
notice.notify({
title: 'Line Ending',
message: `${filename} has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
type: 'primary',
time: 20000,
showConfirm: false
commit('PUSH_TAB_NOTIFICATION', {
tabId: id,
msg: `${filename}" has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`
})
}
},
@ -858,8 +934,8 @@ const actions = {
LINTEN_FOR_EXPORT_SUCCESS ({ commit }) {
ipcRenderer.on('AGANI::export-success', (e, { type, filePath }) => {
notice.notify({
title: 'Export',
message: `Export ${path.basename(filePath)} successfully`,
title: 'Exported successfully',
message: `Exported "${path.basename(filePath)}" successfully!`,
showConfirm: true
})
.then(() => {
@ -893,31 +969,28 @@ const actions = {
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
ipcRenderer.on('AGANI::update-file', (e, { type, change }) => {
// TODO: A new "changed" notification from different files overwrite the old notification
// and the old notification disappears. I think we should bind the notification to the tab.
// TODO: We should only load the changed content if the user want to reload the document.
const { tabs } = state
const { pathname } = change
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
if (tab) {
const { id, isSaved } = tab
const { id, isSaved, filename } = tab
switch (type) {
case 'unlink': {
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
notice.notify({
title: 'File Removed on Disk',
message: `${pathname} has been removed or moved.`,
type: 'warning',
time: 0,
showConfirm: false
commit('PUSH_TAB_NOTIFICATION', {
tabId: id,
msg: `"${filename}" has been removed on disk.`,
style: 'warn',
showConfirm: false,
exclusiveType: 'file_changed'
})
break
}
case 'add':
case 'change': {
const { autoSave } = rootState.preferences
const { filename } = change.data
if (autoSave) {
if (autoSaveTimers.has(id)) {
const timer = autoSaveTimers.get(id)
@ -933,16 +1006,16 @@ const actions = {
}
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
notice.clear()
notice.notify({
title: 'File Changed on Disk',
message: `${filename} has been changed on disk, do you want to reload it?`,
commit('PUSH_TAB_NOTIFICATION', {
tabId: id,
msg: `"${filename}" has been changed on disk. Do you want to reload it?`,
showConfirm: true,
time: 0
})
.then(() => {
exclusiveType: 'file_changed',
action: status => {
if (status) {
commit('LOAD_CHANGE', change)
}
}
})
break
}

View File

@ -30,7 +30,9 @@ export const defaultFileState = {
index: -1,
matches: [],
value: ''
}
},
// Per tab notifications
notifications: []
}
export const getOptionsFromState = file => {