Reorganized side bar menu
This commit is contained in:
parent
8cf0b87f5f
commit
2a865ddb44
@ -2,7 +2,7 @@
|
|||||||
<div class="side-bar flex flex--column">
|
<div class="side-bar flex flex--column">
|
||||||
<div class="side-title flex flex--row">
|
<div class="side-title flex flex--row">
|
||||||
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')" v-title="'Main menu'">
|
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')" v-title="'Main menu'">
|
||||||
<icon-arrow-left></icon-arrow-left>
|
<icon-dots-horizontal></icon-dots-horizontal>
|
||||||
</button>
|
</button>
|
||||||
<div class="side-title__title">
|
<div class="side-title__title">
|
||||||
{{panelName}}
|
{{panelName}}
|
||||||
@ -18,8 +18,8 @@
|
|||||||
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||||
<export-menu v-else-if="panel === 'export'"></export-menu>
|
<export-menu v-else-if="panel === 'export'"></export-menu>
|
||||||
<import-menu v-else-if="panel === 'import'"></import-menu>
|
<import-export-menu v-else-if="panel === 'importExport'"></import-export-menu>
|
||||||
<more-menu v-else-if="panel === 'more'"></more-menu>
|
<workspace-backup-menu v-else-if="panel === 'workspaceBackup'"></workspace-backup-menu>
|
||||||
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
||||||
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
||||||
</div>
|
</div>
|
||||||
@ -39,9 +39,8 @@ import WorkspacesMenu from './menus/WorkspacesMenu';
|
|||||||
import SyncMenu from './menus/SyncMenu';
|
import SyncMenu from './menus/SyncMenu';
|
||||||
import PublishMenu from './menus/PublishMenu';
|
import PublishMenu from './menus/PublishMenu';
|
||||||
import HistoryMenu from './menus/HistoryMenu';
|
import HistoryMenu from './menus/HistoryMenu';
|
||||||
import ExportMenu from './menus/ExportMenu';
|
import ImportExportMenu from './menus/ImportExportMenu';
|
||||||
import ImportMenu from './menus/ImportMenu';
|
import WorkspaceBackupMenu from './menus/WorkspaceBackupMenu';
|
||||||
import MoreMenu from './menus/MoreMenu';
|
|
||||||
import markdownSample from '../data/markdownSample.md';
|
import markdownSample from '../data/markdownSample.md';
|
||||||
import markdownConversionSvc from '../services/markdownConversionSvc';
|
import markdownConversionSvc from '../services/markdownConversionSvc';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
@ -54,8 +53,8 @@ const panelNames = {
|
|||||||
sync: 'Synchronize',
|
sync: 'Synchronize',
|
||||||
publish: 'Publish',
|
publish: 'Publish',
|
||||||
history: 'File history',
|
history: 'File history',
|
||||||
export: 'Export to disk',
|
importExport: 'Import/export',
|
||||||
import: 'Import from disk',
|
workspaceBackup: 'Workspace backup',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -66,9 +65,8 @@ export default {
|
|||||||
SyncMenu,
|
SyncMenu,
|
||||||
PublishMenu,
|
PublishMenu,
|
||||||
HistoryMenu,
|
HistoryMenu,
|
||||||
ExportMenu,
|
ImportExportMenu,
|
||||||
ImportMenu,
|
WorkspaceBackupMenu,
|
||||||
MoreMenu,
|
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
|
||||||
<menu-entry @click.native="exportMarkdown">
|
|
||||||
<icon-download slot="icon"></icon-download>
|
|
||||||
<div>Export as Markdown</div>
|
|
||||||
<span>Save plain text file.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="exportHtml">
|
|
||||||
<icon-download slot="icon"></icon-download>
|
|
||||||
<div>Export as HTML</div>
|
|
||||||
<span>Generate an HTML page from a template.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="exportPdf">
|
|
||||||
<icon-download slot="icon"></icon-download>
|
|
||||||
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export as PDF</div>
|
|
||||||
<span>Produce a PDF from an HTML template.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="exportPandoc">
|
|
||||||
<icon-download slot="icon"></icon-download>
|
|
||||||
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export with Pandoc</div>
|
|
||||||
<span>Convert to PDF, Word, EPUB...</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import MenuEntry from './common/MenuEntry';
|
|
||||||
import exportSvc from '../../services/exportSvc';
|
|
||||||
import store from '../../store';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
MenuEntry,
|
|
||||||
},
|
|
||||||
computed: mapGetters(['isSponsor']),
|
|
||||||
methods: {
|
|
||||||
exportMarkdown() {
|
|
||||||
const currentFile = store.getters['file/current'];
|
|
||||||
return exportSvc.exportToDisk(currentFile.id, 'md')
|
|
||||||
.catch(() => { /* Cancel */ });
|
|
||||||
},
|
|
||||||
exportHtml() {
|
|
||||||
return store.dispatch('modal/open', 'htmlExport')
|
|
||||||
.catch(() => { /* Cancel */ });
|
|
||||||
},
|
|
||||||
exportPdf() {
|
|
||||||
return store.dispatch('modal/open', 'pdfExport')
|
|
||||||
.catch(() => { /* Cancel */ });
|
|
||||||
},
|
|
||||||
exportPandoc() {
|
|
||||||
return store.dispatch('modal/open', 'pandocExport')
|
|
||||||
.catch(() => { /* Cancel */ });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -20,16 +20,39 @@
|
|||||||
<span>Convert an HTML file to Markdown.</span>
|
<span>Convert an HTML file to Markdown.</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="exportMarkdown">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
<div>Export as Markdown</div>
|
||||||
|
<span>Save plain text file.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="exportHtml">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
<div>Export as HTML</div>
|
||||||
|
<span>Generate an HTML page from a template.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="exportPdf">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export as PDF</div>
|
||||||
|
<span>Produce a PDF from an HTML template.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="exportPandoc">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export with Pandoc</div>
|
||||||
|
<span>Convert to PDF, Word, EPUB...</span>
|
||||||
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import TurndownService from 'turndown/lib/turndown.browser.umd';
|
import TurndownService from 'turndown/lib/turndown.browser.umd';
|
||||||
import htmlSanitizer from '../../libs/htmlSanitizer';
|
import htmlSanitizer from '../../libs/htmlSanitizer';
|
||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import Provider from '../../services/providers/common/Provider';
|
import Provider from '../../services/providers/common/Provider';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import workspaceSvc from '../../services/workspaceSvc';
|
import workspaceSvc from '../../services/workspaceSvc';
|
||||||
|
import exportSvc from '../../services/exportSvc';
|
||||||
|
|
||||||
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
||||||
|
|
||||||
@ -52,6 +75,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
},
|
},
|
||||||
|
computed: mapGetters(['isSponsor']),
|
||||||
methods: {
|
methods: {
|
||||||
async onImportMarkdown(evt) {
|
async onImportMarkdown(evt) {
|
||||||
const file = evt.target.files[0];
|
const file = evt.target.files[0];
|
||||||
@ -73,6 +97,23 @@ export default {
|
|||||||
});
|
});
|
||||||
store.commit('file/setCurrentId', item.id);
|
store.commit('file/setCurrentId', item.id);
|
||||||
},
|
},
|
||||||
|
exportMarkdown() {
|
||||||
|
const currentFile = store.getters['file/current'];
|
||||||
|
return exportSvc.exportToDisk(currentFile.id, 'md')
|
||||||
|
.catch(() => { /* Cancel */ });
|
||||||
|
},
|
||||||
|
exportHtml() {
|
||||||
|
return store.dispatch('modal/open', 'htmlExport')
|
||||||
|
.catch(() => { /* Cancel */ });
|
||||||
|
},
|
||||||
|
exportPdf() {
|
||||||
|
return store.dispatch('modal/open', 'pdfExport')
|
||||||
|
.catch(() => { /* Cancel */ });
|
||||||
|
},
|
||||||
|
exportPandoc() {
|
||||||
|
return store.dispatch('modal/open', 'pandocExport')
|
||||||
|
.catch(() => { /* Cancel */ });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
@ -75,13 +75,9 @@
|
|||||||
Markdown cheat sheet
|
Markdown cheat sheet
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('import')">
|
<menu-entry @click.native="setPanel('importExport')">
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
Import from disk
|
Import/export
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="setPanel('export')">
|
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
|
||||||
Export to disk
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="print">
|
<menu-entry @click.native="print">
|
||||||
<icon-printer slot="icon"></icon-printer>
|
<icon-printer slot="icon"></icon-printer>
|
||||||
@ -104,19 +100,10 @@
|
|||||||
<span>Manage access to your external accounts.</span>
|
<span>Manage access to your external accounts.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="exportWorkspace">
|
<menu-entry @click.native="setPanel('workspaceBackup')">
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
Export workspace backup
|
Workspace backup
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
|
||||||
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
|
||||||
<div class="menu-entry__icon flex flex--column flex--center">
|
|
||||||
<icon-content-save></icon-content-save>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex--column">
|
|
||||||
Import workspace backup
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<menu-entry @click.native="reset">
|
<menu-entry @click.native="reset">
|
||||||
<icon-logout slot="icon"></icon-logout>
|
<icon-logout slot="icon"></icon-logout>
|
||||||
<div>Reset application</div>
|
<div>Reset application</div>
|
||||||
@ -138,8 +125,6 @@ import UserImage from '../UserImage';
|
|||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
import userSvc from '../../services/userSvc';
|
import userSvc from '../../services/userSvc';
|
||||||
import backupSvc from '../../services/backupSvc';
|
|
||||||
import utils from '../../services/utils';
|
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -199,29 +184,6 @@ export default {
|
|||||||
print() {
|
print() {
|
||||||
window.print();
|
window.print();
|
||||||
},
|
},
|
||||||
onImportBackup(evt) {
|
|
||||||
const file = evt.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const text = e.target.result;
|
|
||||||
if (text.match(/\uFFFD/)) {
|
|
||||||
store.dispatch('notification/error', 'File is not readable.');
|
|
||||||
} else {
|
|
||||||
backupSvc.importBackup(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const blob = file.slice(0, 10000000);
|
|
||||||
reader.readAsText(blob);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
exportWorkspace() {
|
|
||||||
window.location.href = utils.addQueryParams('app', {
|
|
||||||
...utils.queryParams,
|
|
||||||
exportWorkspace: true,
|
|
||||||
}, true);
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
async settings() {
|
async settings() {
|
||||||
try {
|
try {
|
||||||
const settings = await store.dispatch('modal/open', 'settings');
|
const settings = await store.dispatch('modal/open', 'settings');
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
|
||||||
<menu-entry @click.native="settings">
|
|
||||||
<icon-settings slot="icon"></icon-settings>
|
|
||||||
<div>Settings</div>
|
|
||||||
<span>Tweak application and keyboard shortcuts.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="templates">
|
|
||||||
<icon-code-braces slot="icon"></icon-code-braces>
|
|
||||||
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
|
|
||||||
<span>Configure Handlebars templates for your exports.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="reset">
|
|
||||||
<icon-logout slot="icon"></icon-logout>
|
|
||||||
<div>Reset application</div>
|
|
||||||
<span>Sign out and clean all workspaces.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="exportWorkspace">
|
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
|
||||||
Export workspace backup
|
|
||||||
</menu-entry>
|
|
||||||
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
|
||||||
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
|
||||||
<div class="menu-entry__icon flex flex--column flex--center">
|
|
||||||
<icon-content-save></icon-content-save>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex--column">
|
|
||||||
Import workspace backup
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<hr>
|
|
||||||
<menu-entry href="editor" target="_blank">
|
|
||||||
<icon-open-in-new slot="icon"></icon-open-in-new>
|
|
||||||
<span>StackEdit 4 — deprecated</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="about">
|
|
||||||
<icon-help-circle slot="icon"></icon-help-circle>
|
|
||||||
<span>About StackEdit</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import MenuEntry from './common/MenuEntry';
|
|
||||||
import backupSvc from '../../services/backupSvc';
|
|
||||||
import utils from '../../services/utils';
|
|
||||||
import store from '../../store';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
MenuEntry,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
templateCount() {
|
|
||||||
return Object.keys(store.getters['data/allTemplatesById']).length;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onImportBackup(evt) {
|
|
||||||
const file = evt.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const text = e.target.result;
|
|
||||||
if (text.match(/\uFFFD/)) {
|
|
||||||
store.dispatch('notification/error', 'File is not readable.');
|
|
||||||
} else {
|
|
||||||
backupSvc.importBackup(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const blob = file.slice(0, 10000000);
|
|
||||||
reader.readAsText(blob);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
exportWorkspace() {
|
|
||||||
const url = utils.addQueryParams('app', {
|
|
||||||
...utils.queryParams,
|
|
||||||
exportWorkspace: true,
|
|
||||||
}, true);
|
|
||||||
window.location.href = url;
|
|
||||||
window.location.reload(true);
|
|
||||||
},
|
|
||||||
async settings() {
|
|
||||||
try {
|
|
||||||
const settings = await store.dispatch('modal/open', 'settings');
|
|
||||||
store.dispatch('data/setSettings', settings);
|
|
||||||
} catch (e) {
|
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async templates() {
|
|
||||||
try {
|
|
||||||
const { templates } = await store.dispatch('modal/open', 'templates');
|
|
||||||
store.dispatch('data/setTemplatesById', templates);
|
|
||||||
} catch (e) {
|
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async reset() {
|
|
||||||
try {
|
|
||||||
await store.dispatch('modal/open', 'reset');
|
|
||||||
window.location.href = '#reset=true';
|
|
||||||
window.location.reload();
|
|
||||||
} catch (e) {
|
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
about() {
|
|
||||||
store.dispatch('modal/open', 'about');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -21,7 +21,7 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div v-for="token in bloggerTokens" :key="token.sub">
|
<div v-for="token in bloggerTokens" :key="'blogger-' + token.sub">
|
||||||
<menu-entry @click.native="publishBlogger(token)">
|
<menu-entry @click.native="publishBlogger(token)">
|
||||||
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||||
<div>Publish to Blogger</div>
|
<div>Publish to Blogger</div>
|
||||||
|
55
src/components/menus/WorkspaceBackupMenu.vue
Normal file
55
src/components/menus/WorkspaceBackupMenu.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
||||||
|
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
||||||
|
<div class="menu-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-content-save></icon-content-save>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex--column">
|
||||||
|
Import workspace backup
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<menu-entry @click.native="exportWorkspace">
|
||||||
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
|
Export workspace backup
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MenuEntry from './common/MenuEntry';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
import store from '../../store';
|
||||||
|
import backupSvc from '../../services/backupSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onImportBackup(evt) {
|
||||||
|
const file = evt.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const text = e.target.result;
|
||||||
|
if (text.match(/\uFFFD/)) {
|
||||||
|
store.dispatch('notification/error', 'File is not readable.');
|
||||||
|
} else {
|
||||||
|
backupSvc.importBackup(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const blob = file.slice(0, 10000000);
|
||||||
|
reader.readAsText(blob);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exportWorkspace() {
|
||||||
|
window.location.href = utils.addQueryParams('app', {
|
||||||
|
...utils.queryParams,
|
||||||
|
exportWorkspace: true,
|
||||||
|
}, true);
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
5
src/icons/DotsHorizontal.vue
Normal file
5
src/icons/DotsHorizontal.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M 16,12C 16,10.8954 16.8954,10 18,10C 19.1046,10 20,10.8954 20,12C 20,13.1046 19.1046,14 18,14C 16.8954,14 16,13.1046 16,12 Z M 10,12C 10,10.8954 10.8954,10 12,10C 13.1046,10 14,10.8954 14,12C 14,13.1046 13.1046,14 12,14C 10.8954,14 10,13.1046 10,12 Z M 4,12C 4,10.8954 4.89543,10 6,10C 7.10457,10 8,10.8954 8,12C 8,13.1046 7.10457,14 6,14C 4.89543,14 4,13.1046 4,12 Z "/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -52,6 +52,7 @@ import FormatListChecks from './FormatListChecks';
|
|||||||
import CheckCircle from './CheckCircle';
|
import CheckCircle from './CheckCircle';
|
||||||
import ContentCopy from './ContentCopy';
|
import ContentCopy from './ContentCopy';
|
||||||
import Key from './Key';
|
import Key from './Key';
|
||||||
|
import DotsHorizontal from './DotsHorizontal';
|
||||||
|
|
||||||
Vue.component('iconProvider', Provider);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
@ -106,3 +107,4 @@ Vue.component('iconFormatListChecks', FormatListChecks);
|
|||||||
Vue.component('iconCheckCircle', CheckCircle);
|
Vue.component('iconCheckCircle', CheckCircle);
|
||||||
Vue.component('iconContentCopy', ContentCopy);
|
Vue.component('iconContentCopy', ContentCopy);
|
||||||
Vue.component('iconKey', Key);
|
Vue.component('iconKey', Key);
|
||||||
|
Vue.component('iconDotsHorizontal', DotsHorizontal);
|
||||||
|
@ -7,7 +7,6 @@ const clientId = GOOGLE_CLIENT_ID;
|
|||||||
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
||||||
const appsDomain = null;
|
const appsDomain = null;
|
||||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)
|
||||||
let googlePlusNotification = true;
|
|
||||||
|
|
||||||
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
|
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
|
||||||
const getDriveScopes = token => [token.driveFullAccess
|
const getDriveScopes = token => [token.driveFullAccess
|
||||||
@ -40,17 +39,18 @@ if (utils.queryParams.providerId === 'googleDrive') {
|
|||||||
* https://developers.google.com/people/api/rest/v1/people/get
|
* https://developers.google.com/people/api/rest/v1/people/get
|
||||||
*/
|
*/
|
||||||
const getUser = async (sub, token) => {
|
const getUser = async (sub, token) => {
|
||||||
|
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos`;
|
||||||
const { body } = await networkSvc.request(token
|
const { body } = await networkSvc.request(token
|
||||||
? {
|
? {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `https://people.googleapis.com/v1/people/${sub}`,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token.accessToken}`,
|
Authorization: `Bearer ${token.accessToken}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `https://people.googleapis.com/v1/people/${sub}?key=${apiKey}`,
|
url: `${url}&key=${apiKey}`,
|
||||||
}, true);
|
}, true);
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
@ -60,10 +60,12 @@ userSvc.setInfoResolver('google', subPrefix, async (sub) => {
|
|||||||
try {
|
try {
|
||||||
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
|
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
|
||||||
const body = await getUser(sub, googleToken);
|
const body = await getUser(sub, googleToken);
|
||||||
|
const name = body.names[0] || {};
|
||||||
|
const photo = body.photos[0] || {};
|
||||||
return {
|
return {
|
||||||
id: `${subPrefix}:${body.id}`,
|
id: `${subPrefix}:${sub}`,
|
||||||
name: body.displayName,
|
name: name.displayName,
|
||||||
imageUrl: (body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status !== 404) {
|
if (err.status !== 404) {
|
||||||
@ -141,41 +143,52 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build token object including scopes and sub
|
// Build token object including scopes and sub
|
||||||
const existingToken = store.getters['data/googleTokensBySub'][body.sub] || {
|
const existingToken = store.getters['data/googleTokensBySub'][body.sub];
|
||||||
scopes: [],
|
|
||||||
};
|
|
||||||
const mergedScopes = [...new Set([...scopes, ...existingToken.scopes])];
|
|
||||||
const token = {
|
const token = {
|
||||||
scopes: mergedScopes,
|
scopes,
|
||||||
accessToken,
|
accessToken,
|
||||||
expiresOn: Date.now() + (expiresIn * 1000),
|
expiresOn: Date.now() + (expiresIn * 1000),
|
||||||
idToken,
|
idToken,
|
||||||
sub: body.sub,
|
sub: body.sub,
|
||||||
name: existingToken.name || 'Unknown',
|
name: (existingToken || {}).name || 'Unknown',
|
||||||
isLogin: existingToken.isLogin || (!store.getters['workspace/mainWorkspaceToken'] &&
|
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
|
||||||
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1),
|
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||||
isSponsor: existingToken.isSponsor || false,
|
isSponsor: false,
|
||||||
isDrive: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||||
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
||||||
isBlogger: mergedScopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
||||||
isPhotos: mergedScopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
||||||
driveFullAccess: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call the user info endpoint
|
// Call the user info endpoint
|
||||||
const user = await getUser('me', token);
|
const user = await getUser('me', token);
|
||||||
if (user.displayName) {
|
const userId = user.resourceName.split('/')[1];
|
||||||
token.name = user.displayName;
|
const name = user.names[0] || {};
|
||||||
} else if (googlePlusNotification) {
|
const photo = user.photos[0] || {};
|
||||||
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo.');
|
if (name.displayName) {
|
||||||
googlePlusNotification = false;
|
token.name = name.displayName;
|
||||||
}
|
}
|
||||||
userSvc.addInfo({
|
userSvc.addInfo({
|
||||||
id: `${subPrefix}:${user.id}`,
|
id: `${subPrefix}:${userId}`,
|
||||||
name: user.displayName,
|
name: name.displayName,
|
||||||
imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (existingToken) {
|
||||||
|
// We probably retrieved a new token with restricted scopes.
|
||||||
|
// That's no problem, token will be refreshed later with merged scopes.
|
||||||
|
// Restore flags
|
||||||
|
Object.assign(token, {
|
||||||
|
isLogin: existingToken.isLogin || token.isLogin,
|
||||||
|
isSponsor: existingToken.isSponsor,
|
||||||
|
isDrive: existingToken.isDrive || token.isDrive,
|
||||||
|
isBlogger: existingToken.isBlogger || token.isBlogger,
|
||||||
|
isPhotos: existingToken.isPhotos || token.isPhotos,
|
||||||
|
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (token.isLogin) {
|
if (token.isLogin) {
|
||||||
try {
|
try {
|
||||||
token.isSponsor = (await networkSvc.request({
|
token.isSponsor = (await networkSvc.request({
|
||||||
|
@ -34,17 +34,24 @@ const empty = (id) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Item IDs that will be stored in the localStorage
|
// Item IDs that will be stored in the localStorage
|
||||||
const lsItemIdSet = new Set(constants.localStorageDataIds);
|
const localStorageIdSet = new Set(constants.localStorageDataIds);
|
||||||
|
|
||||||
// Getter/setter/patcher factories
|
// Getter/setter/patcher factories
|
||||||
const getter = id => state => ((lsItemIdSet.has(id)
|
const getter = id => (state) => {
|
||||||
? state.lsItemsById
|
const itemsById = localStorageIdSet.has(id)
|
||||||
: state.itemsById)[id] || {}).data || empty(id).data;
|
? state.lsItemsById
|
||||||
|
: state.itemsById;
|
||||||
|
if (itemsById[id]) {
|
||||||
|
return itemsById[id].data;
|
||||||
|
}
|
||||||
|
return empty(id).data;
|
||||||
|
};
|
||||||
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
|
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
|
||||||
const patcher = id => ({ state, commit }, data) => {
|
const patcher = id => ({ state, commit }, data) => {
|
||||||
const item = Object.assign(empty(id), (lsItemIdSet.has(id)
|
const itemsById = localStorageIdSet.has(id)
|
||||||
? state.lsItemsById
|
? state.lsItemsById
|
||||||
: state.itemsById)[id]);
|
: state.itemsById;
|
||||||
|
const item = Object.assign(empty(id), itemsById[id]);
|
||||||
commit('setItem', {
|
commit('setItem', {
|
||||||
...empty(id),
|
...empty(id),
|
||||||
data: typeof data === 'object' ? {
|
data: typeof data === 'object' ? {
|
||||||
@ -116,7 +123,7 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store item in itemsById or lsItemsById if its stored in the localStorage
|
// Store item in itemsById or lsItemsById if its stored in the localStorage
|
||||||
Vue.set(lsItemIdSet.has(item.id) ? lsItemsById : itemsById, item.id, item);
|
Vue.set(localStorageIdSet.has(item.id) ? lsItemsById : itemsById, item.id, item);
|
||||||
},
|
},
|
||||||
deleteItem({ itemsById }, id) {
|
deleteItem({ itemsById }, id) {
|
||||||
// Only used by localDbSvc to clean itemsById from object moved to localStorage
|
// Only used by localDbSvc to clean itemsById from object moved to localStorage
|
||||||
@ -196,6 +203,7 @@ export default {
|
|||||||
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
|
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
|
||||||
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
|
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
|
||||||
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
|
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
|
||||||
|
badgesByFeatureId: getter('badges'),
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setSettings: setter('settings'),
|
setSettings: setter('settings'),
|
||||||
|
Loading…
Reference in New Issue
Block a user