Added account management modal
This commit is contained in:
parent
91f8cf3c10
commit
8cf0b87f5f
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.history
|
||||
.idea
|
||||
npm-debug.log*
|
||||
.vscode
|
||||
stackedit_v4
|
||||
|
13
src/assets/iconGoogle.svg
Normal file
13
src/assets/iconGoogle.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48">
|
||||
<defs>
|
||||
<path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/>
|
||||
</defs>
|
||||
<clipPath id="b">
|
||||
<use xlink:href="#a" overflow="visible"/>
|
||||
</clipPath>
|
||||
<path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/>
|
||||
<path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/>
|
||||
<path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/>
|
||||
<path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 729 B |
@ -36,6 +36,7 @@ import ImageModal from './modals/ImageModal';
|
||||
import SyncManagementModal from './modals/SyncManagementModal';
|
||||
import PublishManagementModal from './modals/PublishManagementModal';
|
||||
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
|
||||
import AccountManagementModal from './modals/AccountManagementModal';
|
||||
import SponsorModal from './modals/SponsorModal';
|
||||
|
||||
// Providers
|
||||
@ -86,6 +87,7 @@ export default {
|
||||
SyncManagementModal,
|
||||
PublishManagementModal,
|
||||
WorkspaceManagementModal,
|
||||
AccountManagementModal,
|
||||
SponsorModal,
|
||||
// Providers
|
||||
GooglePhotoModal,
|
||||
|
@ -56,7 +56,6 @@ const panelNames = {
|
||||
history: 'File history',
|
||||
export: 'Export to disk',
|
||||
import: 'Import from disk',
|
||||
more: 'More',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -88,8 +88,44 @@
|
||||
Print
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('more')">
|
||||
More...
|
||||
<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="accounts">
|
||||
<icon-key slot="icon"></icon-key>
|
||||
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div>
|
||||
<span>Manage access to your external accounts.</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>
|
||||
<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="about">
|
||||
<icon-help-circle slot="icon"></icon-help-circle>
|
||||
About StackEdit
|
||||
</menu-entry>
|
||||
</div>
|
||||
</template>
|
||||
@ -102,6 +138,8 @@ import UserImage from '../UserImage';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import userSvc from '../../services/userSvc';
|
||||
import backupSvc from '../../services/backupSvc';
|
||||
import utils from '../../services/utils';
|
||||
import store from '../../store';
|
||||
|
||||
export default {
|
||||
@ -131,6 +169,13 @@ export default {
|
||||
publishLocationCount() {
|
||||
return Object.keys(store.getters['publishLocation/current']).length;
|
||||
},
|
||||
templateCount() {
|
||||
return Object.keys(store.getters['data/allTemplatesById']).length;
|
||||
},
|
||||
accountCount() {
|
||||
return Object.values(store.getters['data/tokensByType'])
|
||||
.reduce((count, tokensBySub) => count + Object.values(tokensBySub).length, 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', {
|
||||
@ -154,6 +199,64 @@ export default {
|
||||
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() {
|
||||
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 accounts() {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'accountManagement');
|
||||
} 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>
|
||||
|
@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div class="side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||
<p><b>{{currentFileName}}</b> can not be published as it's a temporary file.</p>
|
||||
<p>{{currentFileName}} can't be published as it's a temporary file.</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="side-bar__info" v-if="noToken">
|
||||
<p>You have to <b>link an account</b> to start publishing files.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="publishLocations.length">
|
||||
<p><b>{{currentFileName}}</b> is already published.</p>
|
||||
<p>{{currentFileName}} is already published.</p>
|
||||
<menu-entry @click.native="requestPublish">
|
||||
<icon-upload slot="icon"></icon-upload>
|
||||
<div>Publish now</div>
|
||||
<span>Update current file publications.</span>
|
||||
<span>Update publications for {{currentFileName}}.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="managePublish">
|
||||
<icon-view-list slot="icon"></icon-view-list>
|
||||
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div>
|
||||
<span>Manage current file publication locations.</span>
|
||||
<span>Manage publication locations for {{currentFileName}}.</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
@ -157,7 +157,7 @@ export default {
|
||||
return Object.keys(this.publishLocations).length;
|
||||
},
|
||||
currentFileName() {
|
||||
return store.getters['file/current'].name;
|
||||
return `"${store.getters['file/current'].name}"`;
|
||||
},
|
||||
bloggerTokens() {
|
||||
return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isBlogger);
|
||||
|
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||
<p><b>{{currentFileName}}</b> can not be synced as it's a temporary file.</p>
|
||||
<p>{{currentFileName}} can't be synced as it's a temporary file.</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="side-bar__info" v-if="noToken">
|
||||
<p>You have to <b>link an account</b> to start syncing files.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="syncLocations.length">
|
||||
<p><b>{{currentFileName}}</b> is already synchronized.</p>
|
||||
<p>{{currentFileName}} is already synchronized.</p>
|
||||
<menu-entry @click.native="requestSync">
|
||||
<icon-sync slot="icon"></icon-sync>
|
||||
<div>Synchronize now</div>
|
||||
@ -17,7 +17,7 @@
|
||||
<menu-entry @click.native="manageSync">
|
||||
<icon-view-list slot="icon"></icon-view-list>
|
||||
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div>
|
||||
<span>Manage current file synchronized locations.</span>
|
||||
<span>Manage synchronized locations for {{currentFileName}}.</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
@ -139,7 +139,7 @@ export default {
|
||||
return Object.keys(this.syncLocations).length;
|
||||
},
|
||||
currentFileName() {
|
||||
return store.getters['file/current'].name;
|
||||
return `"${store.getters['file/current'].name}"`;
|
||||
},
|
||||
dropboxTokens() {
|
||||
return tokensToArray(store.getters['data/dropboxTokensBySub']);
|
||||
|
@ -9,19 +9,19 @@
|
||||
<hr>
|
||||
<menu-entry @click.native="addCouchdbWorkspace">
|
||||
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
||||
<span>Add a <b>CouchDB</b> backed workspace</span>
|
||||
<span>Add a <b>CouchDB</b> workspace</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGithubWorkspace">
|
||||
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
|
||||
<span>Add a <b>GitHub</b> backed workspace</span>
|
||||
<span>Add a <b>GitHub</b> workspace</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGitlabWorkspace">
|
||||
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
|
||||
<span>Add a <b>GitLab</b> backed workspace</span>
|
||||
<span>Add a <b>GitLab</b> workspace</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGoogleDriveWorkspace">
|
||||
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
||||
<span>Add a <b>Google Drive</b> backed workspace</span>
|
||||
<span>Add a <b>Google Drive</b> workspace</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="manageWorkspaces">
|
||||
<icon-database slot="icon"></icon-database>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
|
||||
<div class="modal__content">
|
||||
<div class="logo-background"></div>
|
||||
<small>© 2013-2018 Dock5 Software Ltd.<br>v{{version}}</small>
|
||||
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
|
||||
<hr>
|
||||
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
|
||||
<br>
|
||||
|
267
src/components/modals/AccountManagementModal.vue
Normal file
267
src/components/modals/AccountManagementModal.vue
Normal file
@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<modal-inner class="modal__inner-1--account-management" aria-label="Manage external accounts">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-key></icon-key>
|
||||
</div>
|
||||
<p v-if="entries.length">StackEdit has access to the following external accounts:</p>
|
||||
<p v-else>StackEdit has no access to any external account yet.</p>
|
||||
<div>
|
||||
<div class="account-entry flex flex--column" v-for="entry in entries" :key="entry.token.sub">
|
||||
<div class="account-entry__header flex flex--row flex--align-center">
|
||||
<div class="account-entry__icon flex flex--column flex--center">
|
||||
<icon-provider :provider-id="entry.providerId"></icon-provider>
|
||||
</div>
|
||||
<div class="account-entry__description">
|
||||
{{entry.name}}
|
||||
</div>
|
||||
<div class="account-entry__buttons flex flex--row flex--center">
|
||||
<button class="account-entry__button button" @click="remove(entry)" v-title="'Remove access'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-entry__row">
|
||||
<span v-if="entry.userId">
|
||||
<b>User ID:</b>
|
||||
{{entry.userId}}
|
||||
</span>
|
||||
<span v-if="entry.url">
|
||||
<b>URL:</b>
|
||||
{{entry.url}}
|
||||
</span>
|
||||
<span v-if="entry.scopes">
|
||||
<b>Scopes:</b>
|
||||
{{entry.scopes.join(', ')}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<menu-entry @click.native="addBloggerAccount">
|
||||
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||
<span>Add Blogger account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addDropboxAccount">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<span>Add Dropbox account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGithubAccount">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<span>Add GitHub account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGitlabAccount">
|
||||
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
|
||||
<span>Add GitLab account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGoogleDriveAccount">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<span>Add Google Drive account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGooglePhotosAccount">
|
||||
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||
<span>Add Google Photos account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addWordpressAccount">
|
||||
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
||||
<span>Add WordPress account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addZendeskAccount">
|
||||
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||
<span>Add Zendesk account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import MenuEntry from '../menus/common/MenuEntry';
|
||||
import store from '../../store';
|
||||
import utils from '../../services/utils';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
|
||||
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
|
||||
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
MenuEntry,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
entries() {
|
||||
return [
|
||||
...Object.values(store.getters['data/googleTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'google',
|
||||
userId: token.sub,
|
||||
name: token.name,
|
||||
scopes: ['openid', 'profile', ...token.scopes
|
||||
.map(scope => scope.replace(/^https:\/\/www.googleapis.com\/auth\//, ''))],
|
||||
})),
|
||||
...Object.values(store.getters['data/couchdbTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'couchdb',
|
||||
url: token.dbUrl,
|
||||
name: token.name,
|
||||
})),
|
||||
...Object.values(store.getters['data/dropboxTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'dropbox',
|
||||
userId: token.sub,
|
||||
name: token.name,
|
||||
})),
|
||||
...Object.values(store.getters['data/githubTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'github',
|
||||
userId: token.sub,
|
||||
name: token.name,
|
||||
scopes: token.scopes,
|
||||
})),
|
||||
...Object.values(store.getters['data/gitlabTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'gitlab',
|
||||
url: token.serverUrl,
|
||||
userId: token.sub,
|
||||
name: token.name,
|
||||
scopes: ['api'],
|
||||
})),
|
||||
...Object.values(store.getters['data/wordpressTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'wordpress',
|
||||
userId: token.sub,
|
||||
name: token.name,
|
||||
scopes: ['global'],
|
||||
})),
|
||||
...Object.values(store.getters['data/zendeskTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'zendesk',
|
||||
url: `https://${token.subdomain}.zendesk.com/`,
|
||||
userId: token.sub,
|
||||
name: token.name,
|
||||
scopes: ['read', 'hc:write'],
|
||||
})),
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async remove(entry) {
|
||||
const tokensBySub = utils.deepCopy(store.getters[`data/${entry.providerId}TokensBySub`]);
|
||||
delete tokensBySub[entry.token.sub];
|
||||
await store.dispatch('data/patchTokensByType', {
|
||||
[entry.providerId]: tokensBySub,
|
||||
});
|
||||
},
|
||||
async addBloggerAccount() {
|
||||
try {
|
||||
await googleHelper.addBloggerAccount();
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addDropboxAccount() {
|
||||
try {
|
||||
await store.dispatch('modal/open', { type: 'dropboxAccount' });
|
||||
await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGithubAccount() {
|
||||
try {
|
||||
await store.dispatch('modal/open', { type: 'githubAccount' });
|
||||
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGitlabAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGoogleDriveAccount() {
|
||||
try {
|
||||
await store.dispatch('modal/open', { type: 'googleDriveAccount' });
|
||||
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGooglePhotosAccount() {
|
||||
try {
|
||||
await googleHelper.addPhotosAccount();
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addWordpressAccount() {
|
||||
try {
|
||||
await wordpressHelper.addAccount();
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addZendeskAccount() {
|
||||
try {
|
||||
const { subdomain, clientId } = await store.dispatch('modal/open', { type: 'zendeskAccount' });
|
||||
await zendeskHelper.addAccount(subdomain, clientId);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.account-entry {
|
||||
margin: 1.5em 0;
|
||||
height: auto;
|
||||
font-size: 17px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
$button-size: 30px;
|
||||
|
||||
.account-entry__header {
|
||||
line-height: $button-size;
|
||||
}
|
||||
|
||||
.account-entry__row {
|
||||
border-top: 1px solid $hr-color;
|
||||
opacity: 0.5;
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
.account-entry__icon {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
margin-right: 0.75rem;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.account-entry__description {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.account-entry__buttons {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.account-entry__button {
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -58,8 +58,10 @@ export default modalTemplate({
|
||||
this.config.reject();
|
||||
callback(null);
|
||||
},
|
||||
addGooglePhotosAccount() {
|
||||
return googleHelper.addPhotosAccount();
|
||||
async addGooglePhotosAccount() {
|
||||
try {
|
||||
await googleHelper.addPhotosAccount();
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGooglePhotos(token) {
|
||||
const { callback } = this.config;
|
||||
|
@ -95,9 +95,7 @@ $small-button-size: 22px;
|
||||
}
|
||||
|
||||
.publish-entry__row {
|
||||
margin-top: 1px;
|
||||
padding-top: 1px;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
||||
border-top: 1px solid $hr-color;
|
||||
line-height: $small-button-size;
|
||||
}
|
||||
|
||||
|
@ -99,9 +99,7 @@ $small-button-size: 22px;
|
||||
}
|
||||
|
||||
.sync-entry__row {
|
||||
margin-top: 1px;
|
||||
padding-top: 1px;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
||||
border-top: 1px solid $hr-color;
|
||||
line-height: $small-button-size;
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__info">
|
||||
<b>ProTip:</b> A workspace is accessible <b>offline</b> once it has been opened for the first time.
|
||||
<b>ProTip:</b> Workspaces are accessible offline, try it!
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
@ -149,9 +149,7 @@ $small-button-size: 22px;
|
||||
}
|
||||
|
||||
.workspace-entry__row {
|
||||
margin-top: 1px;
|
||||
padding-top: 1px;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
||||
border-top: 1px solid $hr-color;
|
||||
line-height: $small-button-size;
|
||||
}
|
||||
|
||||
|
5
src/icons/Key.vue
Normal file
5
src/icons/Key.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 7,14C 5.9,14 5,13.1 5,12C 5,10.9 5.9,10 7,10C 8.1,10 9,10.9 9,12C 9,13.1 8.1,14 7,14 Z M 12.65,10C 11.83,7.67 9.61,6 7,6C 3.69,6 1,8.69 1,12C 1,15.31 3.69,18 7,18C 9.61,18 11.83,16.33 12.65,14L 17,14L 17,18L 21,18L 21,14L 23,14L 23,10L 12.65,10 Z "/>
|
||||
</svg>
|
||||
</template>
|
@ -63,6 +63,10 @@ export default {
|
||||
background-image: url(../assets/iconGitlab.svg);
|
||||
}
|
||||
|
||||
.icon-provider--google {
|
||||
background-image: url(../assets/iconGoogle.svg);
|
||||
}
|
||||
|
||||
.icon-provider--dropbox {
|
||||
background-image: url(../assets/iconDropbox.svg);
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ import Magnify from './Magnify';
|
||||
import FormatListChecks from './FormatListChecks';
|
||||
import CheckCircle from './CheckCircle';
|
||||
import ContentCopy from './ContentCopy';
|
||||
import Key from './Key';
|
||||
|
||||
Vue.component('iconProvider', Provider);
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
@ -104,3 +105,4 @@ Vue.component('iconMagnify', Magnify);
|
||||
Vue.component('iconFormatListChecks', FormatListChecks);
|
||||
Vue.component('iconCheckCircle', CheckCircle);
|
||||
Vue.component('iconContentCopy', ContentCopy);
|
||||
Vue.component('iconKey', Key);
|
||||
|
@ -6,7 +6,7 @@ import userSvc from '../../userSvc';
|
||||
const clientId = GOOGLE_CLIENT_ID;
|
||||
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
||||
const appsDomain = null;
|
||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google 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'];
|
||||
@ -37,20 +37,20 @@ if (utils.queryParams.providerId === 'googleDrive') {
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developers.google.com/+/web/api/rest/latest/people/get
|
||||
* https://developers.google.com/people/api/rest/v1/people/get
|
||||
*/
|
||||
const getUser = async (sub, token) => {
|
||||
const { body } = await networkSvc.request(token
|
||||
? {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/plus/v1/people/${sub}`,
|
||||
url: `https://people.googleapis.com/v1/people/${sub}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/plus/v1/people/${sub}?key=${apiKey}`,
|
||||
url: `https://people.googleapis.com/v1/people/${sub}?key=${apiKey}`,
|
||||
}, true);
|
||||
return body;
|
||||
};
|
||||
@ -141,22 +141,25 @@ export default {
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
scopes,
|
||||
scopes: mergedScopes,
|
||||
accessToken,
|
||||
expiresOn: Date.now() + (expiresIn * 1000),
|
||||
idToken,
|
||||
sub: body.sub,
|
||||
name: (existingToken || {}).name || 'Unknown',
|
||||
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||
isSponsor: false,
|
||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
||||
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
||||
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
||||
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
||||
name: existingToken.name || 'Unknown',
|
||||
isLogin: existingToken.isLogin || (!store.getters['workspace/mainWorkspaceToken'] &&
|
||||
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1),
|
||||
isSponsor: existingToken.isSponsor || false,
|
||||
isDrive: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
||||
isBlogger: mergedScopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
||||
isPhotos: mergedScopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
||||
driveFullAccess: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
||||
};
|
||||
|
||||
// Call the user info endpoint
|
||||
@ -173,20 +176,6 @@ export default {
|
||||
imageUrl: (user.image.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) {
|
||||
try {
|
||||
token.isSponsor = (await networkSvc.request({
|
||||
@ -202,7 +191,7 @@ export default {
|
||||
}
|
||||
|
||||
// Add token to google tokens
|
||||
store.dispatch('data/addGoogleToken', token);
|
||||
await store.dispatch('data/addGoogleToken', token);
|
||||
return token;
|
||||
},
|
||||
async refreshToken(token, scopes = []) {
|
||||
|
@ -75,7 +75,7 @@ const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
|
||||
helpers,
|
||||
isAdditional: true,
|
||||
});
|
||||
const additionalTemplates = {
|
||||
const defaultTemplates = {
|
||||
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
||||
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
||||
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
||||
@ -153,7 +153,7 @@ export default {
|
||||
templatesById: getter('templates'),
|
||||
allTemplatesById: (state, { templatesById }) => ({
|
||||
...templatesById,
|
||||
...additionalTemplates,
|
||||
...defaultTemplates,
|
||||
}),
|
||||
lastCreated: getter('lastCreated'),
|
||||
lastOpened: getter('lastOpened'),
|
||||
@ -238,7 +238,7 @@ export default {
|
||||
...templatesById,
|
||||
};
|
||||
// We don't store additional templates
|
||||
Object.keys(additionalTemplates).forEach((id) => {
|
||||
Object.keys(defaultTemplates).forEach((id) => {
|
||||
delete templatesToCommit[id];
|
||||
});
|
||||
commit('setItem', itemTemplate('templates', templatesToCommit));
|
||||
|
@ -8,7 +8,7 @@ $line-height-title: 1.33;
|
||||
$font-size-monospace: 0.85em;
|
||||
$highlighting-color: #ff0;
|
||||
$selection-highlighting-color: #ff9632;
|
||||
$info-bg: transparentize($selection-highlighting-color, 0.85);
|
||||
$info-bg: #ffad3326;
|
||||
$code-border-radius: 3px;
|
||||
$link-color: #0c93e4;
|
||||
$error-color: #f31;
|
||||
|
@ -450,7 +450,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<a href="app" title="The app">The app</a> – <a href="https://community.stackedit.io" target="_blank" title="The app">Community</a><br>
|
||||
Copyright 2013-2018 <a href="https://twitter.com/benweet" target="_blank">Benoit Schweblin</a><br>
|
||||
Copyright 2013-2019 <a href="https://twitter.com/benweet" target="_blank">Benoit Schweblin</a><br>
|
||||
Licensed under an
|
||||
<a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a> –
|
||||
<a href="privacy_policy.html" target="_blank">Privacy Policy</a>
|
||||
|
Loading…
Reference in New Issue
Block a user