GitLab provider (part 1)

This commit is contained in:
Benoit Schweblin 2018-09-19 09:59:22 +01:00
parent fd6ac907bb
commit 2e832fd766
85 changed files with 2059 additions and 810 deletions

12
src/assets/iconGitlab.svg Normal file
View File

@ -0,0 +1,12 @@
<svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1"
xmlns="http://www.w3.org/2000/svg"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<path d="M14.581,28.019l5.369,-16.526l-10.738,0l5.369,16.526l0,0Z" style="fill:#e24329;"/>
<path d="M14.581,28.019l-5.37,-16.526l-7.525,0l12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
<path d="M1.686,11.493l-1.632,5.022c-0.148,0.458 0.015,0.96 0.404,1.243l14.123,10.261l-12.895,-16.526l0,0Z" style="fill:#fca326;"/>
<path d="M1.686,11.493l7.526,0l-3.235,-9.953c-0.166,-0.512 -0.89,-0.512 -1.057,0l-3.234,9.953l0,0Z" style="fill:#e24329;"/>
<path d="M14.581,28.019l5.369,-16.526l7.526,0l-12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
<path d="M27.476,11.493l1.631,5.022c0.149,0.458 -0.014,0.96 -0.404,1.243l-14.122,10.261l12.895,-16.526l0,0Z" style="fill:#fca326;"/>
<path d="M27.476,11.493l-7.526,0l3.234,-9.953c0.167,-0.512 0.891,-0.512 1.058,0l3.234,9.953l0,0Z" style="fill:#e24329;"/>
</svg>

After

Width:  |  Height:  |  Size: 1005 B

View File

@ -21,6 +21,7 @@ import syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc'; import networkSvc from '../services/networkSvc';
import sponsorSvc from '../services/sponsorSvc'; import sponsorSvc from '../services/sponsorSvc';
import tempFileSvc from '../services/tempFileSvc'; import tempFileSvc from '../services/tempFileSvc';
import store from '../store';
import './common/vueGlobals'; import './common/vueGlobals';
const themeClasses = { const themeClasses = {
@ -41,7 +42,7 @@ export default {
}), }),
computed: { computed: {
classes() { classes() {
const result = themeClasses[this.$store.getters['data/computedSettings'].colorTheme]; const result = themeClasses[store.getters['data/computedSettings'].colorTheme];
return Array.isArray(result) ? result : themeClasses.light; return Array.isArray(result) ? result : themeClasses.light;
}, },
}, },
@ -57,7 +58,7 @@ export default {
window.location.reload(); window.location.reload();
} else if (err && err.message !== 'RELOAD') { } else if (err && err.message !== 'RELOAD') {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); store.dispatch('notification/error', err);
} }
} }
}, },

View File

@ -12,6 +12,7 @@
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import store from '../store';
export default { export default {
computed: { computed: {
@ -24,7 +25,7 @@ export default {
methods: { methods: {
close(item = null) { close(item = null) {
this.resolve(item); this.resolve(item);
this.$store.dispatch('contextMenu/close'); store.dispatch('contextMenu/close');
}, },
}, },
}; };

View File

@ -12,6 +12,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import CommentList from './gutters/CommentList'; import CommentList from './gutters/CommentList';
import EditorNewDiscussionButton from './gutters/EditorNewDiscussionButton'; import EditorNewDiscussionButton from './gutters/EditorNewDiscussionButton';
import store from '../store';
export default { export default {
components: { components: {
@ -52,11 +53,11 @@ export default {
editorElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true))); editorElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
editorElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false))); editorElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
editorElt.addEventListener('click', onDiscussionEvt((discussionId) => { editorElt.addEventListener('click', onDiscussionEvt((discussionId) => {
this.$store.commit('discussion/setCurrentDiscussionId', discussionId); store.commit('discussion/setCurrentDiscussionId', discussionId);
})); }));
this.$watch( this.$watch(
() => this.$store.state.discussion.currentDiscussionId, () => store.state.discussion.currentDiscussionId,
(discussionId, oldDiscussionId) => { (discussionId, oldDiscussionId) => {
if (oldDiscussionId) { if (oldDiscussionId) {
editorElt.querySelectorAll(`.discussion-editor-highlighting--${oldDiscussionId}`) editorElt.querySelectorAll(`.discussion-editor-highlighting--${oldDiscussionId}`)

View File

@ -29,6 +29,7 @@
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import ExplorerNode from './ExplorerNode'; import ExplorerNode from './ExplorerNode';
import explorerSvc from '../services/explorerSvc'; import explorerSvc from '../services/explorerSvc';
import store from '../store';
export default { export default {
components: { components: {
@ -55,16 +56,16 @@ export default {
editItem() { editItem() {
const node = this.selectedNode; const node = this.selectedNode;
if (!node.isTrash && !node.isTemp) { if (!node.isTrash && !node.isTemp) {
this.$store.commit('explorer/setEditingId', node.item.id); store.commit('explorer/setEditingId', node.item.id);
} }
}, },
}, },
created() { created() {
this.$watch( this.$watch(
() => this.$store.getters['file/current'].id, () => store.getters['file/current'].id,
(currentFileId) => { (currentFileId) => {
this.$store.commit('explorer/setSelectedId', currentFileId); store.commit('explorer/setSelectedId', currentFileId);
this.$store.dispatch('explorer/openNode', currentFileId); store.dispatch('explorer/openNode', currentFileId);
}, { }, {
immediate: true, immediate: true,
}, },

View File

@ -21,6 +21,7 @@
import { mapMutations, mapActions } from 'vuex'; import { mapMutations, mapActions } from 'vuex';
import workspaceSvc from '../services/workspaceSvc'; import workspaceSvc from '../services/workspaceSvc';
import explorerSvc from '../services/explorerSvc'; import explorerSvc from '../services/explorerSvc';
import store from '../store';
export default { export default {
name: 'explorer-node', // Required for recursivity name: 'explorer-node', // Required for recursivity
@ -36,35 +37,35 @@ export default {
return `${(this.depth + 1) * 15}px`; return `${(this.depth + 1) * 15}px`;
}, },
isSelected() { isSelected() {
return this.$store.getters['explorer/selectedNode'] === this.node; return store.getters['explorer/selectedNode'] === this.node;
}, },
isEditing() { isEditing() {
return this.$store.getters['explorer/editingNode'] === this.node; return store.getters['explorer/editingNode'] === this.node;
}, },
isDragTarget() { isDragTarget() {
return this.$store.getters['explorer/dragTargetNode'] === this.node; return store.getters['explorer/dragTargetNode'] === this.node;
}, },
isDragTargetFolder() { isDragTargetFolder() {
return this.$store.getters['explorer/dragTargetNodeFolder'] === this.node; return store.getters['explorer/dragTargetNodeFolder'] === this.node;
}, },
isOpen() { isOpen() {
return this.$store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot; return store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
}, },
newChild() { newChild() {
return this.$store.getters['explorer/newChildNodeParent'] === this.node return store.getters['explorer/newChildNodeParent'] === this.node
&& this.$store.state.explorer.newChildNode; && store.state.explorer.newChildNode;
}, },
newChildName: { newChildName: {
get() { get() {
return this.$store.state.explorer.newChildNode.item.name; return store.state.explorer.newChildNode.item.name;
}, },
set(value) { set(value) {
this.$store.commit('explorer/setNewItemName', value); store.commit('explorer/setNewItemName', value);
}, },
}, },
editingNodeName: { editingNodeName: {
get() { get() {
return this.$store.getters['explorer/editingNode'].item.name; return store.getters['explorer/editingNode'].item.name;
}, },
set(value) { set(value) {
this.editingValue = value.trim(); this.editingValue = value.trim();
@ -79,25 +80,25 @@ export default {
'setDragTarget', 'setDragTarget',
]), ]),
select(id = this.node.item.id, doOpen = true) { select(id = this.node.item.id, doOpen = true) {
const node = this.$store.getters['explorer/nodeMap'][id]; const node = store.getters['explorer/nodeMap'][id];
if (!node) { if (!node) {
return false; return false;
} }
this.$store.commit('explorer/setSelectedId', id); store.commit('explorer/setSelectedId', id);
if (doOpen) { if (doOpen) {
// Prevent from freezing the UI while loading the file // Prevent from freezing the UI while loading the file
setTimeout(() => { setTimeout(() => {
if (node.isFolder) { if (node.isFolder) {
this.$store.commit('explorer/toggleOpenNode', id); store.commit('explorer/toggleOpenNode', id);
} else { } else {
this.$store.commit('file/setCurrentId', id); store.commit('file/setCurrentId', id);
} }
}, 10); }, 10);
} }
return true; return true;
}, },
async submitNewChild(cancel) { async submitNewChild(cancel) {
const { newChildNode } = this.$store.state.explorer; const { newChildNode } = store.state.explorer;
if (!cancel && !newChildNode.isNil && newChildNode.item.name) { if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
try { try {
if (newChildNode.isFolder) { if (newChildNode.isFolder) {
@ -111,10 +112,10 @@ export default {
// Cancel // Cancel
} }
} }
this.$store.commit('explorer/setNewItem', null); store.commit('explorer/setNewItem', null);
}, },
async submitEdit(cancel) { async submitEdit(cancel) {
const { item } = this.$store.getters['explorer/editingNode']; const { item } = store.getters['explorer/editingNode'];
const value = this.editingValue; const value = this.editingValue;
this.setEditingId(null); this.setEditingId(null);
if (!cancel && item.id && value) { if (!cancel && item.id && value) {
@ -133,14 +134,14 @@ export default {
evt.preventDefault(); evt.preventDefault();
return; return;
} }
this.$store.commit('explorer/setDragSourceId', this.node.item.id); store.commit('explorer/setDragSourceId', this.node.item.id);
// Fix for Firefox // Fix for Firefox
// See https://stackoverflow.com/a/3977637/1333165 // See https://stackoverflow.com/a/3977637/1333165
evt.dataTransfer.setData('Text', ''); evt.dataTransfer.setData('Text', '');
}, },
onDrop() { onDrop() {
const sourceNode = this.$store.getters['explorer/dragSourceNode']; const sourceNode = store.getters['explorer/dragSourceNode'];
const targetNode = this.$store.getters['explorer/dragTargetNodeFolder']; const targetNode = store.getters['explorer/dragTargetNodeFolder'];
this.setDragTarget(); this.setDragTarget();
if (!sourceNode.isNil if (!sourceNode.isNil
&& !targetNode.isNil && !targetNode.isNil
@ -156,7 +157,7 @@ export default {
if (this.select(undefined, false)) { if (this.select(undefined, false)) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
const item = await this.$store.dispatch('contextMenu/open', { const item = await store.dispatch('contextMenu/open', {
coordinates: { coordinates: {
left: evt.clientX, left: evt.clientX,
top: evt.clientY, top: evt.clientY,

View File

@ -223,7 +223,7 @@ export default {
} }
}, },
close() { close() {
this.$store.commit('findReplace/setType'); store.commit('findReplace/setType');
}, },
onEscape() { onEscape() {
editorSvc.clEditor.focus(); editorSvc.clEditor.focus();
@ -260,7 +260,7 @@ export default {
this.onKeyup = (evt) => { this.onKeyup = (evt) => {
if (evt.which === 27) { if (evt.which === 27) {
// Esc key // Esc key
this.$store.commit('findReplace/setType'); store.commit('findReplace/setType');
} }
}; };
window.addEventListener('keyup', this.onKeyup); window.addEventListener('keyup', this.onKeyup);

View File

@ -63,6 +63,7 @@ import CurrentDiscussion from './gutters/CurrentDiscussion';
import FindReplace from './FindReplace'; import FindReplace from './FindReplace';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import markdownConversionSvc from '../services/markdownConversionSvc'; import markdownConversionSvc from '../services/markdownConversionSvc';
import store from '../store';
export default { export default {
components: { components: {
@ -96,7 +97,7 @@ export default {
'layoutSettings', 'layoutSettings',
]), ]),
showFindReplace() { showFindReplace() {
return !!this.$store.state.findReplace.type; return !!store.state.findReplace.type;
}, },
}, },
methods: { methods: {

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="modal" v-if="config" @keydown.esc="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut"> <div class="modal" v-if="config" @keydown.esc="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
<div class="modal__sponsor-banner" v-if="!isSponsor">
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>, please consider
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
</div>
<component v-if="currentModalComponent" :is="currentModalComponent"></component> <component v-if="currentModalComponent" :is="currentModalComponent"></component>
<modal-inner v-else aria-label="Dialog"> <modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div> <div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
@ -15,6 +19,10 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import simpleModals from '../data/simpleModals'; import simpleModals from '../data/simpleModals';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import googleHelper from '../services/providers/helpers/googleHelper';
import store from '../store';
import ModalInner from './modals/common/ModalInner'; import ModalInner from './modals/common/ModalInner';
import FilePropertiesModal from './modals/FilePropertiesModal'; import FilePropertiesModal from './modals/FilePropertiesModal';
import SettingsModal from './modals/SettingsModal'; import SettingsModal from './modals/SettingsModal';
@ -46,6 +54,11 @@ import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal'; import GithubPublishModal from './modals/providers/GithubPublishModal';
import GistSyncModal from './modals/providers/GistSyncModal'; import GistSyncModal from './modals/providers/GistSyncModal';
import GistPublishModal from './modals/providers/GistPublishModal'; import GistPublishModal from './modals/providers/GistPublishModal';
import GitlabAccountModal from './modals/providers/GitlabAccountModal';
import GitlabOpenModal from './modals/providers/GitlabOpenModal';
import GitlabPublishModal from './modals/providers/GitlabPublishModal';
import GitlabSaveModal from './modals/providers/GitlabSaveModal';
import GitlabWorkspaceModal from './modals/providers/GitlabWorkspaceModal';
import WordpressPublishModal from './modals/providers/WordpressPublishModal'; import WordpressPublishModal from './modals/providers/WordpressPublishModal';
import BloggerPublishModal from './modals/providers/BloggerPublishModal'; import BloggerPublishModal from './modals/providers/BloggerPublishModal';
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal'; import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
@ -54,7 +67,7 @@ import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal'; import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal';
import CouchdbCredentialsModal from './modals/providers/CouchdbCredentialsModal'; import CouchdbCredentialsModal from './modals/providers/CouchdbCredentialsModal';
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield') const getTabbables = container => container.querySelectorAll('a[href], button, .textfield, input[type=checkbox]')
// Filter enabled and visible element // Filter enabled and visible element
.cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable')); .cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable'));
@ -90,6 +103,11 @@ export default {
GithubPublishModal, GithubPublishModal,
GistSyncModal, GistSyncModal,
GistPublishModal, GistPublishModal,
GitlabAccountModal,
GitlabOpenModal,
GitlabPublishModal,
GitlabSaveModal,
GitlabWorkspaceModal,
WordpressPublishModal, WordpressPublishModal,
BloggerPublishModal, BloggerPublishModal,
BloggerPagePublishModal, BloggerPagePublishModal,
@ -99,6 +117,9 @@ export default {
CouchdbCredentialsModal, CouchdbCredentialsModal,
}, },
computed: { computed: {
...mapGetters([
'isSponsor',
]),
...mapGetters('modal', [ ...mapGetters('modal', [
'config', 'config',
]), ]),
@ -118,6 +139,19 @@ export default {
}, },
}, },
methods: { methods: {
async sponsor() {
try {
if (!store.getters['workspace/sponsorToken']) {
// User has to sign in
await store.dispatch('modal/open', 'signInForSponsorship');
await googleHelper.signin();
syncSvc.requestSync();
}
if (!store.getters.isSponsor) {
await store.dispatch('modal/open', 'sponsor');
}
} catch (e) { /* cancel */ }
},
onEscape() { onEscape() {
this.config.reject(); this.config.reject();
editorSvc.clEditor.focus(); editorSvc.clEditor.focus();
@ -135,25 +169,17 @@ export default {
} }
}, },
onFocusInOut(evt) { onFocusInOut(evt) {
const isFocusIn = evt.type === 'focusin'; const { parentNode } = evt.target;
if (evt.target.parentNode && evt.target.parentNode.parentNode) { if (parentNode && parentNode.parentNode) {
// Focus effect // Focus effect
if (evt.target.parentNode.classList.contains('form-entry__field') if (parentNode.classList.contains('form-entry__field')
&& evt.target.parentNode.parentNode.classList.contains('form-entry')) { && parentNode.parentNode.classList.contains('form-entry')) {
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn); parentNode.parentNode.classList.toggle(
'form-entry--focused',
evt.type === 'focusin',
);
} }
} }
if (isFocusIn && this.config) {
const modalInner = this.$el.querySelector('.modal__inner-2');
let { target } = evt;
while (target) {
if (target === modalInner) {
return;
}
target = target.parentNode;
}
this.config.reject();
}
}, },
}, },
mounted() { mounted() {
@ -188,6 +214,18 @@ export default {
} }
} }
.modal__sponsor-banner {
position: fixed;
z-index: 1;
width: 100%;
color: darken($error-color, 10%);
background-color: transparentize(lighten($error-color, 33%), 0.1);
font-size: 0.9em;
line-height: 1.33;
text-align: center;
padding: 0.25em 1em;
}
.modal__inner-1 { .modal__inner-1 {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
@ -291,7 +329,7 @@ export default {
.form-entry__label { .form-entry__label {
display: block; display: block;
font-size: 0.9rem; font-size: 0.9rem;
color: #a0a0a0; color: #808080;
.form-entry--focused & { .form-entry--focused & {
color: darken($link-color, 10%); color: darken($link-color, 10%);
@ -307,17 +345,19 @@ export default {
} }
.form-entry__field { .form-entry__field {
border: 1px solid #d8d8d8; border: 1px solid #b0b0b0;
border-radius: $border-radius-base; border-radius: $border-radius-base;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
.form-entry--focused & { .form-entry--focused & {
border-color: $link-color; border-color: $link-color;
box-shadow: 0 0 0 2.5px transparentize($link-color, 0.67);
} }
.form-entry--error & { .form-entry--error & {
border-color: $error-color; border-color: $error-color;
box-shadow: 0 0 0 2.5px transparentize($error-color, 0.67);
} }
} }
@ -353,7 +393,7 @@ export default {
.form-entry__info { .form-entry__info {
font-size: 0.75em; font-size: 0.75em;
opacity: 0.5; opacity: 0.67;
line-height: 1.4; line-height: 1.4;
margin: 0.25em 0; margin: 0.25em 0;
} }

View File

@ -119,11 +119,11 @@ export default {
})); }));
}, },
isSyncPossible() { isSyncPossible() {
return this.$store.getters['workspace/syncToken'] || return store.getters['workspace/syncToken'] ||
this.$store.getters['syncLocation/current'].length; store.getters['syncLocation/current'].length;
}, },
showSpinner() { showSpinner() {
return !this.$store.state.queue.isEmpty; return !store.state.queue.isEmpty;
}, },
titleWidth() { titleWidth() {
if (!this.mounted) { if (!this.mounted) {
@ -152,7 +152,7 @@ export default {
return result; return result;
}, },
editCancelTrigger() { editCancelTrigger() {
const current = this.$store.getters['file/current']; const current = store.getters['file/current'];
return utils.serializeObject([ return utils.serializeObject([
current.id, current.id,
current.name, current.name,
@ -187,7 +187,7 @@ export default {
} }
}, },
pagedownClick(name) { pagedownClick(name) {
if (this.$store.getters['content/isCurrentEditable']) { if (store.getters['content/isCurrentEditable']) {
editorSvc.pagedownEditor.uiManager.doClick(name); editorSvc.pagedownEditor.uiManager.doClick(name);
} }
}, },
@ -197,11 +197,11 @@ export default {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length); this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
} else { } else {
const title = this.title.trim(); const title = this.title.trim();
this.title = this.$store.getters['file/current'].name; this.title = store.getters['file/current'].name;
if (title) { if (title) {
try { try {
await workspaceSvc.storeItem({ await workspaceSvc.storeItem({
...this.$store.getters['file/current'], ...store.getters['file/current'],
name: title, name: title,
}); });
} catch (e) { } catch (e) {

View File

@ -21,6 +21,7 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import CommentList from './gutters/CommentList'; import CommentList from './gutters/CommentList';
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton'; import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
import store from '../store';
const appUri = `${window.location.protocol}//${window.location.host}`; const appUri = `${window.location.protocol}//${window.location.host}`;
@ -84,11 +85,11 @@ export default {
previewElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true))); previewElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
previewElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false))); previewElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
previewElt.addEventListener('click', onDiscussionEvt((discussionId) => { previewElt.addEventListener('click', onDiscussionEvt((discussionId) => {
this.$store.commit('discussion/setCurrentDiscussionId', discussionId); store.commit('discussion/setCurrentDiscussionId', discussionId);
})); }));
this.$watch( this.$watch(
() => this.$store.state.discussion.currentDiscussionId, () => store.state.discussion.currentDiscussionId,
(discussionId, oldDiscussionId) => { (discussionId, oldDiscussionId) => {
if (oldDiscussionId) { if (oldDiscussionId) {
previewElt.querySelectorAll(`.discussion-preview-highlighting--${oldDiscussionId}`) previewElt.querySelectorAll(`.discussion-preview-highlighting--${oldDiscussionId}`)

View File

@ -44,6 +44,7 @@ import ImportMenu from './menus/ImportMenu';
import MoreMenu from './menus/MoreMenu'; 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';
const panelNames = { const panelNames = {
menu: 'Menu', menu: 'Menu',
@ -75,10 +76,10 @@ export default {
}), }),
computed: { computed: {
panel() { panel() {
if (this.$store.state.light) { if (store.state.light) {
return null; // No menu in light mode return null; // No menu in light mode
} }
const result = this.$store.getters['data/layoutSettings'].sideBarPanel; const result = store.getters['data/layoutSettings'].sideBarPanel;
return panelNames[result] ? result : 'menu'; return panelNames[result] ? result : 'menu';
}, },
panelName() { panelName() {
@ -173,7 +174,7 @@ export default {
padding: 10px; padding: 10px;
margin: -10px -10px 10px; margin: -10px -10px 10px;
background-color: $info-bg; background-color: $info-bg;
font-size: 0.9em; font-size: 0.95em;
p { p {
margin: 10px; margin: 10px;

View File

@ -51,6 +51,7 @@
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import store from '../store';
const steps = [ const steps = [
'welcome', 'welcome',
@ -106,7 +107,7 @@ export default {
}); });
}, },
finish() { finish() {
this.$store.dispatch('data/patchLayoutSettings', { store.dispatch('data/patchLayoutSettings', {
welcomeTourFinished: true, welcomeTourFinished: true,
}); });
}, },
@ -116,7 +117,7 @@ export default {
}, },
mounted() { mounted() {
this.$watch( this.$watch(
() => this.$store.getters['layout/styles'], () => store.getters['layout/styles'],
() => this.updatePositions(), () => this.updatePositions(),
{ immediate: true }, { immediate: true },
); );

View File

@ -5,16 +5,22 @@
<script> <script>
import userSvc from '../services/userSvc'; import userSvc from '../services/userSvc';
import store from '../store';
export default { export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
url() { url() {
userSvc.getInfo(this.userId); const userInfo = store.state.userInfo.itemsById[this.userId];
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`; return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
}, },
}, },
watch: {
userId: {
handler: userId => userSvc.getInfo(userId),
immediate: true,
},
},
}; };
</script> </script>

View File

@ -4,15 +4,21 @@
<script> <script>
import userSvc from '../services/userSvc'; import userSvc from '../services/userSvc';
import store from '../store';
export default { export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
name() { name() {
userSvc.getInfo(this.userId); const userInfo = store.state.userInfo.itemsById[this.userId];
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
return userInfo ? userInfo.name : 'Someone'; return userInfo ? userInfo.name : 'Someone';
}, },
}, },
watch: {
userId: {
handler: userId => userSvc.getInfo(userId),
immediate: true,
},
},
}; };
</script> </script>

View File

@ -27,6 +27,7 @@ import UserImage from '../UserImage';
import UserName from '../UserName'; import UserName from '../UserName';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import htmlSanitizer from '../../libs/htmlSanitizer'; import htmlSanitizer from '../../libs/htmlSanitizer';
import store from '../../store';
export default { export default {
components: { components: {
@ -36,8 +37,8 @@ export default {
props: ['comment'], props: ['comment'],
computed: { computed: {
showReply() { showReply() {
return this.comment === this.$store.getters['discussion/currentDiscussionLastComment'] && return this.comment === store.getters['discussion/currentDiscussionLastComment'] &&
!this.$store.state.discussion.isCommenting; !store.state.discussion.isCommenting;
}, },
text() { text() {
return htmlSanitizer.sanitizeHtml(editorSvc.converter.render(this.comment.text)); return htmlSanitizer.sanitizeHtml(editorSvc.converter.render(this.comment.text));
@ -49,8 +50,8 @@ export default {
]), ]),
async removeComment() { async removeComment() {
try { try {
await this.$store.dispatch('modal/open', 'commentDeletion'); await store.dispatch('modal/open', 'commentDeletion');
this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment }); store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
} catch (e) { } catch (e) {
// Cancel // Cancel
} }
@ -59,7 +60,7 @@ export default {
mounted() { mounted() {
const isSticky = this.$el.parentNode.classList.contains('sticky-comment'); const isSticky = this.$el.parentNode.classList.contains('sticky-comment');
if (isSticky) { if (isSticky) {
const commentId = this.$store.getters['discussion/currentDiscussionLastCommentId']; const commentId = store.getters['discussion/currentDiscussionLastCommentId'];
const scrollerElt = this.$el.querySelector('.comment__text-inner'); const scrollerElt = this.$el.querySelector('.comment__text-inner');
let scrollerMirrorElt; let scrollerMirrorElt;

View File

@ -13,6 +13,7 @@ import { mapState, mapGetters, mapMutations } from 'vuex';
import Comment from './Comment'; import Comment from './Comment';
import NewComment from './NewComment'; import NewComment from './NewComment';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import store from '../../store';
import utils from '../../services/utils'; import utils from '../../services/utils';
export default { export default {
@ -63,7 +64,7 @@ export default {
'setCurrentDiscussionId', 'setCurrentDiscussionId',
]), ]),
updateTops() { updateTops() {
const layoutSettings = this.$store.getters['data/layoutSettings']; const layoutSettings = store.getters['data/layoutSettings'];
const minTop = -2; const minTop = -2;
let minCommentTop = minTop; let minCommentTop = minTop;
const getTop = (discussion, commentElt1, commentElt2, isCurrent) => { const getTop = (discussion, commentElt1, commentElt2, isCurrent) => {
@ -126,15 +127,15 @@ export default {
{ immediate: true }, { immediate: true },
); );
const layoutSettings = this.$store.getters['data/layoutSettings']; const layoutSettings = store.getters['data/layoutSettings'];
this.scrollerElt = layoutSettings.showEditor this.scrollerElt = layoutSettings.showEditor
? editorSvc.editorElt.parentNode ? editorSvc.editorElt.parentNode
: editorSvc.previewElt.parentNode; : editorSvc.previewElt.parentNode;
this.updateSticky = () => { this.updateSticky = () => {
const commitIfDifferent = (value) => { const commitIfDifferent = (value) => {
if (this.$store.state.discussion.stickyComment !== value) { if (store.state.discussion.stickyComment !== value) {
this.$store.commit('discussion/setStickyComment', value); store.commit('discussion/setStickyComment', value);
} }
}; };
let height = 0; let height = 0;

View File

@ -33,6 +33,7 @@ import editorSvc from '../../services/editorSvc';
import animationSvc from '../../services/animationSvc'; import animationSvc from '../../services/animationSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc'; import markdownConversionSvc from '../../services/markdownConversionSvc';
import StickyComment from './StickyComment'; import StickyComment from './StickyComment';
import store from '../../store';
export default { export default {
components: { components: {
@ -72,7 +73,7 @@ export default {
]), ]),
goToDiscussion(discussionId = this.currentDiscussionId) { goToDiscussion(discussionId = this.currentDiscussionId) {
this.setCurrentDiscussionId(discussionId); this.setCurrentDiscussionId(discussionId);
const layoutSettings = this.$store.getters['data/layoutSettings']; const layoutSettings = store.getters['data/layoutSettings'];
const discussion = this.currentFileDiscussions[discussionId]; const discussion = this.currentFileDiscussions[discussionId];
const coordinates = layoutSettings.showEditor const coordinates = layoutSettings.showEditor
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end) ? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
@ -98,8 +99,8 @@ export default {
}, },
async removeDiscussion() { async removeDiscussion() {
try { try {
await this.$store.dispatch('modal/open', 'discussionDeletion'); await store.dispatch('modal/open', 'discussionDeletion');
this.$store.dispatch('discussion/cleanCurrentFile', { store.dispatch('discussion/cleanCurrentFile', {
filterDiscussion: this.currentDiscussion, filterDiscussion: this.currentDiscussion,
}); });
} catch (e) { } catch (e) {

View File

@ -7,6 +7,7 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import store from '../../store';
export default { export default {
data: () => ({ data: () => ({
@ -23,7 +24,7 @@ export default {
let offset; let offset;
// Show the button if content is not a revision and has the focus // Show the button if content is not a revision and has the focus
if ( if (
!this.$store.state.content.revisionContent && !store.state.content.revisionContent &&
editorSvc.clEditor.selectionMgr.hasFocus() editorSvc.clEditor.selectionMgr.hasFocus()
) { ) {
this.selection = editorSvc.getTrimmedSelection(); this.selection = editorSvc.getTrimmedSelection();

View File

@ -28,6 +28,8 @@ import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc'; import markdownConversionSvc from '../../services/markdownConversionSvc';
import utils from '../../services/utils'; import utils from '../../services/utils';
import userSvc from '../../services/userSvc';
import store from '../../store';
export default { export default {
components: { components: {
@ -36,8 +38,10 @@ export default {
computed: { computed: {
...mapGetters('workspace', [ ...mapGetters('workspace', [
'loginToken', 'loginToken',
'userId',
]), ]),
userId() {
return userSvc.getCurrentUserId();
},
}, },
methods: { methods: {
...mapMutations('discussion', [ ...mapMutations('discussion', [
@ -47,13 +51,13 @@ export default {
'cancelNewComment', 'cancelNewComment',
]), ]),
addComment() { addComment() {
const text = this.$store.state.discussion.newCommentText.trim(); const text = store.state.discussion.newCommentText.trim();
if (text.length) { if (text.length) {
if (text.length > 2000) { if (text.length > 2000) {
this.$store.dispatch('notification/error', 'Comment is too long.'); store.dispatch('notification/error', 'Comment is too long.');
} else { } else {
// Create comment // Create comment
const discussionId = this.$store.state.discussion.currentDiscussionId; const discussionId = store.state.discussion.currentDiscussionId;
const comment = { const comment = {
discussionId, discussionId,
sub: this.userId, sub: this.userId,
@ -62,20 +66,20 @@ export default {
}; };
const patch = { const patch = {
comments: { comments: {
...this.$store.getters['content/current'].comments, ...store.getters['content/current'].comments,
[utils.uid()]: comment, [utils.uid()]: comment,
}, },
}; };
// Create discussion // Create discussion
if (discussionId === this.$store.state.discussion.newDiscussionId) { if (discussionId === store.state.discussion.newDiscussionId) {
patch.discussions = { patch.discussions = {
...this.$store.getters['content/current'].discussions, ...store.getters['content/current'].discussions,
[discussionId]: this.$store.getters['discussion/newDiscussion'], [discussionId]: store.getters['discussion/newDiscussion'],
}; };
} }
this.$store.dispatch('content/patchCurrent', patch); store.dispatch('content/patchCurrent', patch);
this.$store.commit('discussion/setNewCommentText'); store.commit('discussion/setNewCommentText');
this.$store.commit('discussion/setIsCommenting'); store.commit('discussion/setIsCommenting');
} }
} }
}, },
@ -91,28 +95,28 @@ export default {
), ),
sectionParser: text => markdownConversionSvc sectionParser: text => markdownConversionSvc
.parseSections(editorSvc.converter, text).sections, .parseSections(editorSvc.converter, text).sections,
content: this.$store.state.discussion.newCommentText, content: store.state.discussion.newCommentText,
selectionStart: this.$store.state.discussion.newCommentSelection.start, selectionStart: store.state.discussion.newCommentSelection.start,
selectionEnd: this.$store.state.discussion.newCommentSelection.end, selectionEnd: store.state.discussion.newCommentSelection.end,
getCursorFocusRatio: () => 0.2, getCursorFocusRatio: () => 0.2,
}); });
clEditor.on('focus', () => this.setNewCommentFocus(true)); clEditor.on('focus', () => this.setNewCommentFocus(true));
// Save typed content and selection // Save typed content and selection
clEditor.on('contentChanged', value => clEditor.on('contentChanged', value =>
this.$store.commit('discussion/setNewCommentText', value)); store.commit('discussion/setNewCommentText', value));
clEditor.selectionMgr.on('selectionChanged', (start, end) => clEditor.selectionMgr.on('selectionChanged', (start, end) =>
this.$store.commit('discussion/setNewCommentSelection', { store.commit('discussion/setNewCommentSelection', {
start, end, start, end,
})); }));
const isSticky = this.$el.parentNode.classList.contains('sticky-comment'); const isSticky = this.$el.parentNode.classList.contains('sticky-comment');
const isVisible = () => isSticky || this.$store.state.discussion.stickyComment === null; const isVisible = () => isSticky || store.state.discussion.stickyComment === null;
this.$watch( this.$watch(
() => this.$store.state.discussion.currentDiscussionId, () => store.state.discussion.currentDiscussionId,
() => this.$nextTick(() => { () => this.$nextTick(() => {
if (isVisible() && this.$store.state.discussion.newCommentFocus) { if (isVisible() && store.state.discussion.newCommentFocus) {
clEditor.focus(); clEditor.focus();
} }
}), }),
@ -139,11 +143,11 @@ export default {
(visible) => { (visible) => {
clEditor.toggleEditable(visible); clEditor.toggleEditable(visible);
if (visible) { if (visible) {
const text = this.$store.state.discussion.newCommentText; const text = store.state.discussion.newCommentText;
clEditor.setContent(text); clEditor.setContent(text);
const selection = this.$store.state.discussion.newCommentSelection; const selection = store.state.discussion.newCommentSelection;
clEditor.selectionMgr.setSelectionStartEnd(selection.start, selection.end); clEditor.selectionMgr.setSelectionStartEnd(selection.start, selection.end);
if (this.$store.state.discussion.newCommentFocus) { if (store.state.discussion.newCommentFocus) {
clEditor.focus(); clEditor.focus();
} }
} }
@ -151,7 +155,7 @@ export default {
{ immediate: true }, { immediate: true },
); );
this.$watch( this.$watch(
() => this.$store.state.discussion.newCommentText, () => store.state.discussion.newCommentText,
newCommentText => clEditor.setContent(newCommentText), newCommentText => clEditor.setContent(newCommentText),
); );
} }

View File

@ -7,6 +7,7 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import store from '../../store';
export default { export default {
data: () => ({ data: () => ({
@ -23,7 +24,7 @@ export default {
let offset; let offset;
// Show the button if content is not a revision and preview selection is not empty // Show the button if content is not a revision and preview selection is not empty
if ( if (
!this.$store.state.content.revisionContent && !store.state.content.revisionContent &&
editorSvc.previewSelectionRange editorSvc.previewSelectionRange
) { ) {
this.selection = editorSvc.getTrimmedSelection(); this.selection = editorSvc.getTrimmedSelection();
@ -45,7 +46,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
editorSvc.$on('previewSelectionRange', () => this.checkSelection()); editorSvc.$on('previewSelectionRange', () => this.checkSelection());
this.$watch( this.$watch(
() => this.$store.getters['layout/styles'].previewWidth, () => store.getters['layout/styles'].previewWidth,
() => this.checkSelection(), () => this.checkSelection(),
); );
this.checkSelection(); this.checkSelection();

View File

@ -27,6 +27,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import exportSvc from '../../services/exportSvc'; import exportSvc from '../../services/exportSvc';
import store from '../../store';
export default { export default {
components: { components: {
@ -35,20 +36,20 @@ export default {
computed: mapGetters(['isSponsor']), computed: mapGetters(['isSponsor']),
methods: { methods: {
exportMarkdown() { exportMarkdown() {
const currentFile = this.$store.getters['file/current']; const currentFile = store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md') return exportSvc.exportToDisk(currentFile.id, 'md')
.catch(() => { /* Cancel */ }); .catch(() => { /* Cancel */ });
}, },
exportHtml() { exportHtml() {
return this.$store.dispatch('modal/open', 'htmlExport') return store.dispatch('modal/open', 'htmlExport')
.catch(() => { /* Cancel */ }); .catch(() => { /* Cancel */ });
}, },
exportPdf() { exportPdf() {
return this.$store.dispatch('modal/open', 'pdfExport') return store.dispatch('modal/open', 'pdfExport')
.catch(() => { /* Cancel */ }); .catch(() => { /* Cancel */ });
}, },
exportPandoc() { exportPandoc() {
return this.$store.dispatch('modal/open', 'pandocExport') return store.dispatch('modal/open', 'pandocExport')
.catch(() => { /* Cancel */ }); .catch(() => { /* Cancel */ });
}, },
}, },

View File

@ -55,6 +55,7 @@ import PreviewClassApplier from '../common/PreviewClassApplier';
import utils from '../../services/utils'; import utils from '../../services/utils';
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 store from '../../store';
let editorClassAppliers = []; let editorClassAppliers = [];
let previewClassAppliers = []; let previewClassAppliers = [];
@ -102,14 +103,14 @@ export default {
return providerRegistry.providersById[this.syncLocation.providerId].name; return providerRegistry.providersById[this.syncLocation.providerId].name;
}, },
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return store.getters['file/current'].name;
}, },
historyContext() { historyContext() {
const { syncLocation } = this; const { syncLocation } = this;
if (syncLocation) { if (syncLocation) {
const provider = providerRegistry.providersById[syncLocation.providerId]; const provider = providerRegistry.providersById[syncLocation.providerId];
const token = provider.getToken(syncLocation); const token = provider.getToken(syncLocation);
const fileId = this.$store.getters['file/current'].id; const fileId = store.getters['file/current'].id;
const contentId = `${fileId}/content`; const contentId = `${fileId}/content`;
const historyContext = { const historyContext = {
token, token,
@ -171,7 +172,7 @@ export default {
} }
}, },
close() { close() {
this.$store.dispatch('data/setSideBarPanel', 'menu'); store.dispatch('data/setSideBarPanel', 'menu');
}, },
showMore() { showMore() {
this.showCount += pageSize; this.showCount += pageSize;
@ -182,7 +183,7 @@ export default {
const historyContext = utils.deepCopy(this.historyContext); const historyContext = utils.deepCopy(this.historyContext);
if (historyContext) { if (historyContext) {
const provider = providerRegistry.providersById[this.syncLocation.providerId]; const provider = providerRegistry.providersById[this.syncLocation.providerId];
revisionContentPromise = new Promise((resolve, reject) => this.$store.dispatch( revisionContentPromise = new Promise((resolve, reject) => store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => provider.getFileRevisionContent({ () => provider.getFileRevisionContent({
...historyContext, ...historyContext,
@ -192,14 +193,14 @@ export default {
)); ));
revisionContentPromises[revision.id] = revisionContentPromise; revisionContentPromises[revision.id] = revisionContentPromise;
revisionContentPromise.catch((err) => { revisionContentPromise.catch((err) => {
this.$store.dispatch('notification/error', err); store.dispatch('notification/error', err);
revisionContentPromises[revision.id] = null; revisionContentPromises[revision.id] = null;
}); });
} }
} }
if (revisionContentPromise) { if (revisionContentPromise) {
revisionContentPromise.then(revisionContent => revisionContentPromise.then(revisionContent =>
this.$store.dispatch('content/setRevisionContent', revisionContent)); store.dispatch('content/setRevisionContent', revisionContent));
} }
}, },
refreshHighlighters() { refreshHighlighters() {
@ -256,14 +257,14 @@ export default {
cachedHistoryContextHash = this.historyContextHash; cachedHistoryContextHash = this.historyContextHash;
revisionContentPromises = {}; revisionContentPromises = {};
const provider = providerRegistry.providersById[this.syncLocation.providerId]; const provider = providerRegistry.providersById[this.syncLocation.providerId];
revisionsPromise = new Promise((resolve, reject) => this.$store.dispatch( revisionsPromise = new Promise((resolve, reject) => store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => provider () => provider
.listFileRevisions(historyContext) .listFileRevisions(historyContext)
.then(resolve, reject), .then(resolve, reject),
)) ))
.catch((err) => { .catch((err) => {
this.$store.dispatch('notification/error', err); store.dispatch('notification/error', err);
cachedHistoryContextHash = null; cachedHistoryContextHash = null;
return []; return [];
}); });
@ -282,7 +283,7 @@ export default {
revisions(revisions) { revisions(revisions) {
const { historyContext } = this; const { historyContext } = this;
if (historyContext) { if (historyContext) {
this.$store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => utils.awaitSequence(revisions, async (revision) => { () => utils.awaitSequence(revisions, async (revision) => {
// Make sure revisions and historyContext haven't changed // Make sure revisions and historyContext haven't changed

View File

@ -39,7 +39,7 @@ const readFile = file => new Promise((resolve) => {
reader.onload = (e) => { reader.onload = (e) => {
const content = e.target.result; const content = e.target.result;
if (content.match(/\uFFFD/)) { if (content.match(/\uFFFD/)) {
this.$store.dispatch('notification/error', 'File is not readable.'); store.dispatch('notification/error', 'File is not readable.');
} else { } else {
resolve(content); resolve(content);
} }
@ -60,7 +60,7 @@ export default {
...Provider.parseContent(content), ...Provider.parseContent(content),
name: file.name, name: file.name,
}); });
this.$store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
}, },
async onImportHtml(evt) { async onImportHtml(evt) {
const file = evt.target.files[0]; const file = evt.target.files[0];
@ -71,7 +71,7 @@ export default {
...Provider.parseContent(turndownService.turndown(sanitizedContent)), ...Provider.parseContent(turndownService.turndown(sanitizedContent)),
name: file.name, name: file.name,
}); });
this.$store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
}, },
}, },
}; };

View File

@ -23,6 +23,9 @@
<span v-else-if="currentWorkspace.providerId === 'githubWorkspace'"> <span v-else-if="currentWorkspace.providerId === 'githubWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a>. <b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a>.
</span> </span>
<span v-else-if="currentWorkspace.providerId === 'gitlabWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitLab project</a>.
</span>
</div> </div>
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else> <div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
<div class="menu-entry__icon menu-entry__icon--disabled"> <div class="menu-entry__icon menu-entry__icon--disabled">
@ -38,18 +41,18 @@
</menu-entry> </menu-entry>
<menu-entry @click.native="setPanel('workspaces')"> <menu-entry @click.native="setPanel('workspaces')">
<icon-database slot="icon"></icon-database> <icon-database slot="icon"></icon-database>
<div><div class="menu-entry__label menu-entry__label--warning">new</div> Workspaces</div> <div><div class="menu-entry__label menu-entry__label--count" v-if="workspaceCount">{{workspaceCount}}</div> Workspaces</div>
<span>Switch to another workspace.</span> <span>Switch to another workspace.</span>
</menu-entry> </menu-entry>
<hr> <hr>
<menu-entry @click.native="setPanel('sync')"> <menu-entry @click.native="setPanel('sync')">
<icon-sync slot="icon"></icon-sync> <icon-sync slot="icon"></icon-sync>
<div>Synchronize</div> <div><div class="menu-entry__label menu-entry__label--count" v-if="syncLocationCount">{{syncLocationCount}}</div> Synchronize</div>
<span>Sync your files in the Cloud.</span> <span>Sync your files in the Cloud.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="setPanel('publish')"> <menu-entry @click.native="setPanel('publish')">
<icon-upload slot="icon"></icon-upload> <icon-upload slot="icon"></icon-upload>
<div>Publish</div> <div><div class="menu-entry__label menu-entry__label--count" v-if="publishLocationCount">{{publishLocationCount}}</div>Publish</div>
<span>Export your files to the web.</span> <span>Export your files to the web.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="setPanel('history')"> <menu-entry @click.native="setPanel('history')">
@ -98,6 +101,8 @@ import providerRegistry from '../../services/providers/common/providerRegistry';
import UserImage from '../UserImage'; 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 store from '../../store';
export default { export default {
components: { components: {
@ -109,12 +114,23 @@ export default {
'currentWorkspace', 'currentWorkspace',
'syncToken', 'syncToken',
'loginToken', 'loginToken',
'userId',
]), ]),
userId() {
return userSvc.getCurrentUserId();
},
workspaceLocationUrl() { workspaceLocationUrl() {
const provider = providerRegistry.providersById[this.currentWorkspace.providerId]; const provider = providerRegistry.providersById[this.currentWorkspace.providerId];
return provider.getWorkspaceLocationUrl(this.currentWorkspace); return provider.getWorkspaceLocationUrl(this.currentWorkspace);
}, },
workspaceCount() {
return Object.keys(store.getters['workspace/workspacesById']).length;
},
syncLocationCount() {
return Object.keys(store.getters['syncLocation/currentWithWorkspaceSyncLocation']).length;
},
publishLocationCount() {
return Object.keys(store.getters['publishLocation/current']).length;
},
}, },
methods: { methods: {
...mapActions('data', { ...mapActions('data', {
@ -130,7 +146,7 @@ export default {
}, },
async fileProperties() { async fileProperties() {
try { try {
await this.$store.dispatch('modal/open', 'fileProperties'); await store.dispatch('modal/open', 'fileProperties');
} catch (e) { } catch (e) {
// Cancel // Cancel
} }

View File

@ -45,6 +45,7 @@
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import backupSvc from '../../services/backupSvc'; import backupSvc from '../../services/backupSvc';
import utils from '../../services/utils'; import utils from '../../services/utils';
import store from '../../store';
export default { export default {
components: { components: {
@ -52,7 +53,7 @@ export default {
}, },
computed: { computed: {
templateCount() { templateCount() {
return Object.keys(this.$store.getters['data/allTemplatesById']).length; return Object.keys(store.getters['data/allTemplatesById']).length;
}, },
}, },
methods: { methods: {
@ -63,7 +64,7 @@ export default {
reader.onload = (e) => { reader.onload = (e) => {
const text = e.target.result; const text = e.target.result;
if (text.match(/\uFFFD/)) { if (text.match(/\uFFFD/)) {
this.$store.dispatch('notification/error', 'File is not readable.'); store.dispatch('notification/error', 'File is not readable.');
} else { } else {
backupSvc.importBackup(text); backupSvc.importBackup(text);
} }
@ -82,23 +83,23 @@ export default {
}, },
async settings() { async settings() {
try { try {
const settings = await this.$store.dispatch('modal/open', 'settings'); const settings = await store.dispatch('modal/open', 'settings');
this.$store.dispatch('data/setSettings', settings); store.dispatch('data/setSettings', settings);
} catch (e) { } catch (e) {
// Cancel // Cancel
} }
}, },
async templates() { async templates() {
try { try {
const { templates } = await this.$store.dispatch('modal/open', 'templates'); const { templates } = await store.dispatch('modal/open', 'templates');
this.$store.dispatch('data/setTemplatesById', templates); store.dispatch('data/setTemplatesById', templates);
} catch (e) { } catch (e) {
// Cancel // Cancel
} }
}, },
async reset() { async reset() {
try { try {
await this.$store.dispatch('modal/open', 'reset'); await store.dispatch('modal/open', 'reset');
window.location.href = '#reset=true'; window.location.href = '#reset=true';
window.location.reload(); window.location.reload();
} catch (e) { } catch (e) {
@ -106,7 +107,7 @@ export default {
} }
}, },
about() { about() {
this.$store.dispatch('modal/open', 'about'); store.dispatch('modal/open', 'about');
}, },
}, },
}; };

View File

@ -21,39 +21,6 @@
</menu-entry> </menu-entry>
</div> </div>
<hr> <hr>
<div v-for="token in googleDriveTokens" :key="token.sub">
<menu-entry @click.native="publishGoogleDrive(token)">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
<div>Publish to Google Drive</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in dropboxTokens" :key="token.sub">
<menu-entry @click.native="publishDropbox(token)">
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
<div>Publish to Dropbox</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in githubTokens" :key="token.sub">
<menu-entry @click.native="publishGithub(token)">
<icon-provider slot="icon" provider-id="github"></icon-provider>
<div>Publish to GitHub</div>
<span>{{token.name}}</span>
</menu-entry>
<menu-entry @click.native="publishGist(token)">
<icon-provider slot="icon" provider-id="gist"></icon-provider>
<div>Publish to Gist</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in wordpressTokens" :key="token.sub">
<menu-entry @click.native="publishWordpress(token)">
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
<div>Publish to WordPress</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in bloggerTokens" :key="token.sub"> <div v-for="token in bloggerTokens" :key="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>
@ -66,6 +33,46 @@
<span>{{token.name}}</span> <span>{{token.name}}</span>
</menu-entry> </menu-entry>
</div> </div>
<div v-for="token in dropboxTokens" :key="token.sub">
<menu-entry @click.native="publishDropbox(token)">
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
<div>Publish to Dropbox</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in githubTokens" :key="token.sub">
<menu-entry @click.native="publishGist(token)">
<icon-provider slot="icon" provider-id="gist"></icon-provider>
<div>Publish to Gist</div>
<span>{{token.name}}</span>
</menu-entry>
<menu-entry @click.native="publishGithub(token)">
<icon-provider slot="icon" provider-id="github"></icon-provider>
<div>Publish to GitHub</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in gitlabTokens" :key="token.sub">
<menu-entry @click.native="publishGitlab(token)">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
<div>Publish to GitLab</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in googleDriveTokens" :key="token.sub">
<menu-entry @click.native="publishGoogleDrive(token)">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
<div>Publish to Google Drive</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in wordpressTokens" :key="token.sub">
<menu-entry @click.native="publishWordpress(token)">
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
<div>Publish to WordPress</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in zendeskTokens" :key="token.sub"> <div v-for="token in zendeskTokens" :key="token.sub">
<menu-entry @click.native="publishZendesk(token)"> <menu-entry @click.native="publishZendesk(token)">
<icon-provider slot="icon" provider-id="zendesk"></icon-provider> <icon-provider slot="icon" provider-id="zendesk"></icon-provider>
@ -74,9 +81,9 @@
</menu-entry> </menu-entry>
</div> </div>
<hr> <hr>
<menu-entry @click.native="addGoogleDriveAccount"> <menu-entry @click.native="addBloggerAccount">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider> <icon-provider slot="icon" provider-id="blogger"></icon-provider>
<span>Add Google Drive account</span> <span>Add Blogger account</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addDropboxAccount"> <menu-entry @click.native="addDropboxAccount">
<icon-provider slot="icon" provider-id="dropbox"></icon-provider> <icon-provider slot="icon" provider-id="dropbox"></icon-provider>
@ -86,14 +93,18 @@
<icon-provider slot="icon" provider-id="github"></icon-provider> <icon-provider slot="icon" provider-id="github"></icon-provider>
<span>Add GitHub account</span> <span>Add GitHub account</span>
</menu-entry> </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="addWordpressAccount"> <menu-entry @click.native="addWordpressAccount">
<icon-provider slot="icon" provider-id="wordpress"></icon-provider> <icon-provider slot="icon" provider-id="wordpress"></icon-provider>
<span>Add WordPress account</span> <span>Add WordPress account</span>
</menu-entry> </menu-entry>
<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="addZendeskAccount"> <menu-entry @click.native="addZendeskAccount">
<icon-provider slot="icon" provider-id="zendesk"></icon-provider> <icon-provider slot="icon" provider-id="zendesk"></icon-provider>
<span>Add Zendesk account</span> <span>Add Zendesk account</span>
@ -108,6 +119,7 @@ import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import dropboxHelper from '../../services/providers/helpers/dropboxHelper'; import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper'; import githubHelper from '../../services/providers/helpers/githubHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper'; import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper'; import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
import publishSvc from '../../services/publishSvc'; import publishSvc from '../../services/publishSvc';
@ -145,33 +157,32 @@ export default {
return Object.keys(this.publishLocations).length; return Object.keys(this.publishLocations).length;
}, },
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return store.getters['file/current'].name;
},
googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
},
dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokensBySub']);
},
githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokensBySub']);
},
wordpressTokens() {
return tokensToArray(this.$store.getters['data/wordpressTokensBySub']);
}, },
bloggerTokens() { bloggerTokens() {
return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isBlogger); return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isBlogger);
},
dropboxTokens() {
return tokensToArray(store.getters['data/dropboxTokensBySub']);
},
githubTokens() {
return tokensToArray(store.getters['data/githubTokensBySub']);
},
gitlabTokens() {
return tokensToArray(store.getters['data/gitlabTokensBySub']);
},
googleDriveTokens() {
return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isDrive);
},
wordpressTokens() {
return tokensToArray(store.getters['data/wordpressTokensBySub']);
}, },
zendeskTokens() { zendeskTokens() {
return tokensToArray(this.$store.getters['data/zendeskTokensBySub']); return tokensToArray(store.getters['data/zendeskTokensBySub']);
}, },
noToken() { noToken() {
return !this.googleDriveTokens.length return Object.values(store.getters['data/tokensByType'])
&& !this.dropboxTokens.length .every(tokens => !Object.keys(tokens).length);
&& !this.githubTokens.length
&& !this.wordpressTokens.length
&& !this.bloggerTokens.length
&& !this.zendeskTokens.length;
}, },
}, },
methods: { methods: {
@ -182,30 +193,7 @@ export default {
}, },
async managePublish() { async managePublish() {
try { try {
await this.$store.dispatch('modal/open', 'publishManagement'); await store.dispatch('modal/open', 'publishManagement');
} catch (e) { /* cancel */ }
},
async addGoogleDriveAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
} catch (e) { /* cancel */ }
},
async addDropboxAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'dropboxAccount' });
await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
} catch (e) { /* cancel */ }
},
async addGithubAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'githubAccount' });
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ }
},
async addWordpressAccount() {
try {
await wordpressHelper.addAccount();
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async addBloggerAccount() { async addBloggerAccount() {
@ -213,19 +201,49 @@ export default {
await googleHelper.addBloggerAccount(); await googleHelper.addBloggerAccount();
} catch (e) { /* cancel */ } } 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 addWordpressAccount() {
try {
await wordpressHelper.addAccount();
} catch (e) { /* cancel */ }
},
async addZendeskAccount() { async addZendeskAccount() {
try { try {
const { subdomain, clientId } = await this.$store.dispatch('modal/open', { type: 'zendeskAccount' }); const { subdomain, clientId } = await store.dispatch('modal/open', { type: 'zendeskAccount' });
await zendeskHelper.addAccount(subdomain, clientId); await zendeskHelper.addAccount(subdomain, clientId);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
publishGoogleDrive: publishModalOpener('googleDrivePublish'), publishBlogger: publishModalOpener('bloggerPublish'),
publishBloggerPage: publishModalOpener('bloggerPagePublish'),
publishDropbox: publishModalOpener('dropboxPublish'), publishDropbox: publishModalOpener('dropboxPublish'),
publishGithub: publishModalOpener('githubPublish'), publishGithub: publishModalOpener('githubPublish'),
publishGist: publishModalOpener('gistPublish'), publishGist: publishModalOpener('gistPublish'),
publishGitlab: publishModalOpener('gitlabPublish'),
publishGoogleDrive: publishModalOpener('googleDrivePublish'),
publishWordpress: publishModalOpener('wordpressPublish'), publishWordpress: publishModalOpener('wordpressPublish'),
publishBlogger: publishModalOpener('bloggerPublish'),
publishBloggerPage: publishModalOpener('bloggerPagePublish'),
publishZendesk: publishModalOpener('zendeskPublish'), publishZendesk: publishModalOpener('zendeskPublish'),
}, },
}; };

View File

@ -21,18 +21,6 @@
</menu-entry> </menu-entry>
</div> </div>
<hr> <hr>
<div v-for="token in googleDriveTokens" :key="token.sub">
<menu-entry @click.native="openGoogleDrive(token)">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
<div>Open from Google Drive</div>
<span>{{token.name}}</span>
</menu-entry>
<menu-entry @click.native="saveGoogleDrive(token)">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
<div>Save on Google Drive</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in dropboxTokens" :key="token.sub"> <div v-for="token in dropboxTokens" :key="token.sub">
<menu-entry @click.native="openDropbox(token)"> <menu-entry @click.native="openDropbox(token)">
<icon-provider slot="icon" provider-id="dropbox"></icon-provider> <icon-provider slot="icon" provider-id="dropbox"></icon-provider>
@ -62,11 +50,31 @@
<span>{{token.name}}</span> <span>{{token.name}}</span>
</menu-entry> </menu-entry>
</div> </div>
<div v-for="token in gitlabTokens" :key="token.sub">
<menu-entry @click.native="openGitlab(token)">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
<div>Open from GitLab</div>
<span>{{token.name}}</span>
</menu-entry>
<menu-entry @click.native="saveGitlab(token)">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
<div>Save on GitLab</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in googleDriveTokens" :key="token.sub">
<menu-entry @click.native="openGoogleDrive(token)">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
<div>Open from Google Drive</div>
<span>{{token.name}}</span>
</menu-entry>
<menu-entry @click.native="saveGoogleDrive(token)">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
<div>Save on Google Drive</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<hr> <hr>
<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="addDropboxAccount"> <menu-entry @click.native="addDropboxAccount">
<icon-provider slot="icon" provider-id="dropbox"></icon-provider> <icon-provider slot="icon" provider-id="dropbox"></icon-provider>
<span>Add Dropbox account</span> <span>Add Dropbox account</span>
@ -75,6 +83,14 @@
<icon-provider slot="icon" provider-id="github"></icon-provider> <icon-provider slot="icon" provider-id="github"></icon-provider>
<span>Add GitHub account</span> <span>Add GitHub account</span>
</menu-entry> </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>
</div> </div>
</div> </div>
</template> </template>
@ -85,9 +101,11 @@ import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import dropboxHelper from '../../services/providers/helpers/dropboxHelper'; import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper'; import githubHelper from '../../services/providers/helpers/githubHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import googleDriveProvider from '../../services/providers/googleDriveProvider'; import googleDriveProvider from '../../services/providers/googleDriveProvider';
import dropboxProvider from '../../services/providers/dropboxProvider'; import dropboxProvider from '../../services/providers/dropboxProvider';
import githubProvider from '../../services/providers/githubProvider'; import githubProvider from '../../services/providers/githubProvider';
import gitlabProvider from '../../services/providers/gitlabProvider';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
import store from '../../store'; import store from '../../store';
@ -121,16 +139,19 @@ export default {
return Object.keys(this.syncLocations).length; return Object.keys(this.syncLocations).length;
}, },
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return store.getters['file/current'].name;
},
googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
}, },
dropboxTokens() { dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokensBySub']); return tokensToArray(store.getters['data/dropboxTokensBySub']);
}, },
githubTokens() { githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokensBySub']); return tokensToArray(store.getters['data/githubTokensBySub']);
},
gitlabTokens() {
return tokensToArray(store.getters['data/gitlabTokensBySub']);
},
googleDriveTokens() {
return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isDrive);
}, },
noToken() { noToken() {
return !this.googleDriveTokens.length return !this.googleDriveTokens.length
@ -146,37 +167,43 @@ export default {
}, },
async manageSync() { async manageSync() {
try { try {
await this.$store.dispatch('modal/open', 'syncManagement'); await store.dispatch('modal/open', 'syncManagement');
} catch (e) { /* cancel */ }
},
async addGoogleDriveAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async addDropboxAccount() { async addDropboxAccount() {
try { try {
await this.$store.dispatch('modal/open', { type: 'dropboxAccount' }); await store.dispatch('modal/open', { type: 'dropboxAccount' });
await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess); await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async addGithubAccount() { async addGithubAccount() {
try { try {
await this.$store.dispatch('modal/open', { type: 'githubAccount' }); await store.dispatch('modal/open', { type: 'githubAccount' });
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess); await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ } } 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 openGoogleDrive(token) { async openGoogleDrive(token) {
const files = await googleHelper.openPicker(token, 'doc'); const files = await googleHelper.openPicker(token, 'doc');
this.$store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => googleDriveProvider.openFiles(token, files), () => googleDriveProvider.openFiles(token, files),
); );
}, },
async openDropbox(token) { async openDropbox(token) {
const paths = await dropboxHelper.openChooser(token); const paths = await dropboxHelper.openChooser(token);
this.$store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => dropboxProvider.openFiles(token, paths), () => dropboxProvider.openFiles(token, paths),
); );
@ -197,7 +224,7 @@ export default {
type: 'githubOpen', type: 'githubOpen',
token, token,
}); });
this.$store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => githubProvider.openFile(token, syncLocation), () => githubProvider.openFile(token, syncLocation),
); );
@ -213,6 +240,23 @@ export default {
await openSyncModal(token, 'gistSync'); await openSyncModal(token, 'gistSync');
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async openGitlab(token) {
try {
const syncLocation = await store.dispatch('modal/open', {
type: 'gitlabOpen',
token,
});
store.dispatch(
'queue/enqueue',
() => gitlabProvider.openFile(token, syncLocation),
);
} catch (e) { /* cancel */ }
},
async saveGitlab(token) {
try {
await openSyncModal(token, 'gitlabSave');
} catch (e) { /* cancel */ }
},
}, },
}; };
</script> </script>

View File

@ -17,6 +17,11 @@
<div>GitHub workspace</div> <div>GitHub workspace</div>
<span>Add a workspace synced with a GitHub repository.</span> <span>Add a workspace synced with a GitHub repository.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addGitlabWorkspace">
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
<div>GitLab workspace</div>
<span>Add a workspace synced with a GitLab project.</span>
</menu-entry>
<menu-entry @click.native="addGoogleDriveWorkspace"> <menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<div>Google Drive workspace</div> <div>Google Drive workspace</div>
@ -33,6 +38,8 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import store from '../../store';
export default { export default {
components: { components: {
@ -50,7 +57,7 @@ export default {
methods: { methods: {
async addCouchdbWorkspace() { async addCouchdbWorkspace() {
try { try {
this.$store.dispatch('modal/open', { store.dispatch('modal/open', {
type: 'couchdbWorkspace', type: 'couchdbWorkspace',
}); });
} catch (e) { } catch (e) {
@ -59,17 +66,29 @@ export default {
}, },
async addGithubWorkspace() { async addGithubWorkspace() {
try { try {
this.$store.dispatch('modal/open', { store.dispatch('modal/open', {
type: 'githubWorkspace', type: 'githubWorkspace',
}); });
} catch (e) { } catch (e) {
// Cancel // Cancel
} }
}, },
async addGitlabWorkspace() {
try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
const token = await gitlabHelper.addAccount(serverUrl, applicationId);
store.dispatch('modal/open', {
type: 'gitlabWorkspace',
token,
});
} catch (e) {
// Cancel
}
},
async addGoogleDriveWorkspace() { async addGoogleDriveWorkspace() {
try { try {
const token = await googleHelper.addDriveAccount(true); const token = await googleHelper.addDriveAccount(true);
this.$store.dispatch('modal/open', { store.dispatch('modal/open', {
type: 'googleDriveWorkspace', type: 'googleDriveWorkspace',
token, token,
}); });
@ -78,7 +97,7 @@ export default {
} }
}, },
manageWorkspaces() { manageWorkspaces() {
this.$store.dispatch('modal/open', 'workspaceManagement'); store.dispatch('modal/open', 'workspaceManagement');
}, },
}, },
}; };

View File

@ -92,6 +92,7 @@ import FormEntry from './common/FormEntry';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import utils from '../../services/utils'; import utils from '../../services/utils';
import presets from '../../data/presets'; import presets from '../../data/presets';
import store from '../../store';
const simpleProperties = { const simpleProperties = {
title: '', title: '',
@ -125,17 +126,17 @@ export default {
presets: () => Object.keys(presets).sort(), presets: () => Object.keys(presets).sort(),
tab: { tab: {
get() { get() {
return this.$store.getters['data/localSettings'].filePropertiesTab; return store.getters['data/localSettings'].filePropertiesTab;
}, },
set(value) { set(value) {
this.$store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
filePropertiesTab: value, filePropertiesTab: value,
}); });
}, },
}, },
}, },
created() { created() {
const content = this.$store.getters['content/current']; const content = store.getters['content/current'];
this.contentId = content.id; this.contentId = content.id;
this.setYamlProperties(content.properties); this.setYamlProperties(content.properties);
if (this.tab !== 'yaml') { if (this.tab !== 'yaml') {
@ -214,7 +215,7 @@ export default {
if (this.error) { if (this.error) {
this.setYamlTab(); this.setYamlTab();
} else { } else {
this.$store.commit('content/patchItem', { store.commit('content/patchItem', {
id: this.contentId, id: this.contentId,
properties: utils.sanitizeText(this.yamlProperties), properties: utils.sanitizeText(this.yamlProperties),
}); });

View File

@ -25,6 +25,7 @@
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import exportSvc from '../../services/exportSvc'; import exportSvc from '../../services/exportSvc';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import store from '../../store';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -38,7 +39,7 @@ export default modalTemplate({
this.$watch('selectedTemplate', (selectedTemplate) => { this.$watch('selectedTemplate', (selectedTemplate) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(async () => { timeoutId = setTimeout(async () => {
const currentFile = this.$store.getters['file/current']; const currentFile = store.getters['file/current'];
const html = await exportSvc.applyTemplate( const html = await exportSvc.applyTemplate(
currentFile.id, currentFile.id,
this.allTemplatesById[selectedTemplate], this.allTemplatesById[selectedTemplate],
@ -55,7 +56,7 @@ export default modalTemplate({
]), ]),
resolve() { resolve() {
const { config } = this; const { config } = this;
const currentFile = this.$store.getters['file/current']; const currentFile = store.getters['file/current'];
config.resolve(); config.resolve();
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]); exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
}, },

View File

@ -26,6 +26,7 @@
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import MenuEntry from '../menus/common/MenuEntry'; import MenuEntry from '../menus/common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import store from '../../store';
export default modalTemplate({ export default modalTemplate({
components: { components: {
@ -36,7 +37,7 @@ export default modalTemplate({
}), }),
computed: { computed: {
googlePhotosTokens() { googlePhotosTokens() {
const googleTokensBySub = this.$store.getters['data/googleTokensBySub']; const googleTokensBySub = store.getters['data/googleTokensBySub'];
return Object.values(googleTokensBySub) return Object.values(googleTokensBySub)
.filter(token => token.isPhotos) .filter(token => token.isPhotos)
.sort((token1, token2) => token1.name.localeCompare(token2.name)); .sort((token1, token2) => token1.name.localeCompare(token2.name));
@ -65,7 +66,7 @@ export default modalTemplate({
this.config.reject(); this.config.reject();
const res = await googleHelper.openPicker(token, 'img'); const res = await googleHelper.openPicker(token, 'img');
if (res[0]) { if (res[0]) {
this.$store.dispatch('modal/open', { store.dispatch('modal/open', {
type: 'googlePhoto', type: 'googlePhoto',
url: res[0].url, url: res[0].url,
callback, callback,

View File

@ -32,6 +32,7 @@ import networkSvc from '../../services/networkSvc';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import store from '../../store';
export default modalTemplate({ export default modalTemplate({
computedLocalSettings: { computedLocalSettings: {
@ -40,13 +41,13 @@ export default modalTemplate({
methods: { methods: {
async resolve() { async resolve() {
this.config.resolve(); this.config.resolve();
const currentFile = this.$store.getters['file/current']; const currentFile = store.getters['file/current'];
const currentContent = this.$store.getters['content/current']; const currentContent = store.getters['content/current'];
const { selectedFormat } = this; const { selectedFormat } = this;
this.$store.dispatch('queue/enqueue', async () => { store.dispatch('queue/enqueue', async () => {
const [sponsorToken, token] = await Promise.all([ const [sponsorToken, token] = await Promise.all([
Promise.resolve().then(() => { Promise.resolve().then(() => {
const tokenToRefresh = this.$store.getters['workspace/sponsorToken']; const tokenToRefresh = store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh); return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}), }),
sponsorSvc.getToken(), sponsorSvc.getToken(),
@ -60,7 +61,7 @@ export default modalTemplate({
token, token,
idToken: sponsorToken && sponsorToken.idToken, idToken: sponsorToken && sponsorToken.idToken,
format: selectedFormat, format: selectedFormat,
options: JSON.stringify(this.$store.getters['data/computedSettings'].pandoc), options: JSON.stringify(store.getters['data/computedSettings'].pandoc),
metadata: JSON.stringify(currentContent.properties), metadata: JSON.stringify(currentContent.properties),
}, },
body: JSON.stringify(editorSvc.getPandocAst()), body: JSON.stringify(editorSvc.getPandocAst()),
@ -70,10 +71,10 @@ export default modalTemplate({
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`); FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
} catch (err) { } catch (err) {
if (err.status === 401) { if (err.status === 401) {
this.$store.dispatch('modal/open', 'sponsorOnly'); store.dispatch('modal/open', 'sponsorOnly');
} else { } else {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); store.dispatch('notification/error', err);
} }
} }
}); });

View File

@ -27,6 +27,7 @@ import sponsorSvc from '../../services/sponsorSvc';
import networkSvc from '../../services/networkSvc'; import networkSvc from '../../services/networkSvc';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import store from '../../store';
export default modalTemplate({ export default modalTemplate({
computedLocalSettings: { computedLocalSettings: {
@ -35,11 +36,11 @@ export default modalTemplate({
methods: { methods: {
async resolve() { async resolve() {
this.config.resolve(); this.config.resolve();
const currentFile = this.$store.getters['file/current']; const currentFile = store.getters['file/current'];
this.$store.dispatch('queue/enqueue', async () => { store.dispatch('queue/enqueue', async () => {
const [sponsorToken, token, html] = await Promise.all([ const [sponsorToken, token, html] = await Promise.all([
Promise.resolve().then(() => { Promise.resolve().then(() => {
const tokenToRefresh = this.$store.getters['workspace/sponsorToken']; const tokenToRefresh = store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh); return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}), }),
sponsorSvc.getToken(), sponsorSvc.getToken(),
@ -57,7 +58,7 @@ export default modalTemplate({
params: { params: {
token, token,
idToken: sponsorToken && sponsorToken.idToken, idToken: sponsorToken && sponsorToken.idToken,
options: JSON.stringify(this.$store.getters['data/computedSettings'].wkhtmltopdf), options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf),
}, },
body: html, body: html,
blob: true, blob: true,
@ -66,10 +67,10 @@ export default modalTemplate({
FileSaver.saveAs(body, `${currentFile.name}.pdf`); FileSaver.saveAs(body, `${currentFile.name}.pdf`);
} catch (err) { } catch (err) {
if (err.status === 401) { if (err.status === 401) {
this.$store.dispatch('modal/open', 'sponsorOnly'); store.dispatch('modal/open', 'sponsorOnly');
} else { } else {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); store.dispatch('notification/error', err);
} }
} }
}); });

View File

@ -49,6 +49,7 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import store from '../../store';
export default { export default {
components: { components: {
@ -62,7 +63,7 @@ export default {
publishLocations: 'current', publishLocations: 'current',
}), }),
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return store.getters['file/current'].name;
}, },
}, },
methods: { methods: {
@ -70,7 +71,7 @@ export default {
'info', 'info',
]), ]),
remove(location) { remove(location) {
this.$store.commit('publishLocation/deleteItem', location.id); store.commit('publishLocation/deleteItem', location.id);
}, },
}, },
}; };

View File

@ -37,6 +37,7 @@ import ModalInner from './common/ModalInner';
import Tab from './common/Tab'; import Tab from './common/Tab';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import defaultSettings from '../../data/defaults/defaultSettings.yml'; import defaultSettings from '../../data/defaults/defaultSettings.yml';
import store from '../../store';
const emptySettings = `# Add your custom settings here to override the const emptySettings = `# Add your custom settings here to override the
# default settings. # default settings.
@ -63,7 +64,7 @@ export default {
}, },
}, },
created() { created() {
const settings = this.$store.getters['data/settings']; const settings = store.getters['data/settings'];
this.setCustomSettings(settings === '\n' ? emptySettings : settings); this.setCustomSettings(settings === '\n' ? emptySettings : settings);
}, },
methods: { methods: {

View File

@ -19,13 +19,14 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import utils from '../../services/utils'; import utils from '../../services/utils';
import store from '../../store';
export default { export default {
components: { components: {
ModalInner, ModalInner,
}, },
data() { data() {
const sponsorToken = this.$store.getters['workspace/sponsorToken']; const sponsorToken = store.getters['workspace/sponsorToken'];
const makeButton = (id, price, description, offer) => { const makeButton = (id, price, description, offer) => {
const params = { const params = {
cmd: '_s-xclick', cmd: '_s-xclick',

View File

@ -49,6 +49,7 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import store from '../../store';
export default { export default {
components: { components: {
@ -62,7 +63,7 @@ export default {
syncLocations: 'currentWithWorkspaceSyncLocation', syncLocations: 'currentWithWorkspaceSyncLocation',
}), }),
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return store.getters['file/current'].name;
}, },
}, },
methods: { methods: {
@ -73,7 +74,7 @@ export default {
if (location.id === 'main') { if (location.id === 'main') {
this.info('This location can not be removed.'); this.info('This location can not be removed.');
} else { } else {
this.$store.commit('syncLocation/deleteItem', location.id); store.commit('syncLocation/deleteItem', location.id);
} }
}, },
}, },

View File

@ -57,6 +57,7 @@ import ModalInner from './common/ModalInner';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html'; import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
import emptyTemplateHelpers from '!raw-loader!../../data/empties/emptyTemplateHelpers.js'; // eslint-disable-line import emptyTemplateHelpers from '!raw-loader!../../data/empties/emptyTemplateHelpers.js'; // eslint-disable-line
import store from '../../store';
const collator = new Intl.Collator(undefined, { sensitivity: 'base' }); const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
@ -91,7 +92,7 @@ export default {
}, },
created() { created() {
this.$watch( this.$watch(
() => this.$store.getters['data/allTemplatesById'], () => store.getters['data/allTemplatesById'],
(allTemplatesById) => { (allTemplatesById) => {
const templates = {}; const templates = {};
// Sort templates by name // Sort templates by name

View File

@ -64,6 +64,7 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import workspaceSvc from '../../services/workspaceSvc'; import workspaceSvc from '../../services/workspaceSvc';
import store from '../../store';
export default { export default {
components: { components: {
@ -95,7 +96,7 @@ export default {
const workspace = this.workspacesById[this.editedId]; const workspace = this.workspacesById[this.editedId];
if (workspace) { if (workspace) {
if (!cancel && this.editingName) { if (!cancel && this.editingName) {
this.$store.dispatch('workspace/patchWorkspacesById', { store.dispatch('workspace/patchWorkspacesById', {
[this.editedId]: { [this.editedId]: {
...workspace, ...workspace,
name: this.editingName, name: this.editingName,
@ -114,7 +115,7 @@ export default {
this.info('Please close the workspace before removing it.'); this.info('Please close the workspace before removing it.');
} else { } else {
try { try {
await this.$store.dispatch('modal/open', 'removeWorkspace'); await store.dispatch('modal/open', 'removeWorkspace');
workspaceSvc.removeWorkspace(id); workspaceSvc.removeWorkspace(id);
} catch (e) { /* Cancel */ } } catch (e) { /* Cancel */ }
} }

View File

@ -4,10 +4,6 @@
<button class="modal__close-button button not-tabbable" @click="config.reject()" v-title="'Close modal'"> <button class="modal__close-button button not-tabbable" @click="config.reject()" v-title="'Close modal'">
<icon-close></icon-close> <icon-close></icon-close>
</button> </button>
<div class="modal__sponsor-button" v-if="showSponsorButton">
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>, please consider
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
</div>
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
@ -15,33 +11,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import googleHelper from '../../../services/providers/helpers/googleHelper';
import syncSvc from '../../../services/syncSvc';
export default { export default {
computed: { computed: {
...mapGetters('modal', [ ...mapGetters('modal', [
'config', 'config',
]), ]),
showSponsorButton() {
const { type } = this.$store.getters['modal/config'];
return !this.$store.getters.isSponsor && type !== 'sponsor' && type !== 'signInForSponsorship';
},
},
methods: {
async sponsor() {
try {
if (!this.$store.getters['workspace/sponsorToken']) {
// User has to sign in
await this.$store.dispatch('modal/open', 'signInForSponsorship');
await googleHelper.signin();
syncSvc.requestSync();
}
if (!this.$store.getters.isSponsor) {
await this.$store.dispatch('modal/open', 'sponsor');
}
} catch (e) { /* cancel */ }
},
}, },
}; };
</script> </script>
@ -64,15 +39,4 @@ export default {
color: rgba(0, 0, 0, 0.67); color: rgba(0, 0, 0, 0.67);
} }
} }
.modal__sponsor-button {
display: inline-block;
color: darken($error-color, 10%);
background-color: transparentize($error-color, 0.85);
border-radius: $border-radius-base;
font-size: 0.9em;
padding: 0.75em 1.5em;
margin-bottom: 1.2em;
line-height: 1.55;
}
</style> </style>

View File

@ -21,6 +21,7 @@
<script> <script>
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import store from '../../../store';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -45,7 +46,7 @@ export default modalTemplate({
name: this.name, name: this.name,
password: this.password, password: this.password,
}; };
this.$store.dispatch('data/addCouchdbToken', token); store.dispatch('data/addCouchdbToken', token);
this.config.resolve(); this.config.resolve();
} }
}, },

View File

@ -46,27 +46,23 @@ export default modalTemplate({
}, },
methods: { methods: {
resolve() { resolve() {
if (!this.repoUrl) { const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl'); this.setError('repoUrl');
} }
if (!this.path) { if (!this.path) {
this.setError('path'); this.setError('path');
} }
if (this.repoUrl && this.path) { if (parsedRepo && this.path) {
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl); // Return new location
if (!parsedRepo) { const location = githubProvider.makeLocation(
this.setError('repoUrl'); this.config.token,
} else { parsedRepo.owner,
// Return new location parsedRepo.repo,
const location = githubProvider.makeLocation( this.branch || 'master',
this.config.token, this.path,
parsedRepo.owner, );
parsedRepo.repo, this.config.resolve(location);
this.branch || 'master',
this.path,
);
this.config.resolve(location);
}
} }
}, },
}, },

View File

@ -45,6 +45,7 @@
<script> <script>
import githubProvider from '../../../services/providers/githubProvider'; import githubProvider from '../../../services/providers/githubProvider';
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -60,28 +61,24 @@ export default modalTemplate({
}, },
methods: { methods: {
resolve() { resolve() {
if (!this.repoUrl) { const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl'); this.setError('repoUrl');
} }
if (!this.path) { if (!this.path) {
this.setError('path'); this.setError('path');
} }
if (this.repoUrl && this.path) { if (parsedRepo && this.path) {
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/); // Return new location
if (!parsedRepo) { const location = githubProvider.makeLocation(
this.setError('repoUrl'); this.config.token,
} else { parsedRepo.owner,
// Return new location parsedRepo.repo,
const location = githubProvider.makeLocation( this.branch || 'master',
this.config.token, this.path,
parsedRepo[1], );
parsedRepo[2], location.templateId = this.selectedTemplate;
this.branch || 'master', this.config.resolve(location);
this.path,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}
} }
}, },
}, },

View File

@ -11,12 +11,6 @@
<b>Example:</b> https://github.com/benweet/stackedit <b>Example:</b> https://github.com/benweet/stackedit
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path"> <form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -24,6 +18,12 @@
If the file exists, it will be overwritten. If the file exists, it will be overwritten.
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>

View File

@ -11,18 +11,18 @@
<b>Example:</b> https://github.com/benweet/stackedit <b>Example:</b> https://github.com/benweet/stackedit
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="Folder path" info="optional"> <form-entry label="Folder path" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If not supplied, the root folder will be used. If not supplied, the root folder will be used.
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>

View File

@ -0,0 +1,65 @@
<template>
<modal-inner aria-label="GitLab account">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitlab"></icon-provider>
</div>
<p>Link your <b>GitLab</b> account to <b>StackEdit</b>.</p>
<form-entry label="GitLab URL" error="serverUrl">
<input slot="field" class="textfield" type="text" v-model.trim="serverUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://gitlab.example.com/
</div>
</form-entry>
<form-entry label="Application ID" error="applicationId">
<input slot="field" class="textfield" type="text" v-model.trim="applicationId" @keydown.enter="resolve()">
<div class="form-entry__info">
You have to configure an OAuth2 Application with redirect URL <b>{{redirectUrl}}</b>
</div>
<div class="form-entry__actions">
<a href="https://docs.gitlab.com/ee/integration/oauth_provider.html" target="_blank">More info</a>
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import modalTemplate from '../common/modalTemplate';
import constants from '../../../data/constants';
export default modalTemplate({
data: () => ({
redirectUrl: constants.oauth2RedirectUri,
}),
computedLocalSettings: {
serverUrl: 'gitlabServerUrl',
applicationId: 'gitlabApplicationId',
},
methods: {
resolve() {
if (!this.serverUrl) {
this.setError('serverUrl');
}
if (!this.applicationId) {
this.setError('applicationId');
}
if (this.serverUrl && this.applicationId) {
const parsedUrl = this.serverUrl.match(/^(https:\/\/[^/]+)/);
if (!parsedUrl) {
this.setError('serverUrl');
} else {
this.config.resolve({
serverUrl: parsedUrl[1],
applicationId: this.applicationId,
});
}
}
},
},
});
</script>

View File

@ -0,0 +1,69 @@
<template>
<modal-inner aria-label="Synchronize with GitLab">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitlab"></icon-provider>
</div>
<p>Open a file from your <b>GitLab</b> project and keep it synced.</p>
<form-entry label="Project URL" error="projectUrl">
<input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.serverUrl}}path/to/project
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> path/to/README.md
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import gitlabProvider from '../../../services/providers/gitlabProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
projectUrl: 'gitlabProjectUrl',
},
methods: {
resolve() {
const projectPath = utils.parseGitlabProjectPath(this.projectUrl);
if (!projectPath) {
this.setError('projectUrl');
}
if (!this.path) {
this.setError('path');
}
if (projectPath && this.path) {
// Return new location
const location = gitlabProvider.makeLocation(
this.config.token,
projectPath,
this.branch || 'master',
this.path,
);
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,85 @@
<template>
<modal-inner aria-label="Publish to GitLab">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitlab"></icon-provider>
</div>
<p>Publish <b>{{currentFileName}}</b> to your <b>GitLab</b> project.</p>
<form-entry label="Project URL" error="projectUrl">
<input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.serverUrl}}/path/to/project
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import gitlabProvider from '../../../services/providers/gitlabProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
projectUrl: 'gitlabProjectUrl',
selectedTemplate: 'gitlabPublishTemplate',
},
created() {
this.path = `${this.currentFileName}.md`;
},
methods: {
resolve() {
const projectPath = utils.parseGitlabProjectPath(this.projectUrl);
if (!projectPath) {
this.setError('projectUrl');
}
if (!this.path) {
this.setError('path');
}
if (projectPath && this.path) {
// Return new location
const location = gitlabProvider.makeLocation(
this.config.token,
projectPath,
this.branch || 'master',
this.path,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,72 @@
<template>
<modal-inner aria-label="Synchronize with GitLab">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitlab"></icon-provider>
</div>
<p>Save <b>{{currentFileName}}</b> to your <b>GitLab</b> project and keep it synced.</p>
<form-entry label="Project URL" error="projectUrl">
<input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.serverUrl}}/path/to/project
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import gitlabProvider from '../../../services/providers/gitlabProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
projectUrl: 'gitlabProjectUrl',
},
created() {
this.path = `${this.currentFileName}.md`;
},
methods: {
resolve() {
const projectPath = utils.parseGitlabProjectPath(this.projectUrl);
if (!projectPath) {
this.setError('projectUrl');
}
if (!this.path) {
this.setError('path');
}
if (projectPath && this.path) {
const location = gitlabProvider.makeLocation(
this.config.token,
projectPath,
this.branch || 'master',
this.path,
);
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,67 @@
<template>
<modal-inner aria-label="Synchronize with GitLab">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitlab"></icon-provider>
</div>
<p>Create a workspace synced with a <b>GitLab</b> project folder.</p>
<form-entry label="Project URL" error="projectUrl">
<input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.serverUrl}}/path/to/project
</div>
</form-entry>
<form-entry label="Folder path" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the root folder will be used.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import utils from '../../../services/utils';
import modalTemplate from '../common/modalTemplate';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
projectUrl: 'gitlabWorkspaceProjectUrl',
},
methods: {
resolve() {
const projectPath = utils.parseGitlabProjectPath(this.projectUrl);
if (!projectPath) {
this.setError('projectUrl');
} else {
const path = this.path && this.path.replace(/^\//, '');
const url = utils.addQueryParams('app', {
providerId: 'gitlabWorkspace',
serverUrl: this.config.token.serverUrl,
projectPath,
branch: this.branch || 'master',
path: path || undefined,
sub: this.config.token.sub,
}, true);
this.config.resolve();
window.open(url);
}
},
},
});
</script>

View File

@ -57,6 +57,7 @@
import googleHelper from '../../../services/providers/helpers/googleHelper'; import googleHelper from '../../../services/providers/helpers/googleHelper';
import googleDriveProvider from '../../../services/providers/googleDriveProvider'; import googleDriveProvider from '../../../services/providers/googleDriveProvider';
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import store from '../../../store';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -69,12 +70,12 @@ export default modalTemplate({
}, },
methods: { methods: {
openFolder() { openFolder() {
return this.$store.dispatch( return store.dispatch(
'modal/hideUntil', 'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder') googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => { .then((folders) => {
if (folders[0]) { if (folders[0]) {
this.$store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
googleDriveFolderId: folders[0].id, googleDriveFolderId: folders[0].id,
}); });
} }

View File

@ -32,6 +32,7 @@
import googleHelper from '../../../services/providers/helpers/googleHelper'; import googleHelper from '../../../services/providers/helpers/googleHelper';
import googleDriveProvider from '../../../services/providers/googleDriveProvider'; import googleDriveProvider from '../../../services/providers/googleDriveProvider';
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import store from '../../../store';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -42,12 +43,12 @@ export default modalTemplate({
}, },
methods: { methods: {
openFolder() { openFolder() {
return this.$store.dispatch( return store.dispatch(
'modal/hideUntil', 'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder') googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => { .then((folders) => {
if (folders[0]) { if (folders[0]) {
this.$store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
googleDriveFolderId: folders[0].id, googleDriveFolderId: folders[0].id,
}); });
} }

View File

@ -26,6 +26,7 @@
import googleHelper from '../../../services/providers/helpers/googleHelper'; import googleHelper from '../../../services/providers/helpers/googleHelper';
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils'; import utils from '../../../services/utils';
import store from '../../../store';
export default modalTemplate({ export default modalTemplate({
computedLocalSettings: { computedLocalSettings: {
@ -33,12 +34,12 @@ export default modalTemplate({
}, },
methods: { methods: {
openFolder() { openFolder() {
return this.$store.dispatch( return store.dispatch(
'modal/hideUntil', 'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder') googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => { .then((folders) => {
if (folders[0]) { if (folders[0]) {
this.$store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
googleDriveWorkspaceFolderId: folders[0].id, googleDriveWorkspaceFolderId: folders[0].id,
}); });
} }

View File

@ -14,8 +14,10 @@
<form-entry label="Client Unique Identifier" error="clientId"> <form-entry label="Client Unique Identifier" error="clientId">
<input slot="field" class="textfield" type="text" v-model.trim="clientId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="clientId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
You have to configure an OAuth Client with redirect URL <b>{{redirectUrl}}</b><br> You have to configure an OAuth Client with redirect URL <b>{{redirectUrl}}</b>
<a href="https://support.zendesk.com/hc/en-us/articles/203663836" target="_blank"><b>More info</b></a> </div>
<div class="form-entry__actions">
<a href="https://support.zendesk.com/hc/en-us/articles/203663836" target="_blank">More info</a>
</div> </div>
</form-entry> </form-entry>
</div> </div>

View File

@ -20,11 +20,6 @@ export default {
'layoutSettings', 'layoutSettings',
'tokens', 'tokens',
], ],
userIdPrefixes: {
db: 'dropbox',
gh: 'github',
go: 'google',
},
textMaxLength: 250000, textMaxLength: 250000,
defaultName: 'Untitled', defaultName: 'Untitled',
}; };

View File

@ -19,6 +19,11 @@ export default () => ({
githubPublishTemplate: 'jekyllSite', githubPublishTemplate: 'jekyllSite',
gistIsPublic: false, gistIsPublic: false,
gistPublishTemplate: 'plainText', gistPublishTemplate: 'plainText',
gitlabServerUrl: '',
gitlabApplicationId: '',
gitlabProjectUrl: '',
gitlabWorkspaceProjectUrl: '',
gitlabPublishTemplate: 'plainText',
wordpressDomain: '', wordpressDomain: '',
wordpressPublishTemplate: 'plainHtml', wordpressPublishTemplate: 'plainHtml',
zendeskSiteUrl: '', zendeskSiteUrl: '',

View File

@ -77,8 +77,8 @@ turndown:
linkStyle: inlined linkStyle: inlined
linkReferenceStyle: full linkReferenceStyle: full
# GitHub commit messages # GitHub/GitLab commit messages
github: git:
createFileMessage: '{{path}} created from https://stackedit.io/' createFileMessage: '{{path}} created from https://stackedit.io/'
updateFileMessage: '{{path}} updated from https://stackedit.io/' updateFileMessage: '{{path}} updated from https://stackedit.io/'
deleteFileMessage: '{{path}} deleted from https://stackedit.io/' deleteFileMessage: '{{path}} deleted from https://stackedit.io/'

View File

@ -1,9 +1,9 @@
**Where is my data stored?** **Where is my data stored?**
If your workspace is not synced, your files are only stored inside your browser using the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) and are not stored anywhere else. If your workspace is not synced, your files are stored inside your browser and nowhere else.
We recommend syncing your workspace to make sure files won't be lost in case your browser data is cleared. Self-hosted CouchDB backend is best suited for privacy. We recommend syncing your workspace to make sure files won't be lost in case your browser data is cleared. Self-hosted CouchDB or GitLab backends are well suited for privacy.
**Can StackEdit access my data without telling me?** **Can StackEdit access my data without telling me?**
StackEdit is a frontend application. The access tokens issued by Google, Dropbox, GitHub... are stored in your browser and are not sent to any backend or 3^rd^ parties so your data won't be accessed by anyone. StackEdit is a browser-based application. The access tokens issued by Google, Dropbox, GitHub... are stored in your browser and are not sent to any kind of backend or 3^rd^ party so your data won't be accessed by anyone.

View File

@ -20,6 +20,8 @@ export default {
return 'github'; return 'github';
case 'gist': case 'gist':
return 'github'; return 'github';
case 'gitlabWorkspace':
return 'gitlab';
case 'bloggerPage': case 'bloggerPage':
return 'blogger'; return 'blogger';
case 'couchdbWorkspace': case 'couchdbWorkspace':
@ -57,6 +59,10 @@ export default {
background-image: url(../assets/iconGithub.svg); background-image: url(../assets/iconGithub.svg);
} }
.icon-provider--gitlab {
background-image: url(../assets/iconGitlab.svg);
}
.icon-provider--dropbox { .icon-provider--dropbox {
background-image: url(../assets/iconDropbox.svg); background-image: url(../assets/iconDropbox.svg);
} }

View File

@ -0,0 +1,235 @@
import store from '../store';
import utils from '../services/utils';
const endsWith = (str, suffix) => str.slice(-suffix.length) === suffix;
export default {
shaByPath: Object.create(null),
makeChanges(tree) {
const workspacePath = store.getters['workspace/currentWorkspace'].path || '';
// Store all blobs sha
this.shaByPath = Object.create(null);
// Store interesting paths
const treeFolderMap = Object.create(null);
const treeFileMap = Object.create(null);
const treeDataMap = Object.create(null);
const treeSyncLocationMap = Object.create(null);
const treePublishLocationMap = Object.create(null);
tree.filter(({ type, path }) => type === 'blob' && path.indexOf(workspacePath) === 0)
.forEach((blobEntry) => {
// Make path relative
const path = blobEntry.path.slice(workspacePath.length);
// Collect blob sha
this.shaByPath[path] = blobEntry.sha;
if (path.indexOf('.stackedit-data/') === 0) {
treeDataMap[path] = true;
} else {
// Collect parents path
let parentPath = '';
path.split('/').slice(0, -1).forEach((folderName) => {
const folderPath = `${parentPath}${folderName}/`;
treeFolderMap[folderPath] = parentPath;
parentPath = folderPath;
});
// Collect file path
if (endsWith(path, '.md')) {
treeFileMap[path] = parentPath;
} else if (endsWith(path, '.sync')) {
treeSyncLocationMap[path] = true;
} else if (endsWith(path, '.publish')) {
treePublishLocationMap[path] = true;
}
}
});
// Collect changes
const changes = [];
const idsByPath = {};
const syncDataByPath = store.getters['data/syncDataById'];
const { itemIdsByGitPath } = store.getters;
const getIdFromPath = (path, isFile) => {
let itemId = idsByPath[path];
if (!itemId) {
const existingItemId = itemIdsByGitPath[path];
if (existingItemId
// Reuse a file ID only if it has already been synced
&& (!isFile || syncDataByPath[path]
// Content may have already been synced
|| syncDataByPath[`/${path}`])
) {
itemId = existingItemId;
} else {
// Otherwise, make a new ID for a new item
itemId = utils.uid();
}
// If it's a file path, add the content path as well
if (isFile) {
idsByPath[`/${path}`] = `${itemId}/content`;
}
idsByPath[path] = itemId;
}
return itemId;
};
// Folder creations/updates
// Assume map entries are sorted from top to bottom
Object.entries(treeFolderMap).forEach(([path, parentPath]) => {
if (path === '.stackedit-trash/') {
idsByPath[path] = 'trash';
} else {
const item = utils.addItemHash({
id: getIdFromPath(path),
type: 'folder',
name: path.slice(parentPath.length, -1),
parentId: idsByPath[parentPath] || null,
});
const folderSyncData = syncDataByPath[path];
if (!folderSyncData || folderSyncData.hash !== item.hash) {
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
type: item.type,
hash: item.hash,
},
});
}
}
});
// File/content creations/updates
Object.entries(treeFileMap).forEach(([path, parentPath]) => {
const fileId = getIdFromPath(path, true);
const contentPath = `/${path}`;
const contentId = idsByPath[contentPath];
// File creations/updates
const item = utils.addItemHash({
id: fileId,
type: 'file',
name: path.slice(parentPath.length, -'.md'.length),
parentId: idsByPath[parentPath] || null,
});
const fileSyncData = syncDataByPath[path];
if (!fileSyncData || fileSyncData.hash !== item.hash) {
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
type: item.type,
hash: item.hash,
},
});
}
// Content creations/updates
const contentSyncData = syncDataByPath[contentPath];
if (!contentSyncData || contentSyncData.sha !== this.shaByPath[path]) {
const type = 'content';
// Use `/` as a prefix to get a unique syncData id
changes.push({
syncDataId: contentPath,
item: {
id: contentId,
type,
// Need a truthy value to force downloading the content
hash: 1,
},
syncData: {
id: contentPath,
type,
// Need a truthy value to force downloading the content
hash: 1,
},
});
}
});
// Data creations/updates
const syncDataByItemId = store.getters['data/syncDataByItemId'];
Object.keys(treeDataMap).forEach((path) => {
// Only template data are stored
const [, id] = path.match(/^\.stackedit-data\/(templates)\.json$/) || [];
if (id) {
idsByPath[path] = id;
const syncData = syncDataByItemId[id];
if (!syncData || syncData.sha !== this.shaByPath[path]) {
const type = 'data';
changes.push({
syncDataId: path,
item: {
id,
type,
// Need a truthy value to force saving sync data
hash: 1,
},
syncData: {
id: path,
type,
// Need a truthy value to force downloading the content
hash: 1,
},
});
}
}
});
// Location creations/updates
[{
type: 'syncLocation',
map: treeSyncLocationMap,
pathMatcher: /^([\s\S]+)\.([\w-]+)\.sync$/,
}, {
type: 'publishLocation',
map: treePublishLocationMap,
pathMatcher: /^([\s\S]+)\.([\w-]+)\.publish$/,
}]
.forEach(({ type, map, pathMatcher }) => Object.keys(map).forEach((path) => {
const [, filePath, data] = path.match(pathMatcher) || [];
if (filePath) {
// If there is a corresponding md file in the tree
const fileId = idsByPath[`${filePath}.md`];
if (fileId) {
// Reuse existing ID or create a new one
const id = itemIdsByGitPath[path] || utils.uid();
idsByPath[path] = id;
const item = utils.addItemHash({
...JSON.parse(utils.decodeBase64(data)),
id,
type,
fileId,
});
const locationSyncData = syncDataByPath[path];
if (!locationSyncData || locationSyncData.hash !== item.hash) {
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
type: item.type,
hash: item.hash,
},
});
}
}
}
}));
// Deletions
Object.keys(syncDataByPath).forEach((path) => {
if (!idsByPath[path]) {
changes.push({ syncDataId: path });
}
});
return changes;
},
};

View File

@ -127,7 +127,7 @@ export default new Provider({
const entries = await dropboxHelper.listRevisions(token, syncLocation.dropboxFileId); const entries = await dropboxHelper.listRevisions(token, syncLocation.dropboxFileId);
return entries.map(entry => ({ return entries.map(entry => ({
id: entry.rev, id: entry.rev,
sub: `db:${(entry.sharing_info || {}).modified_by || token.sub}`, sub: `${dropboxHelper.subPrefix}:${(entry.sharing_info || {}).modified_by || token.sub}`,
created: new Date(entry.server_modified).getTime(), created: new Date(entry.server_modified).getTime(),
})); }));
}, },

View File

@ -65,7 +65,7 @@ export default new Provider({
}); });
return entries.map((entry) => { return entries.map((entry) => {
const sub = `gh:${entry.user.id}`; const sub = `${githubHelper.subPrefix}:${entry.user.id}`;
userSvc.addInfo({ id: sub, name: entry.user.login, imageUrl: entry.user.avatar_url }); userSvc.addInfo({ id: sub, name: entry.user.login, imageUrl: entry.user.avatar_url });
return { return {
sub, sub,

View File

@ -134,7 +134,7 @@ export default new Provider({
} else if (committer && committer.login) { } else if (committer && committer.login) {
user = committer; user = committer;
} }
const sub = `gh:${user.id}`; const sub = `${githubHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date) const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date); || (commit.committer && commit.committer.date);

View File

@ -3,19 +3,11 @@ import githubHelper from './helpers/githubHelper';
import Provider from './common/Provider'; import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
import userSvc from '../userSvc'; import userSvc from '../userSvc';
import gitWorkspaceSvc from '../gitWorkspaceSvc';
const getAbsolutePath = ({ id }) => const getAbsolutePath = ({ id }) =>
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`; `${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
let treeShaMap;
let treeFolderMap;
let treeFileMap;
let treeDataMap;
let treeSyncLocationMap;
let treePublishLocationMap;
const endsWith = (str, suffix) => str.slice(-suffix.length) === suffix;
export default new Provider({ export default new Provider({
id: 'githubWorkspace', id: 'githubWorkspace',
name: 'GitHub', name: 'GitHub',
@ -90,238 +82,13 @@ export default new Provider({
return store.getters['workspace/workspacesById'][workspaceId]; return store.getters['workspace/workspacesById'][workspaceId];
}, },
getChanges() { getChanges() {
const syncToken = store.getters['workspace/syncToken'];
return githubHelper.getTree({ return githubHelper.getTree({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token: syncToken, token: this.getToken(),
}); });
}, },
prepareChanges(tree) { prepareChanges(tree) {
const workspacePath = store.getters['workspace/currentWorkspace'].path || ''; return gitWorkspaceSvc.makeChanges(tree);
// Store all blobs sha
treeShaMap = Object.create(null);
// Store interesting paths
treeFolderMap = Object.create(null);
treeFileMap = Object.create(null);
treeDataMap = Object.create(null);
treeSyncLocationMap = Object.create(null);
treePublishLocationMap = Object.create(null);
tree.filter(({ type, path }) => type === 'blob' && path.indexOf(workspacePath) === 0)
.forEach((blobEntry) => {
// Make path relative
const path = blobEntry.path.slice(workspacePath.length);
// Collect blob sha
treeShaMap[path] = blobEntry.sha;
if (path.indexOf('.stackedit-data/') === 0) {
treeDataMap[path] = true;
} else {
// Collect parents path
let parentPath = '';
path.split('/').slice(0, -1).forEach((folderName) => {
const folderPath = `${parentPath}${folderName}/`;
treeFolderMap[folderPath] = parentPath;
parentPath = folderPath;
});
// Collect file path
if (endsWith(path, '.md')) {
treeFileMap[path] = parentPath;
} else if (endsWith(path, '.sync')) {
treeSyncLocationMap[path] = true;
} else if (endsWith(path, '.publish')) {
treePublishLocationMap[path] = true;
}
}
});
// Collect changes
const changes = [];
const idsByPath = {};
const syncDataByPath = store.getters['data/syncDataById'];
const { itemIdsByGitPath } = store.getters;
const getIdFromPath = (path, isFile) => {
let itemId = idsByPath[path];
if (!itemId) {
const existingItemId = itemIdsByGitPath[path];
if (existingItemId
// Reuse a file ID only if it has already been synced
&& (!isFile || syncDataByPath[path]
// Content may have already been synced
|| syncDataByPath[`/${path}`])
) {
itemId = existingItemId;
} else {
// Otherwise, make a new ID for a new item
itemId = utils.uid();
}
// If it's a file path, add the content path as well
if (isFile) {
idsByPath[`/${path}`] = `${itemId}/content`;
}
idsByPath[path] = itemId;
}
return itemId;
};
// Folder creations/updates
// Assume map entries are sorted from top to bottom
Object.entries(treeFolderMap).forEach(([path, parentPath]) => {
if (path === '.stackedit-trash/') {
idsByPath[path] = 'trash';
} else {
const item = utils.addItemHash({
id: getIdFromPath(path),
type: 'folder',
name: path.slice(parentPath.length, -1),
parentId: idsByPath[parentPath] || null,
});
const folderSyncData = syncDataByPath[path];
if (!folderSyncData || folderSyncData.hash !== item.hash) {
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
type: item.type,
hash: item.hash,
},
});
}
}
});
// File/content creations/updates
Object.entries(treeFileMap).forEach(([path, parentPath]) => {
const fileId = getIdFromPath(path, true);
const contentPath = `/${path}`;
const contentId = idsByPath[contentPath];
// File creations/updates
const item = utils.addItemHash({
id: fileId,
type: 'file',
name: path.slice(parentPath.length, -'.md'.length),
parentId: idsByPath[parentPath] || null,
});
const fileSyncData = syncDataByPath[path];
if (!fileSyncData || fileSyncData.hash !== item.hash) {
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
type: item.type,
hash: item.hash,
},
});
}
// Content creations/updates
const contentSyncData = syncDataByPath[contentPath];
if (!contentSyncData || contentSyncData.sha !== treeShaMap[path]) {
const type = 'content';
// Use `/` as a prefix to get a unique syncData id
changes.push({
syncDataId: contentPath,
item: {
id: contentId,
type,
// Need a truthy value to force downloading the content
hash: 1,
},
syncData: {
id: contentPath,
type,
// Need a truthy value to force downloading the content
hash: 1,
},
});
}
});
// Data creations/updates
const syncDataByItemId = store.getters['data/syncDataByItemId'];
Object.keys(treeDataMap).forEach((path) => {
// Only template data are stored
const [, id] = path.match(/^\.stackedit-data\/(templates)\.json$/) || [];
if (id) {
idsByPath[path] = id;
const syncData = syncDataByItemId[id];
if (!syncData || syncData.sha !== treeShaMap[path]) {
const type = 'data';
changes.push({
syncDataId: path,
item: {
id,
type,
// Need a truthy value to force saving sync data
hash: 1,
},
syncData: {
id: path,
type,
// Need a truthy value to force downloading the content
hash: 1,
},
});
}
}
});
// Location creations/updates
[{
type: 'syncLocation',
map: treeSyncLocationMap,
pathMatcher: /^([\s\S]+)\.([\w-]+)\.sync$/,
}, {
type: 'publishLocation',
map: treePublishLocationMap,
pathMatcher: /^([\s\S]+)\.([\w-]+)\.publish$/,
}]
.forEach(({ type, map, pathMatcher }) => Object.keys(map).forEach((path) => {
const [, filePath, data] = path.match(pathMatcher) || [];
if (filePath) {
// If there is a corresponding md file in the tree
const fileId = idsByPath[`${filePath}.md`];
if (fileId) {
// Reuse existing ID or create a new one
const id = itemIdsByGitPath[path] || utils.uid();
idsByPath[path] = id;
const item = utils.addItemHash({
...JSON.parse(utils.decodeBase64(data)),
id,
type,
fileId,
});
const locationSyncData = syncDataByPath[path];
if (!locationSyncData || locationSyncData.hash !== item.hash) {
changes.push({
syncDataId: path,
item,
syncData: {
id: path,
type: item.type,
hash: item.hash,
},
});
}
}
}
}));
// Deletions
Object.keys(syncDataByPath).forEach((path) => {
if (!idsByPath[path]) {
changes.push({ syncDataId: path });
}
});
return changes;
}, },
async saveWorkspaceItem({ item }) { async saveWorkspaceItem({ item }) {
const syncData = { const syncData = {
@ -342,20 +109,20 @@ export default new Provider({
token: syncToken, token: syncToken,
path: getAbsolutePath(syncData), path: getAbsolutePath(syncData),
content: '', content: '',
sha: treeShaMap[syncData.id], sha: gitWorkspaceSvc.shaByPath[syncData.id],
}); });
// Return sync data to save // Return sync data to save
return { syncData }; return { syncData };
}, },
async removeWorkspaceItem({ syncData }) { async removeWorkspaceItem({ syncData }) {
if (treeShaMap[syncData.id]) { if (gitWorkspaceSvc.shaByPath[syncData.id]) {
const syncToken = store.getters['workspace/syncToken']; const syncToken = store.getters['workspace/syncToken'];
await githubHelper.removeFile({ await githubHelper.removeFile({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token: syncToken, token: syncToken,
path: getAbsolutePath(syncData), path: getAbsolutePath(syncData),
sha: treeShaMap[syncData.id], sha: gitWorkspaceSvc.shaByPath[syncData.id],
}); });
} }
}, },
@ -370,7 +137,7 @@ export default new Provider({
token, token,
path: getAbsolutePath(fileSyncData), path: getAbsolutePath(fileSyncData),
}); });
treeShaMap[fileSyncData.id] = sha; gitWorkspaceSvc.shaByPath[fileSyncData.id] = sha;
const content = Provider.parseContent(data, contentId); const content = Provider.parseContent(data, contentId);
return { return {
content, content,
@ -391,7 +158,7 @@ export default new Provider({
token, token,
path: getAbsolutePath(syncData), path: getAbsolutePath(syncData),
}); });
treeShaMap[syncData.id] = sha; gitWorkspaceSvc.shaByPath[syncData.id] = sha;
const item = JSON.parse(data); const item = JSON.parse(data);
return { return {
item, item,
@ -410,7 +177,7 @@ export default new Provider({
token, token,
path: absolutePath, path: absolutePath,
content: Provider.serializeContent(content), content: Provider.serializeContent(content),
sha: treeShaMap[path], sha: gitWorkspaceSvc.shaByPath[path],
}); });
// Return new sync data // Return new sync data
@ -440,7 +207,7 @@ export default new Provider({
token, token,
path: getAbsolutePath(syncData), path: getAbsolutePath(syncData),
content: JSON.stringify(item), content: JSON.stringify(item),
sha: treeShaMap[path], sha: gitWorkspaceSvc.shaByPath[path],
}); });
return { return {
@ -472,7 +239,7 @@ export default new Provider({
} else if (committer && committer.login) { } else if (committer && committer.login) {
user = committer; user = committer;
} }
const sub = `gh:${user.id}`; const sub = `${githubHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date) const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date); || (commit.committer && commit.committer.date);

View File

@ -0,0 +1,178 @@
import store from '../../store';
import gitlabHelper from './helpers/gitlabHelper';
import Provider from './common/Provider';
import utils from '../utils';
import workspaceSvc from '../workspaceSvc';
import userSvc from '../userSvc';
const savedSha = {};
export default new Provider({
id: 'gitlab',
name: 'GitLab',
getToken({ sub }) {
return store.getters['data/gitlabTokensBySub'][sub];
},
getLocationUrl({
sub,
projectPath,
branch,
path,
}) {
const token = this.getToken({ sub });
return `${token.serverUrl}/${projectPath}/blob/${encodeURIComponent(branch)}/${utils.encodeUrlPath(path)}`;
},
getLocationDescription({ path }) {
return path;
},
async downloadContent(token, syncLocation) {
const { sha, data } = await gitlabHelper.downloadFile({
...syncLocation,
token,
});
savedSha[syncLocation.id] = sha;
return Provider.parseContent(data, `${syncLocation.fileId}/content`);
},
async uploadContent(token, content, syncLocation) {
const updatedSyncLocation = {
...syncLocation,
projectId: await gitlabHelper.getProjectId(token, syncLocation),
};
if (!savedSha[updatedSyncLocation.id]) {
try {
// Get the last sha
await this.downloadContent(token, updatedSyncLocation);
} catch (e) {
// Ignore error
}
}
const sha = savedSha[updatedSyncLocation.id];
delete savedSha[updatedSyncLocation.id];
await gitlabHelper.uploadFile({
...updatedSyncLocation,
token,
content: Provider.serializeContent(content),
sha,
});
return updatedSyncLocation;
},
async publish(token, html, metadata, publishLocation) {
const updatedPublishLocation = {
...publishLocation,
projectId: await gitlabHelper.getProjectId(token, publishLocation),
};
try {
// Get the last sha
await this.downloadContent(token, updatedPublishLocation);
} catch (e) {
// Ignore error
}
const sha = savedSha[updatedPublishLocation.id];
delete savedSha[updatedPublishLocation.id];
await gitlabHelper.uploadFile({
...updatedPublishLocation,
token,
content: html,
sha,
});
return updatedPublishLocation;
},
async openFile(token, syncLocation) {
const updatedSyncLocation = {
...syncLocation,
projectId: await gitlabHelper.getProjectId(token, syncLocation),
};
// Check if the file exists and open it
if (!Provider.openFileWithLocation(updatedSyncLocation)) {
// Download content from GitLab
let content;
try {
content = await this.downloadContent(token, updatedSyncLocation);
} catch (e) {
store.dispatch('notification/error', `Could not open file ${updatedSyncLocation.path}.`);
return;
}
// Create the file
let name = updatedSyncLocation.path;
const slashPos = name.lastIndexOf('/');
if (slashPos > -1 && slashPos < name.length - 1) {
name = name.slice(slashPos + 1);
}
const dotPos = name.lastIndexOf('.');
if (dotPos > 0 && slashPos < name.length) {
name = name.slice(0, dotPos);
}
const item = await workspaceSvc.createFile({
name,
parentId: store.getters['file/current'].parentId,
text: content.text,
properties: content.properties,
discussions: content.discussions,
comments: content.comments,
}, true);
store.commit('file/setCurrentId', item.id);
workspaceSvc.addSyncLocation({
...updatedSyncLocation,
fileId: item.id,
});
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from GitLab.`);
}
},
makeLocation(token, projectPath, branch, path) {
return {
providerId: this.id,
sub: token.sub,
projectPath,
branch,
path,
};
},
async listFileRevisions({ token, syncLocation }) {
const entries = await gitlabHelper.getCommits({
...syncLocation,
token,
});
return entries.map(({
author,
committer,
commit,
sha,
}) => {
let user;
if (author && author.login) {
user = author;
} else if (committer && committer.login) {
user = committer;
}
const sub = `${gitlabHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date);
return {
id: sha,
sub,
created: date ? new Date(date).getTime() : 1,
};
});
},
async loadFileRevision() {
// Revision are already loaded
return false;
},
async getFileRevisionContent({
token,
contentId,
syncLocation,
revisionId,
}) {
const { data } = await gitlabHelper.downloadFile({
...syncLocation,
token,
branch: revisionId,
});
return Provider.parseContent(data, contentId);
},
});

View File

@ -0,0 +1,276 @@
import store from '../../store';
import gitlabHelper from './helpers/gitlabHelper';
import Provider from './common/Provider';
import utils from '../utils';
import userSvc from '../userSvc';
import gitWorkspaceSvc from '../gitWorkspaceSvc';
const getAbsolutePath = ({ id }) =>
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
export default new Provider({
id: 'gitlabWorkspace',
name: 'GitLab',
getToken() {
return store.getters['workspace/syncToken'];
},
getWorkspaceParams({
serverUrl,
projectPath,
branch,
path,
}) {
return {
providerId: this.id,
serverUrl,
projectPath,
branch,
path,
};
},
getWorkspaceLocationUrl({
serverUrl,
projectPath,
branch,
path,
}) {
return `${serverUrl}/${projectPath}/blob/${encodeURIComponent(branch)}/${utils.encodeUrlPath(path)}`;
},
getSyncDataUrl({ id }) {
const { projectPath, branch } = store.getters['workspace/currentWorkspace'];
const { serverUrl } = this.getToken();
return `${serverUrl}/${projectPath}/blob/${encodeURIComponent(branch)}/${utils.encodeUrlPath(getAbsolutePath({ id }))}`;
},
getSyncDataDescription({ id }) {
return getAbsolutePath({ id });
},
async initWorkspace() {
const { projectPath, branch } = utils.queryParams;
const workspaceParams = this.getWorkspaceParams({ projectPath, branch });
const path = (utils.queryParams.path || '')
.replace(/^\/*/, '') // Remove leading `/`
.replace(/\/*$/, '/'); // Add trailing `/`
if (path !== '/') {
workspaceParams.path = path;
}
const workspaceId = utils.makeWorkspaceId(workspaceParams);
const workspace = store.getters['workspace/workspacesById'][workspaceId];
// See if we already have a token
let token;
if (workspace) {
// Token sub is in the workspace
token = store.getters['data/gitlabTokensBySub'][workspace.sub];
}
if (!token) {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
token = await gitlabHelper.addAccount(serverUrl, applicationId);
}
if (!workspace) {
const pathEntries = (path || '').split('/');
const projectPathEntries = (projectPath || '').split('/');
const name = pathEntries[pathEntries.length - 2] // path ends with `/`
|| projectPathEntries[projectPathEntries.length - 1];
store.dispatch('workspace/patchWorkspacesById', {
[workspaceId]: {
...workspaceParams,
id: workspaceId,
sub: token.sub,
name,
},
});
}
return store.getters['workspace/workspacesById'][workspaceId];
},
getChanges() {
return gitlabHelper.getTree({
...store.getters['workspace/currentWorkspace'],
token: this.getToken(),
});
},
prepareChanges(tree) {
return gitWorkspaceSvc.makeChanges(tree.map(entry => ({
...entry,
sha: entry.id,
})));
},
async saveWorkspaceItem({ item }) {
const syncData = {
id: store.getters.gitPathsByItemId[item.id],
type: item.type,
hash: item.hash,
};
// Files and folders are not in git, only contents
if (item.type === 'file' || item.type === 'folder') {
return { syncData };
}
// locations are stored as paths, so we upload an empty file
const syncToken = store.getters['workspace/syncToken'];
await gitlabHelper.uploadFile({
...store.getters['workspace/currentWorkspace'],
token: syncToken,
path: getAbsolutePath(syncData),
content: '',
sha: gitWorkspaceSvc.shaByPath[syncData.id],
});
// Return sync data to save
return { syncData };
},
async removeWorkspaceItem({ syncData }) {
if (gitWorkspaceSvc.shaByPath[syncData.id]) {
const syncToken = store.getters['workspace/syncToken'];
await gitlabHelper.removeFile({
...store.getters['workspace/currentWorkspace'],
token: syncToken,
path: getAbsolutePath(syncData),
sha: gitWorkspaceSvc.shaByPath[syncData.id],
});
}
},
async downloadWorkspaceContent({
token,
contentId,
contentSyncData,
fileSyncData,
}) {
const { sha, data } = await gitlabHelper.downloadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: getAbsolutePath(fileSyncData),
});
gitWorkspaceSvc.shaByPath[fileSyncData.id] = sha;
const content = Provider.parseContent(data, contentId);
return {
content,
contentSyncData: {
...contentSyncData,
hash: content.hash,
sha,
},
};
},
async downloadWorkspaceData({ token, syncData }) {
if (!syncData) {
return {};
}
const { sha, data } = await gitlabHelper.downloadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: getAbsolutePath(syncData),
});
gitWorkspaceSvc.shaByPath[syncData.id] = sha;
const item = JSON.parse(data);
return {
item,
syncData: {
...syncData,
hash: item.hash,
sha,
},
};
},
async uploadWorkspaceContent({ token, content, file }) {
const path = store.getters.gitPathsByItemId[file.id];
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
const res = await gitlabHelper.uploadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: absolutePath,
content: Provider.serializeContent(content),
sha: gitWorkspaceSvc.shaByPath[path],
});
// Return new sync data
return {
contentSyncData: {
id: store.getters.gitPathsByItemId[content.id],
type: content.type,
hash: content.hash,
sha: res.content.sha,
},
fileSyncData: {
id: path,
type: 'file',
hash: file.hash,
},
};
},
async uploadWorkspaceData({ token, item }) {
const path = store.getters.gitPathsByItemId[item.id];
const syncData = {
id: path,
type: item.type,
hash: item.hash,
};
const res = await gitlabHelper.uploadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: getAbsolutePath(syncData),
content: JSON.stringify(item),
sha: gitWorkspaceSvc.shaByPath[path],
});
return {
syncData: {
...syncData,
sha: res.content.sha,
},
};
},
async listFileRevisions({ token, fileSyncData }) {
const { projectId, branch } = store.getters['workspace/currentWorkspace'];
const entries = await gitlabHelper.getCommits({
token,
projectId,
sha: branch,
path: getAbsolutePath(fileSyncData),
});
return entries.map(({
author,
committer,
commit,
sha,
}) => {
let user;
if (author && author.login) {
user = author;
} else if (committer && committer.login) {
user = committer;
}
const sub = `${gitlabHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date);
return {
id: sha,
sub,
created: date ? new Date(date).getTime() : 1,
};
});
},
async loadFileRevision() {
// Revisions are already loaded
return false;
},
async getFileRevisionContent({
token,
contentId,
fileSyncData,
revisionId,
}) {
const { data } = await gitlabHelper.downloadFile({
...store.getters['workspace/currentWorkspace'],
token,
branch: revisionId,
path: getAbsolutePath(fileSyncData),
});
return Provider.parseContent(data, contentId);
},
});

View File

@ -171,7 +171,7 @@ export default new Provider({
const revisions = await googleHelper.getAppDataFileRevisions(token, contentSyncData.id); const revisions = await googleHelper.getAppDataFileRevisions(token, contentSyncData.id);
return revisions.map(revision => ({ return revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: `go:${revision.lastModifyingUser.permissionId}`, sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`,
created: new Date(revision.modifiedTime).getTime(), created: new Date(revision.modifiedTime).getTime(),
})); }));
}, },

View File

@ -195,7 +195,7 @@ export default new Provider({
const revisions = await googleHelper.getFileRevisions(token, syncLocation.driveFileId); const revisions = await googleHelper.getFileRevisions(token, syncLocation.driveFileId);
return revisions.map(revision => ({ return revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: `go:${revision.lastModifyingUser.permissionId}`, sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`,
created: new Date(revision.modifiedTime).getTime(), created: new Date(revision.modifiedTime).getTime(),
})); }));
}, },

View File

@ -512,7 +512,7 @@ export default new Provider({
const revisions = await googleHelper.getFileRevisions(token, fileSyncData.id); const revisions = await googleHelper.getFileRevisions(token, fileSyncData.id);
return revisions.map(revision => ({ return revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: `go:${revision.lastModifyingUser.permissionId}`, sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`,
created: new Date(revision.modifiedTime).getTime(), created: new Date(revision.modifiedTime).getTime(),
})); }));
}, },

View File

@ -1,6 +1,7 @@
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import utils from '../../utils'; import utils from '../../utils';
import store from '../../../store'; import store from '../../../store';
import userSvc from '../../userSvc';
const request = async (token, options = {}) => { const request = async (token, options = {}) => {
const baseUrl = `${token.dbUrl}/`; const baseUrl = `${token.dbUrl}/`;
@ -117,7 +118,7 @@ export default {
method: 'POST', method: 'POST',
body: { item, time: Date.now() }, body: { item, time: Date.now() },
}; };
const userId = store.getters['workspace/userId']; const userId = userSvc.getCurrentUserId();
if (userId) { if (userId) {
options.body.sub = userId; options.body.sub = userId;
} }

View File

@ -1,4 +1,5 @@
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import userSvc from '../../userSvc';
import store from '../../../store'; import store from '../../../store';
const getAppKey = (fullAccess) => { const getAppKey = (fullAccess) => {
@ -22,10 +23,40 @@ const request = ({ accessToken }, options, args) => networkSvc.request({
}, },
}); });
/**
* https://www.dropbox.com/developers/documentation/http/documentation#users-get_account
*/
const subPrefix = 'db';
userSvc.setInfoResolver('dropbox', subPrefix, async (sub) => {
const dropboxToken = Object.values(store.getters['data/dropboxTokensBySub'])[0];
try {
const { body } = await request(dropboxToken, {
method: 'POST',
url: 'https://api.dropboxapi.com/2/users/get_account',
body: {
account_id: sub,
},
});
return {
id: `${subPrefix}:${body.account_id}`,
name: body.name.display_name,
imageUrl: body.profile_photo_url || '',
};
} catch (err) {
if (!dropboxToken || err.status !== 404) {
throw new Error('RETRY');
}
throw err;
}
});
export default { export default {
subPrefix,
/** /**
* https://www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize * https://www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize
* https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account
*/ */
async startOauth2(fullAccess, sub = null, silent = false) { async startOauth2(fullAccess, sub = null, silent = false) {
const { accessToken } = await networkSvc.startOauth2( const { accessToken } = await networkSvc.startOauth2(
@ -42,6 +73,11 @@ export default {
method: 'POST', method: 'POST',
url: 'https://api.dropboxapi.com/2/users/get_current_account', url: 'https://api.dropboxapi.com/2/users/get_current_account',
}); });
userSvc.addInfo({
id: `${subPrefix}:${body.account_id}`,
name: body.name.display_name,
imageUrl: body.profile_photo_url || '',
});
// Check the returned sub consistency // Check the returned sub consistency
if (sub && `${body.account_id}` !== sub) { if (sub && `${body.account_id}` !== sub) {
@ -64,28 +100,6 @@ export default {
return this.startOauth2(fullAccess); return this.startOauth2(fullAccess);
}, },
/**
* https://www.dropbox.com/developers/documentation/http/documentation#users-get_account
*/
async getAccount(token, userId) {
const { body } = await request(token, {
method: 'POST',
url: 'https://api.dropboxapi.com/2/users/get_account',
body: {
account_id: userId,
},
});
// Add user info to the store
store.commit('userInfo/addItem', {
id: `db:${body.account_id}`,
name: body.name.display_name,
imageUrl: body.profile_photo_url || '',
});
return body;
},
/** /**
* https://www.dropbox.com/developers/documentation/http/documentation#files-upload * https://www.dropbox.com/developers/documentation/http/documentation#files-upload
*/ */

View File

@ -1,6 +1,7 @@
import utils from '../../utils'; import utils from '../../utils';
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import userSvc from '../../userSvc';
const clientId = GITHUB_CLIENT_ID; const clientId = GITHUB_CLIENT_ID;
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist']; const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
@ -24,11 +25,39 @@ const repoRequest = (token, owner, repo, options) => request(token, {
.then(res => res.body); .then(res => res.body);
const getCommitMessage = (name, path) => { const getCommitMessage = (name, path) => {
const message = store.getters['data/computedSettings'].github[name]; const message = store.getters['data/computedSettings'].git[name];
return message.replace(/{{path}}/g, path); return message.replace(/{{path}}/g, path);
}; };
/**
* Getting a user from its userId is not feasible with API v3.
* Using an undocumented endpoint...
*/
const subPrefix = 'gh';
userSvc.setInfoResolver('github', subPrefix, async (sub) => {
try {
const user = (await networkSvc.request({
url: `https://api.github.com/user/${sub}`,
params: {
t: Date.now(), // Prevent from caching
},
})).body;
return {
id: `${subPrefix}:${user.id}`,
name: user.login,
imageUrl: user.avatar_url || '',
};
} catch (err) {
if (err.status !== 404) {
throw new Error('RETRY');
}
throw err;
}
});
export default { export default {
subPrefix,
/** /**
* https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/ * https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/
@ -61,6 +90,11 @@ export default {
access_token: accessToken, access_token: accessToken,
}, },
})).body; })).body;
userSvc.addInfo({
id: `${subPrefix}:${user.id}`,
name: user.login,
imageUrl: user.avatar_url || '',
});
// Check the returned sub consistency // Check the returned sub consistency
if (sub && `${user.id}` !== sub) { if (sub && `${user.id}` !== sub) {
@ -84,28 +118,6 @@ export default {
return this.startOauth2(getScopes({ repoFullAccess })); return this.startOauth2(getScopes({ repoFullAccess }));
}, },
/**
* Getting a user from its userId is not feasible with API v3.
* Using an undocumented endpoint...
*/
async getUser(userId) {
const user = (await networkSvc.request({
url: `https://api.github.com/user/${userId}`,
params: {
t: Date.now(), // Prevent from caching
},
})).body;
// Add user info to the store
store.commit('userInfo/addItem', {
id: `gh:${user.id}`,
name: user.login,
imageUrl: user.avatar_url || '',
});
return user;
},
/** /**
* https://developer.github.com/v3/repos/commits/#get-a-single-commit * https://developer.github.com/v3/repos/commits/#get-a-single-commit
* https://developer.github.com/v3/git/trees/#get-a-tree * https://developer.github.com/v3/git/trees/#get-a-tree

View File

@ -0,0 +1,207 @@
import utils from '../../utils';
import networkSvc from '../../networkSvc';
import store from '../../../store';
import userSvc from '../../userSvc';
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
...options,
url: `${serverUrl}/api/v4/${options.url}`,
headers: {
...options.headers || {},
Authorization: `Bearer ${accessToken}`,
},
})
.then(res => res.body);
const getCommitMessage = (name, path) => {
const message = store.getters['data/computedSettings'].git[name];
return message.replace(/{{path}}/g, path);
};
/**
* https://docs.gitlab.com/ee/api/users.html#for-user
*/
const subPrefix = 'gl';
userSvc.setInfoResolver('gitlab', subPrefix, async (sub) => {
try {
const [, serverUrl, id] = sub.match(/^(.+)\/([^/]+)$/);
const user = (await networkSvc.request({
url: `${serverUrl}/api/v4/users/${id}`,
})).body;
const uniqueSub = `${serverUrl}/${user.id}`;
return {
id: `${subPrefix}:${uniqueSub}`,
name: user.username,
imageUrl: user.avatar_url || '',
};
} catch (err) {
if (err.status !== 404) {
throw new Error('RETRY');
}
throw err;
}
});
export default {
subPrefix,
/**
* https://docs.gitlab.com/ee/api/oauth2.html
*/
async startOauth2(serverUrl, applicationId, sub = null, silent = false) {
const { accessToken } = await networkSvc.startOauth2(
`${serverUrl}/oauth/authorize`,
{
client_id: applicationId,
response_type: 'token',
scope: 'api',
},
silent,
);
// Call the user info endpoint
const user = await request({ accessToken, serverUrl }, {
url: 'user',
});
const uniqueSub = `${serverUrl}/${user.id}`;
userSvc.addInfo({
id: `${subPrefix}:${uniqueSub}`,
name: user.username,
imageUrl: user.avatar_url || '',
});
// Check the returned sub consistency
if (sub && uniqueSub !== sub) {
throw new Error('GitLab account ID not expected.');
}
// Build token object including scopes and sub
const token = {
accessToken,
name: user.username,
serverUrl,
sub: uniqueSub,
};
// Add token to gitlab tokens
store.dispatch('data/addGitlabToken', token);
return token;
},
addAccount(serverUrl, applicationId) {
return this.startOauth2(serverUrl, applicationId);
},
/**
* https://docs.gitlab.com/ee/api/projects.html#get-single-project
*/
async getProjectId(token, { projectPath, projectId }) {
if (projectId) {
return projectId;
}
const project = await request(token, {
url: `projects/${encodeURIComponent(projectPath)}`,
});
return project.id;
},
/**
* https://docs.gitlab.com/ee/api/repositories.html#list-repository-tree
*/
async getTree({
token,
projectId,
branch,
}) {
return request(token, {
url: `projects/${encodeURIComponent(projectId)}/repository/tree`,
params: {
ref: branch,
recursive: true,
},
});
},
/**
* https://docs.gitlab.com/ee/api/commits.html#list-repository-commits
*/
async getCommits({
token,
projectId,
branch,
path,
}) {
return request(token, {
url: `projects/${encodeURIComponent(projectId)}/repository/tree`,
params: {
ref_name: branch,
path,
},
});
},
/**
* https://docs.gitlab.com/ee/api/repository_files.html#create-new-file-in-repository
* https://docs.gitlab.com/ee/api/repository_files.html#update-existing-file-in-repository
*/
async uploadFile({
token,
projectId,
branch,
path,
content,
sha,
}) {
return request(token, {
method: sha ? 'PUT' : 'POST',
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
body: {
commit_message: getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path),
content,
last_commit_id: sha,
branch,
},
});
},
/**
* https://docs.gitlab.com/ee/api/repository_files.html#delete-existing-file-in-repository
*/
async removeFile({
token,
projectId,
branch,
path,
sha,
}) {
return request(token, {
method: 'DELETE',
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
body: {
commit_message: getCommitMessage('deleteFileMessage', path),
last_commit_id: sha,
branch,
},
});
},
/**
* https://docs.gitlab.com/ee/api/repository_files.html#get-file-from-repository
*/
async downloadFile({
token,
projectId,
branch,
path,
}) {
const res = await request(token, {
url: `projects/${encodeURIComponent(projectId)}/repository/files/${encodeURIComponent(path)}`,
params: { ref: branch },
});
return {
sha: res.last_commit_id,
data: utils.decodeBase64(res.content),
};
},
};

View File

@ -1,6 +1,7 @@
import utils from '../../utils'; import utils from '../../utils';
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import userSvc from '../../userSvc';
const clientId = GOOGLE_CLIENT_ID; const clientId = GOOGLE_CLIENT_ID;
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk'; const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
@ -34,7 +35,45 @@ if (utils.queryParams.providerId === 'googleDrive') {
} }
} }
/**
* https://developers.google.com/+/web/api/rest/latest/people/get
*/
const getUser = async (sub, token) => {
const { body } = await networkSvc.request(token
? {
method: 'GET',
url: `https://www.googleapis.com/plus/v1/people/${sub}`,
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
}
: {
method: 'GET',
url: `https://www.googleapis.com/plus/v1/people/${sub}?key=${apiKey}`,
}, true);
return body;
};
const subPrefix = 'go';
userSvc.setInfoResolver('google', subPrefix, async (sub) => {
try {
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
const body = await getUser(sub, googleToken);
return {
id: `${subPrefix}:${body.id}`,
name: body.displayName,
imageUrl: (body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
};
} catch (err) {
if (err.status !== 404) {
throw new Error('RETRY');
}
throw err;
}
});
export default { export default {
subPrefix,
folderMimeType: 'application/vnd.google-apps.folder', folderMimeType: 'application/vnd.google-apps.folder',
driveState, driveState,
driveActionFolder: null, driveActionFolder: null,
@ -119,16 +158,14 @@ export default {
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1, driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
}; };
try { // Call the user info endpoint
// Call the user info endpoint const user = await getUser('me', token);
token.name = (await this.getUser(token.sub)).displayName; token.name = user.displayName;
} catch (err) { userSvc.addInfo({
if (err.status === 404) { id: `${subPrefix}:${user.id}`,
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo.'); name: user.displayName,
} else { imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
throw err; });
}
}
if (existingToken) { if (existingToken) {
// We probably retrieved a new token with restricted scopes. // We probably retrieved a new token with restricted scopes.
@ -413,7 +450,7 @@ export default {
}); });
revisions.forEach((revision) => { revisions.forEach((revision) => {
store.commit('userInfo/addItem', { store.commit('userInfo/addItem', {
id: `go:${revision.lastModifyingUser.permissionId}`, id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
name: revision.lastModifyingUser.displayName, name: revision.lastModifyingUser.displayName,
imageUrl: revision.lastModifyingUser.photoLink, imageUrl: revision.lastModifyingUser.photoLink,
}); });
@ -454,22 +491,6 @@ export default {
return this.$downloadFileRevision(refreshedToken, fileId, revisionId); return this.$downloadFileRevision(refreshedToken, fileId, revisionId);
}, },
/**
* https://developers.google.com/+/web/api/rest/latest/people/get
*/
async getUser(userId) {
const { body } = await networkSvc.request({
method: 'GET',
url: `https://www.googleapis.com/plus/v1/people/${userId}?key=${apiKey}`,
}, true);
store.commit('userInfo/addItem', {
id: `go:${body.id}`,
name: body.displayName,
imageUrl: (body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
});
return body;
},
/** /**
* https://developers.google.com/drive/v3/reference/changes/list * https://developers.google.com/drive/v3/reference/changes/list
*/ */

View File

@ -5,11 +5,11 @@ import Provider from './common/Provider';
export default new Provider({ export default new Provider({
id: 'wordpress', id: 'wordpress',
name: 'WordPress', name: 'WordPress',
getToken(location) { getToken({ sub }) {
return store.getters['data/wordpressTokensBySub'][location.sub]; return store.getters['data/wordpressTokensBySub'][sub];
}, },
getLocationUrl(location) { getLocationUrl({ siteId, postId }) {
return `https://wordpress.com/post/${location.siteId}/${location.postId}`; return `https://wordpress.com/post/${siteId}/${postId}`;
}, },
getLocationDescription({ postId }) { getLocationDescription({ postId }) {
return postId; return postId;

View File

@ -5,12 +5,12 @@ import Provider from './common/Provider';
export default new Provider({ export default new Provider({
id: 'zendesk', id: 'zendesk',
name: 'Zendesk', name: 'Zendesk',
getToken(location) { getToken({ sub }) {
return store.getters['data/zendeskTokensBySub'][location.sub]; return store.getters['data/zendeskTokensBySub'][sub];
}, },
getLocationUrl(location) { getLocationUrl({ sub, locale, articleId }) {
const token = this.getToken(location); const token = this.getToken({ sub });
return `https://${token.subdomain}.zendesk.com/hc/${location.locale}/articles/${location.articleId}`; return `https://${token.subdomain}.zendesk.com/hc/${locale}/articles/${articleId}`;
}, },
getLocationDescription({ articleId }) { getLocationDescription({ articleId }) {
return articleId; return articleId;

View File

@ -129,7 +129,8 @@ const createPublishLocation = (publishLocation) => {
store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
async () => { async () => {
workspaceSvc.addPublishLocation(await publish(publishLocation)); const publishLocationToStore = await publish(publishLocation);
workspaceSvc.addPublishLocation(publishLocationToStore);
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`); store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
}, },
); );

View File

@ -7,6 +7,7 @@ import providerRegistry from './providers/common/providerRegistry';
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider'; import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
import './providers/couchdbWorkspaceProvider'; import './providers/couchdbWorkspaceProvider';
import './providers/githubWorkspaceProvider'; import './providers/githubWorkspaceProvider';
import './providers/gitlabWorkspaceProvider';
import './providers/googleDriveWorkspaceProvider'; import './providers/googleDriveWorkspaceProvider';
import tempFileSvc from './tempFileSvc'; import tempFileSvc from './tempFileSvc';
import workspaceSvc from './workspaceSvc'; import workspaceSvc from './workspaceSvc';

View File

@ -1,70 +1,79 @@
import googleHelper from './providers/helpers/googleHelper';
import githubHelper from './providers/helpers/githubHelper';
import store from '../store'; import store from '../store';
import dropboxHelper from './providers/helpers/dropboxHelper';
import constants from '../data/constants';
const promised = {}; const infoPromisesByUserId = {};
const infoResolversByType = {};
const subPrefixesByType = {};
const typesBySubPrefix = {};
const parseUserId = (userId) => { const parseUserId = (userId) => {
const prefix = userId[2] === ':' && userId.slice(0, 2); const prefix = userId[2] === ':' && userId.slice(0, 2);
const type = prefix && constants.userIdPrefixes[prefix]; const type = typesBySubPrefix[prefix];
return type ? [type, userId.slice(3)] : ['google', userId]; return type ? [type, userId.slice(3)] : ['google', userId];
}; };
export default { export default {
addInfo({ id, name, imageUrl }) { setInfoResolver(type, subPrefix, resolver) {
promised[id] = true; infoResolversByType[type] = resolver;
store.commit('userInfo/addItem', { id, name, imageUrl }); subPrefixesByType[type] = subPrefix;
typesBySubPrefix[subPrefix] = type;
},
getCurrentUserId() {
const loginToken = store.getters['workspace/loginToken'];
if (!loginToken) {
return null;
}
const loginType = store.getters['workspace/loginToken'];
const prefix = subPrefixesByType[loginType];
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
},
addInfo(info) {
infoPromisesByUserId[info.id] = Promise.resolve(info);
store.commit('userInfo/addItem', info);
}, },
async getInfo(userId) { async getInfo(userId) {
if (userId && !promised[userId]) { if (!userId) {
const [type, sub] = parseUserId(userId); return {};
}
// Try to find a token with this sub let infoPromise = infoPromisesByUserId[userId];
const token = store.getters[`data/${type}TokensBySub`][sub]; if (infoPromise) {
if (token) { return infoPromise;
store.commit('userInfo/addItem', { }
id: userId,
name: token.name,
});
}
// Get user info from provider const [type, sub] = parseUserId(userId);
if (!store.state.offline) {
promised[userId] = true; // Try to find a token with this sub to resolve name as soon as possible
switch (type) { const token = store.getters[`data/${type}TokensBySub`][sub];
case 'dropbox': { if (token) {
const dropboxToken = Object.values(store.getters['data/dropboxTokensBySub'])[0]; store.commit('userInfo/addItem', {
try { id: userId,
await dropboxHelper.getAccount(dropboxToken, sub); name: token.name,
} catch (err) { });
if (!token || err.status !== 404) { }
promised[userId] = false;
} if (store.state.offline) {
} return {};
break; }
// Get user info from helper
infoPromise = new Promise(async (resolve) => {
const infoResolver = infoResolversByType[type];
if (infoResolver) {
try {
const userInfo = await infoResolver(sub);
this.addInfo(userInfo);
resolve(userInfo);
} catch (err) {
if (err && err.message === 'RETRY') {
infoPromisesByUserId[userId] = null;
} }
case 'github': resolve({});
try {
await githubHelper.getUser(sub);
} catch (err) {
if (err.status !== 404) {
promised[userId] = false;
}
}
break;
case 'google':
default:
try {
await googleHelper.getUser(sub);
} catch (err) {
if (err.status !== 404) {
promised[userId] = false;
}
}
} }
} }
} });
infoPromisesByUserId[userId] = infoPromise;
return infoPromise;
}, },
}; };

View File

@ -287,6 +287,10 @@ export default {
repo: parsedRepo[2], repo: parsedRepo[2],
}; };
}, },
parseGitlabProjectPath(url) {
const parsedProject = url && url.match(/^https:\/\/[^/]+\/(.+?)(?:\.git|\/)?$/);
return parsedProject && parsedProject[1];
},
createHiddenIframe(url) { createHiddenIframe(url) {
const iframeElt = document.createElement('iframe'); const iframeElt = document.createElement('iframe');
iframeElt.style.position = 'absolute'; iframeElt.style.position = 'absolute';

View File

@ -85,7 +85,7 @@ const additionalTemplates = {
// For tokens // For tokens
const tokenAdder = providerId => ({ getters, dispatch }, token) => { const tokenAdder = providerId => ({ getters, dispatch }, token) => {
dispatch('patchTokensByProviderId', { dispatch('patchTokensByType', {
[providerId]: { [providerId]: {
...getters[`${providerId}TokensBySub`], ...getters[`${providerId}TokensBySub`],
[token.sub]: token, [token.sub]: token,
@ -188,13 +188,14 @@ export default {
return result; return result;
}, },
dataSyncDataById: getter('dataSyncData'), dataSyncDataById: getter('dataSyncData'),
tokensByProviderId: getter('tokens'), tokensByType: getter('tokens'),
googleTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.google || {}, googleTokensBySub: (state, { tokensByType }) => tokensByType.google || {},
couchdbTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.couchdb || {}, couchdbTokensBySub: (state, { tokensByType }) => tokensByType.couchdb || {},
dropboxTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.dropbox || {}, dropboxTokensBySub: (state, { tokensByType }) => tokensByType.dropbox || {},
githubTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.github || {}, githubTokensBySub: (state, { tokensByType }) => tokensByType.github || {},
wordpressTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.wordpress || {}, gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
zendeskTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.zendesk || {}, wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
}, },
actions: { actions: {
setSettings: setter('settings'), setSettings: setter('settings'),
@ -258,11 +259,12 @@ export default {
setSyncDataById: setter('syncData'), setSyncDataById: setter('syncData'),
patchSyncDataById: patcher('syncData'), patchSyncDataById: patcher('syncData'),
patchDataSyncDataById: patcher('dataSyncData'), patchDataSyncDataById: patcher('dataSyncData'),
patchTokensByProviderId: patcher('tokens'), patchTokensByType: patcher('tokens'),
addGoogleToken: tokenAdder('google'), addGoogleToken: tokenAdder('google'),
addCouchdbToken: tokenAdder('couchdb'), addCouchdbToken: tokenAdder('couchdb'),
addDropboxToken: tokenAdder('dropbox'), addDropboxToken: tokenAdder('dropbox'),
addGithubToken: tokenAdder('github'), addGithubToken: tokenAdder('github'),
addGitlabToken: tokenAdder('gitlab'),
addWordpressToken: tokenAdder('wordpress'), addWordpressToken: tokenAdder('wordpress'),
addZendeskToken: tokenAdder('zendesk'), addZendeskToken: tokenAdder('zendesk'),
}, },

View File

@ -1,6 +1,5 @@
import utils from '../services/utils'; import utils from '../services/utils';
import providerRegistry from '../services/providers/common/providerRegistry'; import providerRegistry from '../services/providers/common/providerRegistry';
import constants from '../data/constants';
export default { export default {
namespaced: true, namespaced: true,
@ -44,9 +43,11 @@ export default {
currentWorkspace: ({ currentWorkspaceId }, { workspacesById, mainWorkspace }) => currentWorkspace: ({ currentWorkspaceId }, { workspacesById, mainWorkspace }) =>
workspacesById[currentWorkspaceId] || mainWorkspace, workspacesById[currentWorkspaceId] || mainWorkspace,
currentWorkspaceIsGit: (state, { currentWorkspace }) => currentWorkspaceIsGit: (state, { currentWorkspace }) =>
currentWorkspace.providerId === 'githubWorkspace', currentWorkspace.providerId === 'githubWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace',
currentWorkspaceHasUniquePaths: (state, { currentWorkspace }) => currentWorkspaceHasUniquePaths: (state, { currentWorkspace }) =>
currentWorkspace.providerId === 'githubWorkspace', currentWorkspace.providerId === 'githubWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace',
lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`, lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`,
lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`, lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`,
mainWorkspaceToken: (state, getters, rootState, rootGetters) => mainWorkspaceToken: (state, getters, rootState, rootGetters) =>
@ -62,33 +63,28 @@ export default {
return rootGetters['data/googleTokensBySub'][currentWorkspace.sub]; return rootGetters['data/googleTokensBySub'][currentWorkspace.sub];
case 'githubWorkspace': case 'githubWorkspace':
return rootGetters['data/githubTokensBySub'][currentWorkspace.sub]; return rootGetters['data/githubTokensBySub'][currentWorkspace.sub];
case 'gitlabWorkspace':
return rootGetters['data/gitlabTokensBySub'][currentWorkspace.sub];
case 'couchdbWorkspace': case 'couchdbWorkspace':
return rootGetters['data/couchdbTokensBySub'][currentWorkspace.id]; return rootGetters['data/couchdbTokensBySub'][currentWorkspace.id];
default: default:
return mainWorkspaceToken; return mainWorkspaceToken;
} }
}, },
loginToken: (state, { currentWorkspace, mainWorkspaceToken }, rootState, rootGetters) => { loginType: (state, { currentWorkspace }) => {
switch (currentWorkspace.providerId) { switch (currentWorkspace.providerId) {
case 'googleDriveWorkspace': case 'googleDriveWorkspace':
return rootGetters['data/googleTokensBySub'][currentWorkspace.sub];
case 'githubWorkspace':
return rootGetters['data/githubTokensBySub'][currentWorkspace.sub];
default: default:
return mainWorkspaceToken; return 'google';
case 'githubWorkspace':
return 'github';
case 'gitlabWorkspace':
return 'gitlab';
} }
}, },
userId: (state, { loginToken }, rootState, rootGetters) => { loginToken: (state, { loginType, currentWorkspace }, rootState, rootGetters) => {
if (!loginToken) { const tokensBySub = rootGetters['data/tokensByType'][loginType];
return null; return tokensBySub && tokensBySub[currentWorkspace.sub];
}
const prefix = utils.someResult(Object.entries(constants.userIdPrefixes), ([key, value]) => {
if (rootGetters[`data/${value}TokensBySub`][loginToken.sub]) {
return key;
}
return null;
});
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
}, },
sponsorToken: (state, { mainWorkspaceToken }) => mainWorkspaceToken, sponsorToken: (state, { mainWorkspaceToken }) => mainWorkspaceToken,
}, },

View File

@ -49,6 +49,10 @@ body {
outline: none; outline: none;
} }
input[type=checkbox] {
outline: #349be8 auto 5px;
}
.icon { .icon {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -153,6 +157,7 @@ textarea {
color: #fff; color: #fff;
margin: -2px 0 -2px 4px; margin: -2px 0 -2px 4px;
padding: 10px 20px; padding: 10px 20px;
font-size: 18px;
&:active, &:active,
&:focus, &:focus,

View File

@ -9,10 +9,10 @@ $font-size-monospace: 0.85em;
$highlighting-color: #ff0; $highlighting-color: #ff0;
$selection-highlighting-color: #ff9632; $selection-highlighting-color: #ff9632;
$info-bg: transparentize($selection-highlighting-color, 0.85); $info-bg: transparentize($selection-highlighting-color, 0.85);
$code-border-radius: 2px; $code-border-radius: 3px;
$link-color: #0c93e4; $link-color: #0c93e4;
$error-color: #f31; $error-color: #f31;
$border-radius-base: 2px; $border-radius-base: 3px;
$hr-color: rgba(128, 128, 128, 0.2); $hr-color: rgba(128, 128, 128, 0.2);
$navbar-bg: #2c2c2c; $navbar-bg: #2c2c2c;
$navbar-color: mix($navbar-bg, #fff, 33%); $navbar-color: mix($navbar-bg, #fff, 33%);