diff --git a/src/components/SideBar.vue b/src/components/SideBar.vue index 5d58775a..c7d1f9d5 100644 --- a/src/components/SideBar.vue +++ b/src/components/SideBar.vue @@ -2,7 +2,7 @@
-
+
Publish to Blogger
diff --git a/src/components/menus/WorkspaceBackupMenu.vue b/src/components/menus/WorkspaceBackupMenu.vue new file mode 100644 index 00000000..309f107c --- /dev/null +++ b/src/components/menus/WorkspaceBackupMenu.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/icons/DotsHorizontal.vue b/src/icons/DotsHorizontal.vue new file mode 100644 index 00000000..4376aeeb --- /dev/null +++ b/src/icons/DotsHorizontal.vue @@ -0,0 +1,5 @@ + diff --git a/src/icons/index.js b/src/icons/index.js index 1fc33d96..e09304d2 100644 --- a/src/icons/index.js +++ b/src/icons/index.js @@ -52,6 +52,7 @@ import FormatListChecks from './FormatListChecks'; import CheckCircle from './CheckCircle'; import ContentCopy from './ContentCopy'; import Key from './Key'; +import DotsHorizontal from './DotsHorizontal'; Vue.component('iconProvider', Provider); Vue.component('iconFormatBold', FormatBold); @@ -106,3 +107,4 @@ Vue.component('iconFormatListChecks', FormatListChecks); Vue.component('iconCheckCircle', CheckCircle); Vue.component('iconContentCopy', ContentCopy); Vue.component('iconKey', Key); +Vue.component('iconDotsHorizontal', DotsHorizontal); diff --git a/src/services/providers/helpers/googleHelper.js b/src/services/providers/helpers/googleHelper.js index 87a88b9e..6958a82a 100644 --- a/src/services/providers/helpers/googleHelper.js +++ b/src/services/providers/helpers/googleHelper.js @@ -7,7 +7,6 @@ const clientId = GOOGLE_CLIENT_ID; const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk'; const appsDomain = null; const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h) -let googlePlusNotification = true; const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata']; const getDriveScopes = token => [token.driveFullAccess @@ -40,17 +39,18 @@ if (utils.queryParams.providerId === 'googleDrive') { * https://developers.google.com/people/api/rest/v1/people/get */ const getUser = async (sub, token) => { + const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos`; const { body } = await networkSvc.request(token ? { method: 'GET', - url: `https://people.googleapis.com/v1/people/${sub}`, + url, headers: { Authorization: `Bearer ${token.accessToken}`, }, } : { method: 'GET', - url: `https://people.googleapis.com/v1/people/${sub}?key=${apiKey}`, + url: `${url}&key=${apiKey}`, }, true); return body; }; @@ -60,10 +60,12 @@ userSvc.setInfoResolver('google', subPrefix, async (sub) => { try { const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0]; const body = await getUser(sub, googleToken); + const name = body.names[0] || {}; + const photo = body.photos[0] || {}; return { - id: `${subPrefix}:${body.id}`, - name: body.displayName, - imageUrl: (body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'), + id: `${subPrefix}:${sub}`, + name: name.displayName, + imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'), }; } catch (err) { if (err.status !== 404) { @@ -141,41 +143,52 @@ export default { } // Build token object including scopes and sub - const existingToken = store.getters['data/googleTokensBySub'][body.sub] || { - scopes: [], - }; - const mergedScopes = [...new Set([...scopes, ...existingToken.scopes])]; + const existingToken = store.getters['data/googleTokensBySub'][body.sub]; const token = { - scopes: mergedScopes, + scopes, accessToken, expiresOn: Date.now() + (expiresIn * 1000), idToken, sub: body.sub, - 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, + 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, }; // Call the user info endpoint const user = await getUser('me', token); - if (user.displayName) { - token.name = user.displayName; - } else if (googlePlusNotification) { - store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo.'); - googlePlusNotification = false; + const userId = user.resourceName.split('/')[1]; + const name = user.names[0] || {}; + const photo = user.photos[0] || {}; + if (name.displayName) { + token.name = name.displayName; } userSvc.addInfo({ - id: `${subPrefix}:${user.id}`, - name: user.displayName, - imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'), + id: `${subPrefix}:${userId}`, + name: name.displayName, + 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) { try { token.isSponsor = (await networkSvc.request({ diff --git a/src/store/data.js b/src/store/data.js index 45c9175a..e8e20857 100644 --- a/src/store/data.js +++ b/src/store/data.js @@ -34,17 +34,24 @@ const empty = (id) => { }; // 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 -const getter = id => state => ((lsItemIdSet.has(id) - ? state.lsItemsById - : state.itemsById)[id] || {}).data || empty(id).data; +const getter = id => (state) => { + const itemsById = localStorageIdSet.has(id) + ? 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 patcher = id => ({ state, commit }, data) => { - const item = Object.assign(empty(id), (lsItemIdSet.has(id) + const itemsById = localStorageIdSet.has(id) ? state.lsItemsById - : state.itemsById)[id]); + : state.itemsById; + const item = Object.assign(empty(id), itemsById[id]); commit('setItem', { ...empty(id), data: typeof data === 'object' ? { @@ -116,7 +123,7 @@ export default { }); // 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) { // Only used by localDbSvc to clean itemsById from object moved to localStorage @@ -196,6 +203,7 @@ export default { gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {}, wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {}, zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {}, + badgesByFeatureId: getter('badges'), }, actions: { setSettings: setter('settings'),