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 sponsorSvc from '../services/sponsorSvc';
import tempFileSvc from '../services/tempFileSvc';
import store from '../store';
import './common/vueGlobals';
const themeClasses = {
@ -41,7 +42,7 @@ export default {
}),
computed: {
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;
},
},
@ -57,7 +58,7 @@ export default {
window.location.reload();
} else if (err && err.message !== 'RELOAD') {
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>
import { mapState } from 'vuex';
import store from '../store';
export default {
computed: {
@ -24,7 +25,7 @@ export default {
methods: {
close(item = null) {
this.resolve(item);
this.$store.dispatch('contextMenu/close');
store.dispatch('contextMenu/close');
},
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,9 @@
<template>
<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>
<modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
@ -15,6 +19,10 @@
import { mapGetters } from 'vuex';
import simpleModals from '../data/simpleModals';
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 FilePropertiesModal from './modals/FilePropertiesModal';
import SettingsModal from './modals/SettingsModal';
@ -46,6 +54,11 @@ import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal';
import GistSyncModal from './modals/providers/GistSyncModal';
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 BloggerPublishModal from './modals/providers/BloggerPublishModal';
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
@ -54,7 +67,7 @@ import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal';
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
.cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable'));
@ -90,6 +103,11 @@ export default {
GithubPublishModal,
GistSyncModal,
GistPublishModal,
GitlabAccountModal,
GitlabOpenModal,
GitlabPublishModal,
GitlabSaveModal,
GitlabWorkspaceModal,
WordpressPublishModal,
BloggerPublishModal,
BloggerPagePublishModal,
@ -99,6 +117,9 @@ export default {
CouchdbCredentialsModal,
},
computed: {
...mapGetters([
'isSponsor',
]),
...mapGetters('modal', [
'config',
]),
@ -118,6 +139,19 @@ export default {
},
},
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() {
this.config.reject();
editorSvc.clEditor.focus();
@ -135,25 +169,17 @@ export default {
}
},
onFocusInOut(evt) {
const isFocusIn = evt.type === 'focusin';
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
const { parentNode } = evt.target;
if (parentNode && parentNode.parentNode) {
// Focus effect
if (evt.target.parentNode.classList.contains('form-entry__field')
&& evt.target.parentNode.parentNode.classList.contains('form-entry')) {
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn);
if (parentNode.classList.contains('form-entry__field')
&& parentNode.parentNode.classList.contains('form-entry')) {
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() {
@ -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 {
margin: 0 auto;
width: 100%;
@ -291,7 +329,7 @@ export default {
.form-entry__label {
display: block;
font-size: 0.9rem;
color: #a0a0a0;
color: #808080;
.form-entry--focused & {
color: darken($link-color, 10%);
@ -307,17 +345,19 @@ export default {
}
.form-entry__field {
border: 1px solid #d8d8d8;
border: 1px solid #b0b0b0;
border-radius: $border-radius-base;
position: relative;
overflow: hidden;
.form-entry--focused & {
border-color: $link-color;
box-shadow: 0 0 0 2.5px transparentize($link-color, 0.67);
}
.form-entry--error & {
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 {
font-size: 0.75em;
opacity: 0.5;
opacity: 0.67;
line-height: 1.4;
margin: 0.25em 0;
}

View File

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

View File

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

View File

@ -44,6 +44,7 @@ import ImportMenu from './menus/ImportMenu';
import MoreMenu from './menus/MoreMenu';
import markdownSample from '../data/markdownSample.md';
import markdownConversionSvc from '../services/markdownConversionSvc';
import store from '../store';
const panelNames = {
menu: 'Menu',
@ -75,10 +76,10 @@ export default {
}),
computed: {
panel() {
if (this.$store.state.light) {
if (store.state.light) {
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';
},
panelName() {
@ -173,7 +174,7 @@ export default {
padding: 10px;
margin: -10px -10px 10px;
background-color: $info-bg;
font-size: 0.9em;
font-size: 0.95em;
p {
margin: 10px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ const readFile = file => new Promise((resolve) => {
reader.onload = (e) => {
const content = e.target.result;
if (content.match(/\uFFFD/)) {
this.$store.dispatch('notification/error', 'File is not readable.');
store.dispatch('notification/error', 'File is not readable.');
} else {
resolve(content);
}
@ -60,7 +60,7 @@ export default {
...Provider.parseContent(content),
name: file.name,
});
this.$store.commit('file/setCurrentId', item.id);
store.commit('file/setCurrentId', item.id);
},
async onImportHtml(evt) {
const file = evt.target.files[0];
@ -71,7 +71,7 @@ export default {
...Provider.parseContent(turndownService.turndown(sanitizedContent)),
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'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a>.
</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 class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
<div class="menu-entry__icon menu-entry__icon--disabled">
@ -38,18 +41,18 @@
</menu-entry>
<menu-entry @click.native="setPanel('workspaces')">
<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>
</menu-entry>
<hr>
<menu-entry @click.native="setPanel('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>
</menu-entry>
<menu-entry @click.native="setPanel('publish')">
<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>
</menu-entry>
<menu-entry @click.native="setPanel('history')">
@ -98,6 +101,8 @@ import providerRegistry from '../../services/providers/common/providerRegistry';
import UserImage from '../UserImage';
import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc';
import userSvc from '../../services/userSvc';
import store from '../../store';
export default {
components: {
@ -109,12 +114,23 @@ export default {
'currentWorkspace',
'syncToken',
'loginToken',
'userId',
]),
userId() {
return userSvc.getCurrentUserId();
},
workspaceLocationUrl() {
const provider = providerRegistry.providersById[this.currentWorkspace.providerId];
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: {
...mapActions('data', {
@ -130,7 +146,7 @@ export default {
},
async fileProperties() {
try {
await this.$store.dispatch('modal/open', 'fileProperties');
await store.dispatch('modal/open', 'fileProperties');
} catch (e) {
// Cancel
}

View File

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

View File

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

View File

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

View File

@ -17,6 +17,11 @@
<div>GitHub workspace</div>
<span>Add a workspace synced with a GitHub repository.</span>
</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">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<div>Google Drive workspace</div>
@ -33,6 +38,8 @@
import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import store from '../../store';
export default {
components: {
@ -50,7 +57,7 @@ export default {
methods: {
async addCouchdbWorkspace() {
try {
this.$store.dispatch('modal/open', {
store.dispatch('modal/open', {
type: 'couchdbWorkspace',
});
} catch (e) {
@ -59,17 +66,29 @@ export default {
},
async addGithubWorkspace() {
try {
this.$store.dispatch('modal/open', {
store.dispatch('modal/open', {
type: 'githubWorkspace',
});
} catch (e) {
// 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() {
try {
const token = await googleHelper.addDriveAccount(true);
this.$store.dispatch('modal/open', {
store.dispatch('modal/open', {
type: 'googleDriveWorkspace',
token,
});
@ -78,7 +97,7 @@ export default {
}
},
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 utils from '../../services/utils';
import presets from '../../data/presets';
import store from '../../store';
const simpleProperties = {
title: '',
@ -125,17 +126,17 @@ export default {
presets: () => Object.keys(presets).sort(),
tab: {
get() {
return this.$store.getters['data/localSettings'].filePropertiesTab;
return store.getters['data/localSettings'].filePropertiesTab;
},
set(value) {
this.$store.dispatch('data/patchLocalSettings', {
store.dispatch('data/patchLocalSettings', {
filePropertiesTab: value,
});
},
},
},
created() {
const content = this.$store.getters['content/current'];
const content = store.getters['content/current'];
this.contentId = content.id;
this.setYamlProperties(content.properties);
if (this.tab !== 'yaml') {
@ -214,7 +215,7 @@ export default {
if (this.error) {
this.setYamlTab();
} else {
this.$store.commit('content/patchItem', {
store.commit('content/patchItem', {
id: this.contentId,
properties: utils.sanitizeText(this.yamlProperties),
});

View File

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

View File

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

View File

@ -32,6 +32,7 @@ import networkSvc from '../../services/networkSvc';
import editorSvc from '../../services/editorSvc';
import googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate';
import store from '../../store';
export default modalTemplate({
computedLocalSettings: {
@ -40,13 +41,13 @@ export default modalTemplate({
methods: {
async resolve() {
this.config.resolve();
const currentFile = this.$store.getters['file/current'];
const currentContent = this.$store.getters['content/current'];
const currentFile = store.getters['file/current'];
const currentContent = store.getters['content/current'];
const { selectedFormat } = this;
this.$store.dispatch('queue/enqueue', async () => {
store.dispatch('queue/enqueue', async () => {
const [sponsorToken, token] = await Promise.all([
Promise.resolve().then(() => {
const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
const tokenToRefresh = store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}),
sponsorSvc.getToken(),
@ -60,7 +61,7 @@ export default modalTemplate({
token,
idToken: sponsorToken && sponsorToken.idToken,
format: selectedFormat,
options: JSON.stringify(this.$store.getters['data/computedSettings'].pandoc),
options: JSON.stringify(store.getters['data/computedSettings'].pandoc),
metadata: JSON.stringify(currentContent.properties),
},
body: JSON.stringify(editorSvc.getPandocAst()),
@ -70,10 +71,10 @@ export default modalTemplate({
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
} catch (err) {
if (err.status === 401) {
this.$store.dispatch('modal/open', 'sponsorOnly');
store.dispatch('modal/open', 'sponsorOnly');
} else {
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 googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate';
import store from '../../store';
export default modalTemplate({
computedLocalSettings: {
@ -35,11 +36,11 @@ export default modalTemplate({
methods: {
async resolve() {
this.config.resolve();
const currentFile = this.$store.getters['file/current'];
this.$store.dispatch('queue/enqueue', async () => {
const currentFile = store.getters['file/current'];
store.dispatch('queue/enqueue', async () => {
const [sponsorToken, token, html] = await Promise.all([
Promise.resolve().then(() => {
const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
const tokenToRefresh = store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}),
sponsorSvc.getToken(),
@ -57,7 +58,7 @@ export default modalTemplate({
params: {
token,
idToken: sponsorToken && sponsorToken.idToken,
options: JSON.stringify(this.$store.getters['data/computedSettings'].wkhtmltopdf),
options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf),
},
body: html,
blob: true,
@ -66,10 +67,10 @@ export default modalTemplate({
FileSaver.saveAs(body, `${currentFile.name}.pdf`);
} catch (err) {
if (err.status === 401) {
this.$store.dispatch('modal/open', 'sponsorOnly');
store.dispatch('modal/open', 'sponsorOnly');
} else {
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>
import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner';
import store from '../../store';
export default {
components: {
@ -62,7 +63,7 @@ export default {
publishLocations: 'current',
}),
currentFileName() {
return this.$store.getters['file/current'].name;
return store.getters['file/current'].name;
},
},
methods: {
@ -70,7 +71,7 @@ export default {
'info',
]),
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 CodeEditor from '../CodeEditor';
import defaultSettings from '../../data/defaults/defaultSettings.yml';
import store from '../../store';
const emptySettings = `# Add your custom settings here to override the
# default settings.
@ -63,7 +64,7 @@ export default {
},
},
created() {
const settings = this.$store.getters['data/settings'];
const settings = store.getters['data/settings'];
this.setCustomSettings(settings === '\n' ? emptySettings : settings);
},
methods: {

View File

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

View File

@ -49,6 +49,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner';
import store from '../../store';
export default {
components: {
@ -62,7 +63,7 @@ export default {
syncLocations: 'currentWithWorkspaceSyncLocation',
}),
currentFileName() {
return this.$store.getters['file/current'].name;
return store.getters['file/current'].name;
},
},
methods: {
@ -73,7 +74,7 @@ export default {
if (location.id === 'main') {
this.info('This location can not be removed.');
} 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 emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
import emptyTemplateHelpers from '!raw-loader!../../data/empties/emptyTemplateHelpers.js'; // eslint-disable-line
import store from '../../store';
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
@ -91,7 +92,7 @@ export default {
},
created() {
this.$watch(
() => this.$store.getters['data/allTemplatesById'],
() => store.getters['data/allTemplatesById'],
(allTemplatesById) => {
const templates = {};
// Sort templates by name

View File

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

View File

@ -4,10 +4,6 @@
<button class="modal__close-button button not-tabbable" @click="config.reject()" v-title="'Close modal'">
<icon-close></icon-close>
</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>
</div>
</div>
@ -15,33 +11,12 @@
<script>
import { mapGetters } from 'vuex';
import googleHelper from '../../../services/providers/helpers/googleHelper';
import syncSvc from '../../../services/syncSvc';
export default {
computed: {
...mapGetters('modal', [
'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>
@ -64,15 +39,4 @@ export default {
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>

View File

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

View File

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

View File

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

View File

@ -11,12 +11,6 @@
<b>Example:</b> https://github.com/benweet/stackedit
</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="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -24,6 +18,12 @@
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>

View File

@ -11,18 +11,18 @@
<b>Example:</b> https://github.com/benweet/stackedit
</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="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>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
**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?**
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';
case 'gist':
return 'github';
case 'gitlabWorkspace':
return 'gitlab';
case 'bloggerPage':
return 'blogger';
case 'couchdbWorkspace':
@ -57,6 +59,10 @@ export default {
background-image: url(../assets/iconGithub.svg);
}
.icon-provider--gitlab {
background-image: url(../assets/iconGitlab.svg);
}
.icon-provider--dropbox {
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);
return entries.map(entry => ({
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(),
}));
},

View File

@ -65,7 +65,7 @@ export default new Provider({
});
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 });
return {
sub,

View File

@ -134,7 +134,7 @@ export default new Provider({
} else if (committer && committer.login) {
user = committer;
}
const sub = `gh:${user.id}`;
const sub = `${githubHelper.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);

View File

@ -3,19 +3,11 @@ import githubHelper from './helpers/githubHelper';
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}`;
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({
id: 'githubWorkspace',
name: 'GitHub',
@ -90,238 +82,13 @@ export default new Provider({
return store.getters['workspace/workspacesById'][workspaceId];
},
getChanges() {
const syncToken = store.getters['workspace/syncToken'];
return githubHelper.getTree({
...store.getters['workspace/currentWorkspace'],
token: syncToken,
token: this.getToken(),
});
},
prepareChanges(tree) {
const workspacePath = store.getters['workspace/currentWorkspace'].path || '';
// 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;
return gitWorkspaceSvc.makeChanges(tree);
},
async saveWorkspaceItem({ item }) {
const syncData = {
@ -342,20 +109,20 @@ export default new Provider({
token: syncToken,
path: getAbsolutePath(syncData),
content: '',
sha: treeShaMap[syncData.id],
sha: gitWorkspaceSvc.shaByPath[syncData.id],
});
// Return sync data to save
return { syncData };
},
async removeWorkspaceItem({ syncData }) {
if (treeShaMap[syncData.id]) {
if (gitWorkspaceSvc.shaByPath[syncData.id]) {
const syncToken = store.getters['workspace/syncToken'];
await githubHelper.removeFile({
...store.getters['workspace/currentWorkspace'],
token: syncToken,
path: getAbsolutePath(syncData),
sha: treeShaMap[syncData.id],
sha: gitWorkspaceSvc.shaByPath[syncData.id],
});
}
},
@ -370,7 +137,7 @@ export default new Provider({
token,
path: getAbsolutePath(fileSyncData),
});
treeShaMap[fileSyncData.id] = sha;
gitWorkspaceSvc.shaByPath[fileSyncData.id] = sha;
const content = Provider.parseContent(data, contentId);
return {
content,
@ -391,7 +158,7 @@ export default new Provider({
token,
path: getAbsolutePath(syncData),
});
treeShaMap[syncData.id] = sha;
gitWorkspaceSvc.shaByPath[syncData.id] = sha;
const item = JSON.parse(data);
return {
item,
@ -410,7 +177,7 @@ export default new Provider({
token,
path: absolutePath,
content: Provider.serializeContent(content),
sha: treeShaMap[path],
sha: gitWorkspaceSvc.shaByPath[path],
});
// Return new sync data
@ -440,7 +207,7 @@ export default new Provider({
token,
path: getAbsolutePath(syncData),
content: JSON.stringify(item),
sha: treeShaMap[path],
sha: gitWorkspaceSvc.shaByPath[path],
});
return {
@ -472,7 +239,7 @@ export default new Provider({
} else if (committer && committer.login) {
user = committer;
}
const sub = `gh:${user.id}`;
const sub = `${githubHelper.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);

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);
return revisions.map(revision => ({
id: revision.id,
sub: `go:${revision.lastModifyingUser.permissionId}`,
sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`,
created: new Date(revision.modifiedTime).getTime(),
}));
},

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import networkSvc from '../../networkSvc';
import userSvc from '../../userSvc';
import store from '../../../store';
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 {
subPrefix,
/**
* 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) {
const { accessToken } = await networkSvc.startOauth2(
@ -42,6 +73,11 @@ export default {
method: 'POST',
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
if (sub && `${body.account_id}` !== sub) {
@ -64,28 +100,6 @@ export default {
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
*/

View File

@ -1,6 +1,7 @@
import utils from '../../utils';
import networkSvc from '../../networkSvc';
import store from '../../../store';
import userSvc from '../../userSvc';
const clientId = GITHUB_CLIENT_ID;
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);
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);
};
/**
* 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 {
subPrefix,
/**
* https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/
@ -61,6 +90,11 @@ export default {
access_token: accessToken,
},
})).body;
userSvc.addInfo({
id: `${subPrefix}:${user.id}`,
name: user.login,
imageUrl: user.avatar_url || '',
});
// Check the returned sub consistency
if (sub && `${user.id}` !== sub) {
@ -84,28 +118,6 @@ export default {
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/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 networkSvc from '../../networkSvc';
import store from '../../../store';
import userSvc from '../../userSvc';
const clientId = GOOGLE_CLIENT_ID;
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 {
subPrefix,
folderMimeType: 'application/vnd.google-apps.folder',
driveState,
driveActionFolder: null,
@ -119,16 +158,14 @@ export default {
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
};
try {
// Call the user info endpoint
token.name = (await this.getUser(token.sub)).displayName;
} catch (err) {
if (err.status === 404) {
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo.');
} else {
throw err;
}
}
// Call the user info endpoint
const user = await getUser('me', token);
token.name = user.displayName;
userSvc.addInfo({
id: `${subPrefix}:${user.id}`,
name: user.displayName,
imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
});
if (existingToken) {
// We probably retrieved a new token with restricted scopes.
@ -413,7 +450,7 @@ export default {
});
revisions.forEach((revision) => {
store.commit('userInfo/addItem', {
id: `go:${revision.lastModifyingUser.permissionId}`,
id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
name: revision.lastModifyingUser.displayName,
imageUrl: revision.lastModifyingUser.photoLink,
});
@ -454,22 +491,6 @@ export default {
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
*/

View File

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

View File

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

View File

@ -129,7 +129,8 @@ const createPublishLocation = (publishLocation) => {
store.dispatch(
'queue/enqueue',
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}".`);
},
);

View File

@ -7,6 +7,7 @@ import providerRegistry from './providers/common/providerRegistry';
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
import './providers/couchdbWorkspaceProvider';
import './providers/githubWorkspaceProvider';
import './providers/gitlabWorkspaceProvider';
import './providers/googleDriveWorkspaceProvider';
import tempFileSvc from './tempFileSvc';
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 dropboxHelper from './providers/helpers/dropboxHelper';
import constants from '../data/constants';
const promised = {};
const infoPromisesByUserId = {};
const infoResolversByType = {};
const subPrefixesByType = {};
const typesBySubPrefix = {};
const parseUserId = (userId) => {
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];
};
export default {
addInfo({ id, name, imageUrl }) {
promised[id] = true;
store.commit('userInfo/addItem', { id, name, imageUrl });
setInfoResolver(type, subPrefix, resolver) {
infoResolversByType[type] = resolver;
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) {
if (userId && !promised[userId]) {
const [type, sub] = parseUserId(userId);
if (!userId) {
return {};
}
// Try to find a token with this sub
const token = store.getters[`data/${type}TokensBySub`][sub];
if (token) {
store.commit('userInfo/addItem', {
id: userId,
name: token.name,
});
}
let infoPromise = infoPromisesByUserId[userId];
if (infoPromise) {
return infoPromise;
}
// Get user info from provider
if (!store.state.offline) {
promised[userId] = true;
switch (type) {
case 'dropbox': {
const dropboxToken = Object.values(store.getters['data/dropboxTokensBySub'])[0];
try {
await dropboxHelper.getAccount(dropboxToken, sub);
} catch (err) {
if (!token || err.status !== 404) {
promised[userId] = false;
}
}
break;
const [type, sub] = parseUserId(userId);
// Try to find a token with this sub to resolve name as soon as possible
const token = store.getters[`data/${type}TokensBySub`][sub];
if (token) {
store.commit('userInfo/addItem', {
id: userId,
name: token.name,
});
}
if (store.state.offline) {
return {};
}
// 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':
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;
}
}
resolve({});
}
}
}
});
infoPromisesByUserId[userId] = infoPromise;
return infoPromise;
},
};

View File

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

View File

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

View File

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

View File

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

View File

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