New workspace and sync management modals. UI enhancements
This commit is contained in:
parent
7a87015af1
commit
a1673d3e87
@ -21,7 +21,7 @@ import syncSvc from '../services/syncSvc';
|
|||||||
import networkSvc from '../services/networkSvc';
|
import networkSvc from '../services/networkSvc';
|
||||||
import sponsorSvc from '../services/sponsorSvc';
|
import sponsorSvc from '../services/sponsorSvc';
|
||||||
import tempFileSvc from '../services/tempFileSvc';
|
import tempFileSvc from '../services/tempFileSvc';
|
||||||
import './common/globals';
|
import './common/vueGlobals';
|
||||||
|
|
||||||
const themeClasses = {
|
const themeClasses = {
|
||||||
light: ['app--light'],
|
light: ['app--light'],
|
||||||
@ -53,7 +53,7 @@ export default {
|
|||||||
this.ready = true;
|
this.ready = true;
|
||||||
tempFileSvc.setReady();
|
tempFileSvc.setReady();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && err.message !== 'reload') {
|
if (err && err.message !== 'RELOAD') {
|
||||||
console.error(err); // eslint-disable-line no-console
|
console.error(err); // eslint-disable-line no-console
|
||||||
this.$store.dispatch('notification/error', err);
|
this.$store.dispatch('notification/error', err);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import cledit from '../services/cledit';
|
import cledit from '../services/editor/cledit';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['value', 'lang', 'disabled'],
|
props: ['value', 'lang', 'disabled'],
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapMutations, mapActions } from 'vuex';
|
import { mapMutations, mapActions } from 'vuex';
|
||||||
import fileSvc from '../services/fileSvc';
|
import workspaceSvc from '../services/workspaceSvc';
|
||||||
import explorerSvc from '../services/explorerSvc';
|
import explorerSvc from '../services/explorerSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -102,10 +102,10 @@ export default {
|
|||||||
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
|
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
|
||||||
try {
|
try {
|
||||||
if (newChildNode.isFolder) {
|
if (newChildNode.isFolder) {
|
||||||
const item = await fileSvc.storeItem(newChildNode.item);
|
const item = await workspaceSvc.storeItem(newChildNode.item);
|
||||||
this.select(item.id);
|
this.select(item.id);
|
||||||
} else {
|
} else {
|
||||||
const item = await fileSvc.createFile(newChildNode.item);
|
const item = await workspaceSvc.createFile(newChildNode.item);
|
||||||
this.select(item.id);
|
this.select(item.id);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -120,7 +120,7 @@ export default {
|
|||||||
this.setEditingId(null);
|
this.setEditingId(null);
|
||||||
if (!cancel && item.id && value) {
|
if (!cancel && item.id && value) {
|
||||||
try {
|
try {
|
||||||
await fileSvc.storeItem({
|
await workspaceSvc.storeItem({
|
||||||
...item,
|
...item,
|
||||||
name: value,
|
name: value,
|
||||||
});
|
});
|
||||||
@ -147,7 +147,7 @@ export default {
|
|||||||
&& !targetNode.isNil
|
&& !targetNode.isNil
|
||||||
&& sourceNode.item.id !== targetNode.item.id
|
&& sourceNode.item.id !== targetNode.item.id
|
||||||
) {
|
) {
|
||||||
fileSvc.storeItem({
|
workspaceSvc.storeItem({
|
||||||
...sourceNode.item,
|
...sourceNode.item,
|
||||||
parentId: targetNode.item.id,
|
parentId: targetNode.item.id,
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
import cledit from '../services/cledit';
|
import cledit from '../services/editor/cledit';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import EditorClassApplier from './common/EditorClassApplier';
|
import EditorClassApplier from './common/EditorClassApplier';
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
|
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
|
<button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
|
||||||
<button class="button" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
|
<button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</div>
|
</div>
|
||||||
@ -250,7 +250,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal__sub-title {
|
.modal__sub-title {
|
||||||
opacity: 0.5;
|
opacity: 0.6;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
@ -278,8 +278,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal__button-bar {
|
.modal__button-bar {
|
||||||
margin-top: 1.75rem;
|
margin-top: 2rem;
|
||||||
text-align: right;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-entry {
|
.form-entry {
|
||||||
|
@ -56,7 +56,7 @@ import tempFileSvc from '../services/tempFileSvc';
|
|||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
import pagedownButtons from '../data/pagedownButtons';
|
import pagedownButtons from '../data/pagedownButtons';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import fileSvc from '../services/fileSvc';
|
import workspaceSvc from '../services/workspaceSvc';
|
||||||
|
|
||||||
// According to mousetrap
|
// According to mousetrap
|
||||||
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
|
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
|
||||||
@ -200,7 +200,7 @@ export default {
|
|||||||
this.title = this.$store.getters['file/current'].name;
|
this.title = this.$store.getters['file/current'].name;
|
||||||
if (title) {
|
if (title) {
|
||||||
try {
|
try {
|
||||||
await fileSvc.storeItem({
|
await workspaceSvc.storeItem({
|
||||||
...this.$store.getters['file/current'],
|
...this.$store.getters['file/current'],
|
||||||
name: title,
|
name: title,
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="side-bar__inner">
|
<div class="side-bar__inner">
|
||||||
<main-menu v-if="panel === 'menu'"></main-menu>
|
<main-menu v-if="panel === 'menu'"></main-menu>
|
||||||
<workspaces-menu v-if="panel === 'workspaces'"></workspaces-menu>
|
<workspaces-menu v-else-if="panel === 'workspaces'"></workspaces-menu>
|
||||||
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
||||||
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||||
@ -75,7 +75,11 @@ export default {
|
|||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
panel() {
|
panel() {
|
||||||
return !this.$store.state.light && this.$store.getters['data/layoutSettings'].sideBarPanel;
|
if (this.$store.state.light) {
|
||||||
|
return null; // No menu in light mode
|
||||||
|
}
|
||||||
|
const result = this.$store.getters['data/layoutSettings'].sideBarPanel;
|
||||||
|
return panelNames[result] ? result : 'menu';
|
||||||
},
|
},
|
||||||
panelName() {
|
panelName() {
|
||||||
return panelNames[this.panel];
|
return panelNames[this.panel];
|
||||||
@ -164,7 +168,7 @@ export default {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin: -10px -10px 10px;
|
margin: -10px -10px 10px;
|
||||||
background-color: $info-bg;
|
background-color: $info-bg;
|
||||||
font-size: 0.95em;
|
font-size: 0.9em;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
@ -64,7 +64,7 @@ export default {
|
|||||||
const updateMaskY = () => {
|
const updateMaskY = () => {
|
||||||
const scrollPosition = editorSvc.getScrollPosition();
|
const scrollPosition = editorSvc.getScrollPosition();
|
||||||
if (scrollPosition) {
|
if (scrollPosition) {
|
||||||
const sectionDesc = editorSvc.previewCtx.sectionDescList[scrollPosition.sectionIdx];
|
const sectionDesc = editorSvc.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
|
||||||
this.maskY = sectionDesc.tocDimension.startOffset +
|
this.maskY = sectionDesc.tocDimension.startOffset +
|
||||||
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
|
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import cledit from '../../services/cledit';
|
import cledit from '../../services/editor/cledit';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import cledit from '../../services/cledit';
|
import cledit from '../../services/editor/cledit';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import Clipboard from 'clipboard';
|
||||||
import timeSvc from '../../services/timeSvc';
|
import timeSvc from '../../services/timeSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
@ -32,10 +33,43 @@ Vue.directive('show', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setElTitle = (el, title) => {
|
||||||
|
el.title = title;
|
||||||
|
el.setAttribute('aria-label', title);
|
||||||
|
};
|
||||||
Vue.directive('title', {
|
Vue.directive('title', {
|
||||||
bind(el, { value }) {
|
bind(el, { value }) {
|
||||||
el.title = value;
|
setElTitle(el, value);
|
||||||
el.setAttribute('aria-label', value);
|
},
|
||||||
|
update(el, { value, oldValue }) {
|
||||||
|
if (value !== oldValue) {
|
||||||
|
setElTitle(el, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clipboard directive
|
||||||
|
const createClipboard = (el, value) => {
|
||||||
|
el.seClipboard = new Clipboard(el, { text: () => value });
|
||||||
|
};
|
||||||
|
const destroyClipboard = (el) => {
|
||||||
|
if (el.seClipboard) {
|
||||||
|
el.seClipboard.destroy();
|
||||||
|
el.seClipboard = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Vue.directive('clipboard', {
|
||||||
|
bind(el, { value }) {
|
||||||
|
createClipboard(el, value);
|
||||||
|
},
|
||||||
|
update(el, { value, oldValue }) {
|
||||||
|
if (value !== oldValue) {
|
||||||
|
destroyClipboard(el);
|
||||||
|
createClipboard(el, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unbind(el) {
|
||||||
|
destroyClipboard(el);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -28,7 +28,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapMutations } from 'vuex';
|
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import animationSvc from '../../services/animationSvc';
|
import animationSvc from '../../services/animationSvc';
|
||||||
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||||
@ -67,6 +67,9 @@ export default {
|
|||||||
...mapMutations('discussion', [
|
...mapMutations('discussion', [
|
||||||
'setCurrentDiscussionId',
|
'setCurrentDiscussionId',
|
||||||
]),
|
]),
|
||||||
|
...mapActions('notification', [
|
||||||
|
'info',
|
||||||
|
]),
|
||||||
goToDiscussion(discussionId = this.currentDiscussionId) {
|
goToDiscussion(discussionId = this.currentDiscussionId) {
|
||||||
this.setCurrentDiscussionId(discussionId);
|
this.setCurrentDiscussionId(discussionId);
|
||||||
const layoutSettings = this.$store.getters['data/layoutSettings'];
|
const layoutSettings = this.$store.getters['data/layoutSettings'];
|
||||||
@ -75,7 +78,7 @@ export default {
|
|||||||
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
||||||
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
||||||
if (!coordinates) {
|
if (!coordinates) {
|
||||||
this.$store.dispatch('notification/info', "Discussion can't be located in the file.");
|
this.info("Discussion can't be located in the file.");
|
||||||
} else {
|
} else {
|
||||||
const scrollerElt = layoutSettings.showEditor
|
const scrollerElt = layoutSettings.showEditor
|
||||||
? editorSvc.editorElt.parentNode
|
? editorSvc.editorElt.parentNode
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
import { mapGetters, mapMutations, mapActions } from 'vuex';
|
import { mapGetters, mapMutations, mapActions } from 'vuex';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import UserImage from '../UserImage';
|
import UserImage from '../UserImage';
|
||||||
import cledit from '../../services/cledit';
|
import cledit from '../../services/editor/cledit';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
@ -12,18 +12,19 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="exportPdf">
|
<menu-entry @click.native="exportPdf">
|
||||||
<icon-download slot="icon"></icon-download>
|
<icon-download slot="icon"></icon-download>
|
||||||
<div><div class="menu-entry__label">sponsor</div> Export as PDF</div>
|
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export as PDF</div>
|
||||||
<span>Produce a PDF from an HTML template.</span>
|
<span>Produce a PDF from an HTML template.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="exportPandoc">
|
<menu-entry @click.native="exportPandoc">
|
||||||
<icon-download slot="icon"></icon-download>
|
<icon-download slot="icon"></icon-download>
|
||||||
<div><div class="menu-entry__label">sponsor</div> Export with Pandoc</div>
|
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export with Pandoc</div>
|
||||||
<span>Convert to PDF, Word, EPUB...</span>
|
<span>Convert to PDF, Word, EPUB...</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import exportSvc from '../../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
},
|
},
|
||||||
|
computed: mapGetters(['isSponsor']),
|
||||||
methods: {
|
methods: {
|
||||||
exportMarkdown() {
|
exportMarkdown() {
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
|
@ -169,7 +169,7 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
// Find the workspace provider
|
// Find the workspace provider
|
||||||
const workspace = this.$store.getters['workspace/currentWorkspace'];
|
const workspace = this.$store.getters['workspace/currentWorkspace'];
|
||||||
this.workspaceProvider = providerRegistry.providers[workspace.providerId];
|
this.workspaceProvider = providerRegistry.providersById[workspace.providerId];
|
||||||
|
|
||||||
// Watch file changes
|
// Watch file changes
|
||||||
this.$watch(
|
this.$watch(
|
||||||
|
@ -29,7 +29,7 @@ import htmlSanitizer from '../../libs/htmlSanitizer';
|
|||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import Provider from '../../services/providers/common/Provider';
|
import Provider from '../../services/providers/common/Provider';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import fileSvc from '../../services/fileSvc';
|
import workspaceSvc from '../../services/workspaceSvc';
|
||||||
|
|
||||||
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ export default {
|
|||||||
async onImportMarkdown(evt) {
|
async onImportMarkdown(evt) {
|
||||||
const file = evt.target.files[0];
|
const file = evt.target.files[0];
|
||||||
const content = await readFile(file);
|
const content = await readFile(file);
|
||||||
const item = await fileSvc.createFile({
|
const item = await workspaceSvc.createFile({
|
||||||
...Provider.parseContent(content),
|
...Provider.parseContent(content),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
});
|
});
|
||||||
@ -67,7 +67,7 @@ export default {
|
|||||||
const content = await readFile(file);
|
const content = await readFile(file);
|
||||||
const sanitizedContent = htmlSanitizer.sanitizeHtml(content)
|
const sanitizedContent = htmlSanitizer.sanitizeHtml(content)
|
||||||
.replace(/ /g, ' '); // Replace non-breaking spaces with classic spaces
|
.replace(/ /g, ' '); // Replace non-breaking spaces with classic spaces
|
||||||
const item = await fileSvc.createFile({
|
const item = await workspaceSvc.createFile({
|
||||||
...Provider.parseContent(turndownService.turndown(sanitizedContent)),
|
...Provider.parseContent(turndownService.turndown(sanitizedContent)),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="menu-info-entries">
|
<div class="side-bar__info">
|
||||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-if="loginToken">
|
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-if="loginToken">
|
||||||
<div class="menu-entry__icon menu-entry__icon--image">
|
<div class="menu-entry__icon menu-entry__icon--image">
|
||||||
<user-image :user-id="userId"></user-image>
|
<user-image :user-id="userId"></user-image>
|
||||||
@ -11,7 +11,15 @@
|
|||||||
<div class="menu-entry__icon menu-entry__icon--image">
|
<div class="menu-entry__icon menu-entry__icon--image">
|
||||||
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
|
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<span><b>{{currentWorkspace.name}}</b> synced.</span>
|
<span v-if="currentWorkspace.providerId === 'googleDriveAppData'">
|
||||||
|
<b>{{currentWorkspace.name}}</b> synced with your Google Drive app data folder.
|
||||||
|
</span>
|
||||||
|
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
|
||||||
|
<b>{{currentWorkspace.name}}</b> synced with a <a :href="currentWorkspaceUrl" target="_blank">Google Drive folder</a>.
|
||||||
|
</span>
|
||||||
|
<span v-else-if="currentWorkspace.providerId === 'couchdbWorkspace'">
|
||||||
|
<b>{{currentWorkspace.name}}</b> synced with a <a :href="currentWorkspaceUrl" target="_blank">CouchDB database</a>.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
|
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
|
||||||
<div class="menu-entry__icon menu-entry__icon--disabled">
|
<div class="menu-entry__icon menu-entry__icon--disabled">
|
||||||
@ -27,7 +35,7 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="setPanel('workspaces')">
|
<menu-entry @click.native="setPanel('workspaces')">
|
||||||
<icon-database slot="icon"></icon-database>
|
<icon-database slot="icon"></icon-database>
|
||||||
<div>Workspaces</div>
|
<div><div class="menu-entry__label menu-entry__label--warning">new</div> Workspaces</div>
|
||||||
<span>Switch to another workspace.</span>
|
<span>Switch to another workspace.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
@ -83,6 +91,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
|
import providerRegistry from '../../services/providers/common/providerRegistry';
|
||||||
import UserImage from '../UserImage';
|
import UserImage from '../UserImage';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
@ -99,6 +108,10 @@ export default {
|
|||||||
'loginToken',
|
'loginToken',
|
||||||
'userId',
|
'userId',
|
||||||
]),
|
]),
|
||||||
|
currentWorkspaceUrl() {
|
||||||
|
const provider = providerRegistry.providersById[this.currentWorkspace.providerId];
|
||||||
|
return provider.getWorkspaceLocationUrl(this.currentWorkspace);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('data', {
|
...mapActions('data', {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="managePublish">
|
<menu-entry @click.native="managePublish">
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
<div>File publication</div>
|
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div>
|
||||||
<span>Manage current file publication locations.</span>
|
<span>Manage current file publication locations.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
@ -113,8 +113,7 @@ import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
|||||||
import publishSvc from '../../services/publishSvc';
|
import publishSvc from '../../services/publishSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
|
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
||||||
.map(sub => tokens[sub])
|
|
||||||
.filter(token => filter(token))
|
.filter(token => filter(token))
|
||||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||||
|
|
||||||
@ -142,6 +141,9 @@ export default {
|
|||||||
...mapGetters('publishLocation', {
|
...mapGetters('publishLocation', {
|
||||||
publishLocations: 'current',
|
publishLocations: 'current',
|
||||||
}),
|
}),
|
||||||
|
locationCount() {
|
||||||
|
return Object.keys(this.publishLocations).length;
|
||||||
|
},
|
||||||
currentFileName() {
|
currentFileName() {
|
||||||
return this.$store.getters['file/current'].name;
|
return this.$store.getters['file/current'].name;
|
||||||
},
|
},
|
||||||
@ -178,8 +180,10 @@ export default {
|
|||||||
publishSvc.requestPublish();
|
publishSvc.requestPublish();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
managePublish() {
|
async managePublish() {
|
||||||
return this.$store.dispatch('modal/open', 'publishManagement');
|
try {
|
||||||
|
await this.$store.dispatch('modal/open', 'publishManagement');
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async addGoogleDriveAccount() {
|
async addGoogleDriveAccount() {
|
||||||
try {
|
try {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||||
<p><b>{{currentFileName}}</b> can not be synchronized as it's a temporary file.</p>
|
<p><b>{{currentFileName}}</b> can not be synced as it's a temporary file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="side-bar__info" v-if="noToken">
|
<div class="side-bar__info" v-if="noToken">
|
||||||
@ -16,7 +16,7 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="manageSync">
|
<menu-entry @click.native="manageSync">
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
<div>File synchronization</div>
|
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div>
|
||||||
<span>Manage current file synchronized locations.</span>
|
<span>Manage current file synchronized locations.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
@ -91,8 +91,7 @@ import githubProvider from '../../services/providers/githubProvider';
|
|||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
|
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
||||||
.map(sub => tokens[sub])
|
|
||||||
.filter(token => filter(token))
|
.filter(token => filter(token))
|
||||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||||
|
|
||||||
@ -116,8 +115,11 @@ export default {
|
|||||||
'isCurrentTemp',
|
'isCurrentTemp',
|
||||||
]),
|
]),
|
||||||
...mapGetters('syncLocation', {
|
...mapGetters('syncLocation', {
|
||||||
syncLocations: 'current',
|
syncLocations: 'currentWithWorkspaceSyncLocation',
|
||||||
}),
|
}),
|
||||||
|
locationCount() {
|
||||||
|
return Object.keys(this.syncLocations).length;
|
||||||
|
},
|
||||||
currentFileName() {
|
currentFileName() {
|
||||||
return this.$store.getters['file/current'].name;
|
return this.$store.getters['file/current'].name;
|
||||||
},
|
},
|
||||||
@ -142,8 +144,10 @@ export default {
|
|||||||
syncSvc.requestSync();
|
syncSvc.requestSync();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
manageSync() {
|
async manageSync() {
|
||||||
return this.$store.dispatch('modal/open', 'syncManagement');
|
try {
|
||||||
|
await this.$store.dispatch('modal/open', 'syncManagement');
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async addGoogleDriveAccount() {
|
async addGoogleDriveAccount() {
|
||||||
try {
|
try {
|
||||||
@ -180,16 +184,12 @@ export default {
|
|||||||
async saveGoogleDrive(token) {
|
async saveGoogleDrive(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'googleDriveSave');
|
await openSyncModal(token, 'googleDriveSave');
|
||||||
} catch (e) {
|
} catch (e) { /* cancel */ }
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async saveDropbox(token) {
|
async saveDropbox(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'dropboxSave');
|
await openSyncModal(token, 'dropboxSave');
|
||||||
} catch (e) {
|
} catch (e) { /* cancel */ }
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async openGithub(token) {
|
async openGithub(token) {
|
||||||
try {
|
try {
|
||||||
@ -201,23 +201,17 @@ export default {
|
|||||||
'queue/enqueue',
|
'queue/enqueue',
|
||||||
() => githubProvider.openFile(token, syncLocation),
|
() => githubProvider.openFile(token, syncLocation),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) { /* cancel */ }
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async saveGithub(token) {
|
async saveGithub(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'githubSave');
|
await openSyncModal(token, 'githubSave');
|
||||||
} catch (e) {
|
} catch (e) { /* cancel */ }
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async saveGist(token) {
|
async saveGist(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'gistSync');
|
await openSyncModal(token, 'gistSync');
|
||||||
} catch (e) {
|
} catch (e) { /* cancel */ }
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="workspace" v-for="(workspace, id) in sanitizedWorkspacesById" :key="id">
|
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
|
||||||
<menu-entry :href="workspace.url" target="_blank">
|
<menu-entry :href="workspace.url" target="_blank">
|
||||||
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
||||||
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div>
|
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div>
|
||||||
@ -9,19 +9,22 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="addCouchdbWorkspace">
|
<menu-entry @click.native="addCouchdbWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
||||||
<span>Add CouchDB workspace</span>
|
<div>CouchDB workspace</div>
|
||||||
|
<span>Add a workspace synced with your CouchDB database.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="addGithubWorkspace">
|
<menu-entry @click.native="addGithubWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
|
||||||
<span>Add GitHub workspace</span>
|
<div>GitHub workspace</div>
|
||||||
|
<span>Add a workspace synced with a GitHub repository.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="addGoogleDriveWorkspace">
|
<menu-entry @click.native="addGoogleDriveWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
||||||
<span>Add Google Drive workspace</span>
|
<div>Google Drive workspace</div>
|
||||||
|
<span>Add a workspace synced with a Google Drive folder.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="manageWorkspaces">
|
<menu-entry @click.native="manageWorkspaces">
|
||||||
<icon-database slot="icon"></icon-database>
|
<icon-database slot="icon"></icon-database>
|
||||||
<span>Manage workspaces</span>
|
<span><div class="menu-entry__label menu-entry__label--count">{{workspaceCount}}</div> Manage workspaces</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -36,12 +39,13 @@ export default {
|
|||||||
MenuEntry,
|
MenuEntry,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('data', [
|
|
||||||
'sanitizedWorkspaceById',
|
|
||||||
]),
|
|
||||||
...mapGetters('workspace', [
|
...mapGetters('workspace', [
|
||||||
|
'workspacesById',
|
||||||
'currentWorkspace',
|
'currentWorkspace',
|
||||||
]),
|
]),
|
||||||
|
workspaceCount() {
|
||||||
|
return Object.keys(this.workspacesById).length;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async addCouchdbWorkspace() {
|
async addCouchdbWorkspace() {
|
||||||
|
@ -24,9 +24,13 @@
|
|||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
opacity: 0.5;
|
opacity: 0.6;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
|
||||||
|
.menu-entry__label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline;
|
display: inline;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -34,12 +38,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-info-entries {
|
|
||||||
padding: 10px;
|
|
||||||
margin: -10px -10px 10px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-entry--info {
|
.menu-entry--info {
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
padding-bottom: 3px;
|
padding-bottom: 3px;
|
||||||
@ -70,10 +68,22 @@
|
|||||||
float: right;
|
float: right;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.05em 0.25em;
|
line-height: 1;
|
||||||
background-color: darken($error-color, 10);
|
padding: 0.15em 0.25em;
|
||||||
|
background-color: #fff;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__label--warning {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
background-color: darken($error-color, 10);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__label--count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-entry__text {
|
.menu-entry__text {
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.resolve()">Close</button>
|
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -78,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -14,15 +14,15 @@
|
|||||||
</form-entry>
|
</form-entry>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button button--copy">Copy to clipboard</button>
|
<button class="button button--copy" v-clipboard="result" @click="info('HTML copied to clipboard!')">Copy</button>
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Clipboard from 'clipboard';
|
import { mapActions } from 'vuex';
|
||||||
import exportSvc from '../../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
import modalTemplate from './common/modalTemplate';
|
import modalTemplate from './common/modalTemplate';
|
||||||
|
|
||||||
@ -48,14 +48,11 @@ export default modalTemplate({
|
|||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
this.clipboard = new Clipboard(this.$el.querySelector('.button--copy'), {
|
|
||||||
text: () => this.result,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
this.clipboard.destroy();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions('notification', [
|
||||||
|
'info',
|
||||||
|
]),
|
||||||
resolve() {
|
resolve() {
|
||||||
const { config } = this;
|
const { config } = this;
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="reject()">Cancel</button>
|
<button class="button" @click="reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
@ -36,9 +36,8 @@ export default modalTemplate({
|
|||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
googlePhotosTokens() {
|
googlePhotosTokens() {
|
||||||
const googleTokens = this.$store.getters['data/googleTokensBySub'];
|
const googleTokensBySub = this.$store.getters['data/googleTokensBySub'];
|
||||||
return Object.entries(googleTokens)
|
return Object.values(googleTokensBySub)
|
||||||
.map(([, token]) => token)
|
|
||||||
.filter(token => token.isPhotos)
|
.filter(token => token.isPhotos)
|
||||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="reject()">Cancel</button>
|
<button class="button" @click="reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations">
|
<modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations">
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-upload></icon-upload>
|
||||||
|
</div>
|
||||||
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||||
<div>
|
<div>
|
||||||
@ -12,21 +15,21 @@
|
|||||||
{{location.description}}
|
{{location.description}}
|
||||||
</div>
|
</div>
|
||||||
<div class="publish-entry__buttons flex flex--row flex--center">
|
<div class="publish-entry__buttons flex flex--row flex--center">
|
||||||
<a class="publish-entry__button button" :href="location.url" target="_blank">
|
<a class="publish-entry__button button" :href="location.url" target="_blank" v-title="'Open location'">
|
||||||
<icon-open-in-new></icon-open-in-new>
|
<icon-open-in-new></icon-open-in-new>
|
||||||
</a>
|
</a>
|
||||||
<button class="publish-entry__button button" @click="remove(location)">
|
<button class="publish-entry__button button" @click="remove(location)" v-title="'Remove location'">
|
||||||
<icon-delete></icon-delete>
|
<icon-delete></icon-delete>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__info" v-if="publishLocations.length">
|
<div class="modal__info" v-if="publishLocations.length">
|
||||||
<b>Note:</b> Removing a synchronized location won't delete any file.
|
<b>Tip:</b> Removing a location won't delete any file.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.resolve()">Close</button>
|
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
<button class="button button--resolve" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,38 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations">
|
<modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations">
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-sync></icon-sync>
|
||||||
|
</div>
|
||||||
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
||||||
<div>
|
<div>
|
||||||
<div class="sync-entry flex flex--row flex--align-center" v-for="location in syncLocations" :key="location.id">
|
<div class="sync-entry flex flex--column" v-for="location in syncLocations" :key="location.id">
|
||||||
<div class="sync-entry__icon flex flex--column flex--center">
|
<div class="sync-entry__header flex flex--row flex--align-center">
|
||||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
<div class="sync-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<div class="sync-entry__description">
|
||||||
|
{{location.description}}
|
||||||
|
</div>
|
||||||
|
<div class="sync-entry__buttons flex flex--row flex--center">
|
||||||
|
<button class="sync-entry__button button" @click="remove(location)" v-title="'Remove location'">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sync-entry__description">
|
<div class="sync-entry__row flex flex--row flex--align-center">
|
||||||
{{location.description}}
|
<div class="sync-entry__url">
|
||||||
</div>
|
{{location.url || 'Workspace location'}}
|
||||||
<div class="sync-entry__buttons flex flex--row flex--center">
|
</div>
|
||||||
<a class="sync-entry__button button" :href="location.url" target="_blank">
|
<div class="sync-entry__buttons flex flex--row flex--center" v-if="location.url">
|
||||||
<icon-open-in-new></icon-open-in-new>
|
<button class="sync-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'Copy URL'">
|
||||||
</a>
|
<icon-content-copy></icon-content-copy>
|
||||||
<button class="sync-entry__button button" @click="remove(location)">
|
</button>
|
||||||
<icon-delete></icon-delete>
|
<a class="sync-entry__button button" v-if="location.url" :href="location.url" target="_blank" v-title="'Open location'">
|
||||||
</button>
|
<icon-open-in-new></icon-open-in-new>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__info" v-if="syncLocations.length">
|
<div class="modal__info" v-if="syncLocations.length">
|
||||||
<b>Note:</b> Removing a synchronized location won't delete any file.
|
<b>Tip:</b> Removing a location won't delete any file.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.resolve()">Close</button>
|
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -44,15 +59,22 @@ export default {
|
|||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
...mapGetters('syncLocation', {
|
...mapGetters('syncLocation', {
|
||||||
syncLocations: 'current',
|
syncLocations: 'currentWithWorkspaceSyncLocation',
|
||||||
}),
|
}),
|
||||||
currentFileName() {
|
currentFileName() {
|
||||||
return this.$store.getters['file/current'].name;
|
return this.$store.getters['file/current'].name;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions('notification', [
|
||||||
|
'info',
|
||||||
|
]),
|
||||||
remove(location) {
|
remove(location) {
|
||||||
this.$store.commit('syncLocation/deleteItem', location.id);
|
if (location.id === 'main') {
|
||||||
|
this.info('This location can not be removed.');
|
||||||
|
} else {
|
||||||
|
this.$store.commit('syncLocation/deleteItem', location.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -66,44 +88,75 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sync-entry {
|
.sync-entry {
|
||||||
padding: 0.5rem 0.25rem;
|
margin: 1.5em 0;
|
||||||
border-bottom: 1px solid $hr-color;
|
height: auto;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
$button-size: 30px;
|
||||||
border-bottom: none;
|
$small-button-size: 22px;
|
||||||
}
|
|
||||||
|
.sync-entry__header {
|
||||||
|
line-height: $button-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-entry__row {
|
||||||
|
margin-top: 1px;
|
||||||
|
padding-top: 1px;
|
||||||
|
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
||||||
|
line-height: $small-button-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-entry__icon {
|
.sync-entry__icon {
|
||||||
height: 30px;
|
height: 22px;
|
||||||
width: 30px;
|
width: 22px;
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-entry__description {
|
.sync-entry__description {
|
||||||
opacity: 0.5;
|
|
||||||
line-height: 1.4;
|
|
||||||
font-size: 0.9em;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-entry__url {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.67em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-entry__buttons {
|
.sync-entry__buttons {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
|
|
||||||
|
.sync-entry__row & {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-entry__button {
|
.sync-entry__button {
|
||||||
width: 38px;
|
width: $button-size;
|
||||||
height: 38px;
|
height: $button-size;
|
||||||
padding: 6px;
|
padding: 4px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|
||||||
|
.sync-entry__row & {
|
||||||
|
width: $small-button-size;
|
||||||
|
height: $small-button-size;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,39 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces">
|
<modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces">
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
<div class="workspace-entry flex flex--row flex--align-center" v-for="(workspace, id) in sanitizedWorkspacesById" :key="id">
|
<div class="modal__image">
|
||||||
<div class="workspace-entry__icon flex flex--column flex--center">
|
<icon-database></icon-database>
|
||||||
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
</div>
|
||||||
</div>
|
<p>The following workspaces are locally available:</p>
|
||||||
<div class="workspace-entry__description flex flex--column">
|
<div class="workspace-entry flex flex--column" v-for="(workspace, id) in workspacesById" :key="id">
|
||||||
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName">
|
<div class="flex flex--column">
|
||||||
<div class="workspace-entry__name" v-else>
|
<div class="workspace-entry__header flex flex--row flex--align-center">
|
||||||
{{workspace.name}}
|
<div class="workspace-entry__icon">
|
||||||
|
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName">
|
||||||
|
<div class="workspace-entry__name" v-else>{{workspace.name}}</div>
|
||||||
|
<div class="workspace-entry__buttons flex flex--row">
|
||||||
|
<button class="workspace-entry__button button" @click="edit(id)" v-title="'Edit name'">
|
||||||
|
<icon-pen></icon-pen>
|
||||||
|
</button>
|
||||||
|
<button class="workspace-entry__button button" @click="remove(id)" v-title="'Remove'">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workspace-entry__url">
|
<div class="workspace-entry__row flex flex--row flex--align-center">
|
||||||
{{workspace.url}}
|
<div class="workspace-entry__url">
|
||||||
|
{{workspace.url}}
|
||||||
|
</div>
|
||||||
|
<div class="workspace-entry__buttons flex flex--row">
|
||||||
|
<button class="workspace-entry__button button" v-clipboard="workspace.url" @click="info('Workspace URL copied to clipboard!')" v-title="'Copy URL'">
|
||||||
|
<icon-content-copy></icon-content-copy>
|
||||||
|
</button>
|
||||||
|
<a class="workspace-entry__button button" :href="workspace.url" target="_blank" v-title="'Open workspace'">
|
||||||
|
<icon-open-in-new></icon-open-in-new>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="workspace-entry__row flex flex--row flex--align-center" v-if="workspace.locationUrl">
|
||||||
|
<div class="workspace-entry__url">
|
||||||
|
{{workspace.locationUrl}}
|
||||||
|
</div>
|
||||||
|
<div class="workspace-entry__buttons flex flex--row">
|
||||||
|
<button class="workspace-entry__button button" v-clipboard="workspace.locationUrl" @click="info('Workspace URL copied to clipboard!')" v-title="'Copy URL'">
|
||||||
|
<icon-content-copy></icon-content-copy>
|
||||||
|
</button>
|
||||||
|
<a class="workspace-entry__button button" :href="workspace.locationUrl" target="_blank" v-title="'Open workspace location'">
|
||||||
|
<icon-open-in-new></icon-open-in-new>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workspace-entry__buttons flex flex--row flex--center">
|
</div>
|
||||||
<button class="workspace-entry__button button" @click="edit(id)">
|
<div class="modal__info">
|
||||||
<icon-pen></icon-pen>
|
<b>ProTip:</b> Workspaces are accessible <b>offline</b> after their first use.
|
||||||
</button>
|
|
||||||
<button class="workspace-entry__button button" v-if="id !== currentWorkspace.id && id !== mainWorkspace.id" @click="remove(id)">
|
|
||||||
<icon-delete></icon-delete>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.resolve()">Close</button>
|
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import localDbSvc from '../../services/localDbSvc';
|
import workspaceSvc from '../../services/workspaceSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -47,40 +77,46 @@ export default {
|
|||||||
...mapGetters('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
...mapGetters('data', [
|
|
||||||
'workspacesById',
|
|
||||||
'sanitizedWorkspacesById',
|
|
||||||
]),
|
|
||||||
...mapGetters('workspace', [
|
...mapGetters('workspace', [
|
||||||
|
'workspacesById',
|
||||||
'mainWorkspace',
|
'mainWorkspace',
|
||||||
'currentWorkspace',
|
'currentWorkspace',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions('notification', [
|
||||||
|
'info',
|
||||||
|
]),
|
||||||
edit(id) {
|
edit(id) {
|
||||||
this.editedId = id;
|
this.editedId = id;
|
||||||
this.editingName = this.workspacesById[id].name;
|
this.editingName = this.workspacesById[id].name;
|
||||||
},
|
},
|
||||||
submitEdit(cancel) {
|
submitEdit(cancel) {
|
||||||
const workspace = this.workspacesById[this.editedId];
|
const workspace = this.workspacesById[this.editedId];
|
||||||
if (workspace && !cancel && this.editingName) {
|
if (workspace) {
|
||||||
this.$store.dispatch('data/patchWorkspacesById', {
|
if (!cancel && this.editingName) {
|
||||||
[this.editedId]: {
|
this.$store.dispatch('workspace/patchWorkspacesById', {
|
||||||
...workspace,
|
[this.editedId]: {
|
||||||
name: this.editingName,
|
...workspace,
|
||||||
},
|
name: this.editingName,
|
||||||
});
|
},
|
||||||
} else {
|
});
|
||||||
this.editingName = workspace.name;
|
} else {
|
||||||
|
this.editingName = workspace.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.editedId = null;
|
this.editedId = null;
|
||||||
},
|
},
|
||||||
async remove(id) {
|
async remove(id) {
|
||||||
try {
|
if (id === this.mainWorkspace.id) {
|
||||||
await this.$store.dispatch('modal/open', 'removeWorkspace');
|
this.info('Your main workspace can not be removed.');
|
||||||
localDbSvc.removeWorkspace(id);
|
} else if (id === this.currentWorkspace.id) {
|
||||||
} catch (e) {
|
this.info('Please close the workspace before removing it.');
|
||||||
// Cancel
|
} else {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('modal/open', 'removeWorkspace');
|
||||||
|
workspaceSvc.removeWorkspace(id);
|
||||||
|
} catch (e) { /* Cancel */ }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -95,58 +131,78 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry {
|
.workspace-entry {
|
||||||
text-align: left;
|
margin: 1.75em 0;
|
||||||
padding-left: 10px;
|
|
||||||
margin: 15px 0;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
text-transform: none;
|
}
|
||||||
|
|
||||||
&:last-child {
|
$button-size: 30px;
|
||||||
border-bottom: none;
|
$small-button-size: 22px;
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
.workspace-entry__header {
|
||||||
text-overflow: ellipsis;
|
line-height: $button-size;
|
||||||
overflow: hidden;
|
|
||||||
|
.text-input {
|
||||||
|
border: 1px solid $link-color;
|
||||||
|
padding: 0 5px;
|
||||||
|
line-height: $button-size;
|
||||||
|
height: $button-size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-entry__row {
|
||||||
|
margin-top: 1px;
|
||||||
|
padding-top: 1px;
|
||||||
|
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
||||||
|
line-height: $small-button-size;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-entry__icon {
|
.workspace-entry__icon {
|
||||||
height: 20px;
|
height: 22px;
|
||||||
width: 20px;
|
width: 22px;
|
||||||
margin-right: 12px;
|
margin-right: 0.75rem;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry__description {
|
|
||||||
width: 100%;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-entry__name {
|
.workspace-entry__name {
|
||||||
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry__url {
|
.workspace-entry__url {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-size: 0.75em;
|
font-size: 0.67em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry__buttons {
|
.workspace-entry__buttons {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
|
|
||||||
|
.workspace-entry__row & {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry__button {
|
.workspace-entry__button {
|
||||||
width: 36px;
|
width: $button-size;
|
||||||
height: 36px;
|
height: $button-size;
|
||||||
padding: 6px;
|
padding: 4px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|
||||||
|
.workspace-entry__row & {
|
||||||
|
width: $small-button-size;
|
||||||
|
height: $small-button-size;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,20 +4,20 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="couchdb"></icon-provider>
|
<icon-provider provider-id="couchdb"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Create a workspace synchronized with a <b>CouchDB</b> database.</p>
|
<p>Create a workspace synced with a <b>CouchDB</b> database.</p>
|
||||||
<form-entry label="Database URL" error="dbUrl">
|
<form-entry label="Database URL" error="dbUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> https://instance.smileupps.com/stackedit-workspace
|
<b>Example:</b> https://instance.smileupps.com/stackedit-workspace
|
||||||
</div>
|
</div>
|
||||||
<div class="form-entry__actions">
|
<div class="form-entry__actions">
|
||||||
<a href="https://community.stackedit.io/t/couchdb-workspace-setup/" target="_blank">More info</a>
|
<a href="https://community.stackedit.io/t/couchdb-workspace-setup/" target="_blank">How to setup?</a>
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
<button class="button button--resolve" @click="config.resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p>
|
<p>Save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synced.</p>
|
||||||
<form-entry label="File path" error="path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="gist"></icon-provider>
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synchronized.</p>
|
<p>Save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synced.</p>
|
||||||
<form-entry label="Filename" error="filename">
|
<form-entry label="Filename" error="filename">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
<button class="button button--resolve" @click="config.resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Open a file from your <b>GitHub</b> repository and keep it synchronized.</p>
|
<p>Open a file from your <b>GitHub</b> repository and keep it synced.</p>
|
||||||
<form-entry label="Repository URL" error="repoUrl">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
@ -34,6 +34,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import githubProvider from '../../../services/providers/githubProvider';
|
import githubProvider from '../../../services/providers/githubProvider';
|
||||||
import modalTemplate from '../common/modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
import utils from '../../../services/utils';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -52,7 +53,7 @@ export default modalTemplate({
|
|||||||
this.setError('path');
|
this.setError('path');
|
||||||
}
|
}
|
||||||
if (this.repoUrl && this.path) {
|
if (this.repoUrl && this.path) {
|
||||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
|
||||||
if (!parsedRepo) {
|
if (!parsedRepo) {
|
||||||
this.setError('repoUrl');
|
this.setError('repoUrl');
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p>
|
<p>Save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synced.</p>
|
||||||
<form-entry label="Repository URL" error="repoUrl">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
@ -35,6 +35,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import githubProvider from '../../../services/providers/githubProvider';
|
import githubProvider from '../../../services/providers/githubProvider';
|
||||||
import modalTemplate from '../common/modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
import utils from '../../../services/utils';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -49,7 +50,7 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
|
||||||
if (!parsedRepo) {
|
if (!parsedRepo) {
|
||||||
this.setError('repoUrl');
|
this.setError('repoUrl');
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Create a workspace synchronized with a <b>GitHub</b> repository folder.</p>
|
<p>Create a workspace synced with a <b>GitHub</b> repository folder.</p>
|
||||||
<form-entry label="Repository URL" error="repoUrl">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -26,13 +26,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import githubProvider from '../../../services/providers/githubProvider';
|
|
||||||
import utils from '../../../services/utils';
|
import utils from '../../../services/utils';
|
||||||
import modalTemplate from '../common/modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
@ -46,14 +45,14 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
|
||||||
if (!parsedRepo) {
|
if (!parsedRepo) {
|
||||||
this.setError('repoUrl');
|
this.setError('repoUrl');
|
||||||
} else {
|
} else {
|
||||||
const path = this.path && this.path.replace(/^\//, '');
|
const path = this.path && this.path.replace(/^\//, '');
|
||||||
const url = utils.addQueryParams('app', {
|
const url = utils.addQueryParams('app', {
|
||||||
|
...parsedRepo,
|
||||||
providerId: 'githubWorkspace',
|
providerId: 'githubWorkspace',
|
||||||
repo: `${parsedRepo.owner}/${parsedRepo.repo}`,
|
|
||||||
branch: this.branch || 'master',
|
branch: this.branch || 'master',
|
||||||
path: path || undefined,
|
path: path || undefined,
|
||||||
}, true);
|
}, true);
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
<button class="button button--resolve" @click="config.resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
@ -73,9 +73,11 @@ export default modalTemplate({
|
|||||||
'modal/hideUntil',
|
'modal/hideUntil',
|
||||||
googleHelper.openPicker(this.config.token, 'folder')
|
googleHelper.openPicker(this.config.token, 'folder')
|
||||||
.then((folders) => {
|
.then((folders) => {
|
||||||
this.$store.dispatch('data/patchLocalSettings', {
|
if (folders[0]) {
|
||||||
googleDriveFolderId: folders[0].id,
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
});
|
googleDriveFolderId: folders[0].id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p>
|
<p>Save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synced.</p>
|
||||||
<form-entry label="Folder ID" info="optional">
|
<form-entry label="Folder ID" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
@ -46,9 +46,11 @@ export default modalTemplate({
|
|||||||
'modal/hideUntil',
|
'modal/hideUntil',
|
||||||
googleHelper.openPicker(this.config.token, 'folder')
|
googleHelper.openPicker(this.config.token, 'folder')
|
||||||
.then((folders) => {
|
.then((folders) => {
|
||||||
this.$store.dispatch('data/patchLocalSettings', {
|
if (folders[0]) {
|
||||||
googleDriveFolderId: folders[0].id,
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
});
|
googleDriveFolderId: folders[0].id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>Create a workspace synchronized with a <b>Google Drive</b> folder.</p>
|
<p>Create a workspace synced with a <b>Google Drive</b> folder.</p>
|
||||||
<form-entry label="Folder ID" info="optional">
|
<form-entry label="Folder ID" info="optional">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
@ -37,9 +37,11 @@ export default modalTemplate({
|
|||||||
'modal/hideUntil',
|
'modal/hideUntil',
|
||||||
googleHelper.openPicker(this.config.token, 'folder')
|
googleHelper.openPicker(this.config.token, 'folder')
|
||||||
.then((folders) => {
|
.then((folders) => {
|
||||||
this.$store.dispatch('data/patchLocalSettings', {
|
if (folders[0]) {
|
||||||
googleDriveWorkspaceFolderId: folders[0].id,
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
});
|
googleDriveWorkspaceFolderId: folders[0].id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="reject()">Cancel</button>
|
<button class="button" @click="reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
main: {
|
main: {
|
||||||
name: 'Main workspace',
|
name: 'Main workspace',
|
||||||
// The rest will be filled by the data/sanitizedWorkspacesById getter
|
// The rest will be filled by the workspace/workspacesById getter
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -4,14 +4,6 @@ If your workspace is not synced, your files are only stored inside your browser
|
|||||||
|
|
||||||
We recommend syncing your workspace to make sure files won't be lost in case your browser data is cleared.
|
We recommend syncing your workspace to make sure files won't be lost in case your browser data is cleared.
|
||||||
|
|
||||||
**Where is my data stored once I sync my workspace?**
|
|
||||||
|
|
||||||
If you sign in with Google, your main workspace will be stored in Google Drive, in your [app data folder](https://developers.google.com/drive/v3/web/appdata).
|
|
||||||
|
|
||||||
If you open a Google Drive workspace, the files in the workspace will be stored inside a Google Drive folder which you can share with other users.
|
|
||||||
|
|
||||||
If you open a CouchDB workspace, the files in the workspace will be stored in the CouchDB database which can be hosted on premises for privacy concerns.
|
|
||||||
|
|
||||||
**Can StackEdit access my data without telling me?**
|
**Can StackEdit access my data without telling me?**
|
||||||
|
|
||||||
StackEdit is a frontend application. The access tokens issued by Google, Dropbox, GitHub... are stored in your browser and are not sent to any backend or 3^rd^ parties so your data won't be accessed by anyone.
|
StackEdit is a 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.
|
||||||
|
5
src/icons/ContentCopy.vue
Normal file
5
src/icons/ContentCopy.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M 19,21L 8,21L 8,7L 19,7M 19,5L 8,5C 6.9,5 6,5.9 6,7L 6,21C 6,22.1 6.9,23 8,23L 19,23C 20.1,23 21,22.1 21,21L 21,7C 21,5.9 20.1,5 19,5 Z M 16,1L 4,1C 2.9,1 2,1.9 2,3L 2,17L 4,17L 4,3L 16,3L 16,1 Z " />
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -50,6 +50,7 @@ import Database from './Database';
|
|||||||
import Magnify from './Magnify';
|
import Magnify from './Magnify';
|
||||||
import FormatListChecks from './FormatListChecks';
|
import FormatListChecks from './FormatListChecks';
|
||||||
import CloseCircle from './CloseCircle';
|
import CloseCircle from './CloseCircle';
|
||||||
|
import ContentCopy from './ContentCopy';
|
||||||
|
|
||||||
Vue.component('iconProvider', Provider);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
@ -102,3 +103,4 @@ Vue.component('iconDatabase', Database);
|
|||||||
Vue.component('iconMagnify', Magnify);
|
Vue.component('iconMagnify', Magnify);
|
||||||
Vue.component('iconFormatListChecks', FormatListChecks);
|
Vue.component('iconFormatListChecks', FormatListChecks);
|
||||||
Vue.component('iconCloseCircle', CloseCircle);
|
Vue.component('iconCloseCircle', CloseCircle);
|
||||||
|
Vue.component('iconContentCopy', ContentCopy);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import fileSvc from './fileSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -51,7 +51,7 @@ export default {
|
|||||||
|
|
||||||
await utils.awaitSequence(
|
await utils.awaitSequence(
|
||||||
Object.keys(folderNameMap),
|
Object.keys(folderNameMap),
|
||||||
async externalId => fileSvc.setOrPatchItem({
|
async externalId => workspaceSvc.setOrPatchItem({
|
||||||
id: folderIdMap[externalId],
|
id: folderIdMap[externalId],
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
name: folderNameMap[externalId],
|
name: folderNameMap[externalId],
|
||||||
@ -61,7 +61,7 @@ export default {
|
|||||||
|
|
||||||
await utils.awaitSequence(
|
await utils.awaitSequence(
|
||||||
Object.keys(fileNameMap),
|
Object.keys(fileNameMap),
|
||||||
async externalId => fileSvc.createFile({
|
async externalId => workspaceSvc.createFile({
|
||||||
name: fileNameMap[externalId],
|
name: fileNameMap[externalId],
|
||||||
parentId: folderIdMap[parentIdMap[externalId]],
|
parentId: folderIdMap[parentIdMap[externalId]],
|
||||||
text: textMap[externalId],
|
text: textMap[externalId],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import DiffMatchPatch from 'diff-match-patch';
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
import TurndownService from 'turndown/lib/turndown.browser.umd';
|
import TurndownService from 'turndown/lib/turndown.browser.umd';
|
||||||
import htmlSanitizer from '../../libs/htmlSanitizer';
|
import htmlSanitizer from '../../../libs/htmlSanitizer';
|
||||||
import store from '../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
function cledit(contentElt, scrollEltOpt, isMarkdown = false) {
|
function cledit(contentElt, scrollEltOpt, isMarkdown = false) {
|
||||||
const scrollElt = scrollEltOpt || contentElt;
|
const scrollElt = scrollEltOpt || contentElt;
|
@ -29,10 +29,14 @@ function SelectionMgr(editor) {
|
|||||||
|
|
||||||
this.createRange = (start, end) => {
|
this.createRange = (start, end) => {
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
const startContainer = Number.isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start);
|
const startContainer = typeof start === 'number'
|
||||||
|
? this.findContainer(start < 0 ? 0 : start)
|
||||||
|
: start;
|
||||||
let endContainer = startContainer;
|
let endContainer = startContainer;
|
||||||
if (start !== end) {
|
if (start !== end) {
|
||||||
endContainer = Number.isNaN(end) ? end : this.findContainer(end < 0 ? 0 : end);
|
endContainer = typeof end === 'number'
|
||||||
|
? this.findContainer(end < 0 ? 0 : end)
|
||||||
|
: end;
|
||||||
}
|
}
|
||||||
range.setStart(startContainer.container, startContainer.offsetInContainer);
|
range.setStart(startContainer.container, startContainer.offsetInContainer);
|
||||||
range.setEnd(endContainer.container, endContainer.offsetInContainer);
|
range.setEnd(endContainer.container, endContainer.offsetInContainer);
|
@ -1,4 +1,4 @@
|
|||||||
import '../../libs/clunderscore';
|
import '../../../libs/clunderscore';
|
||||||
import cledit from './cleditCore';
|
import cledit from './cleditCore';
|
||||||
import './cleditHighlighter';
|
import './cleditHighlighter';
|
||||||
import './cleditKeystroke';
|
import './cleditKeystroke';
|
@ -1,10 +1,10 @@
|
|||||||
import DiffMatchPatch from 'diff-match-patch';
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
import cledit from './cledit';
|
import cledit from './cledit';
|
||||||
import utils from './utils';
|
import utils from '../utils';
|
||||||
import diffUtils from './diffUtils';
|
import diffUtils from '../diffUtils';
|
||||||
import store from '../store';
|
import store from '../../store';
|
||||||
import EditorClassApplier from '../components/common/EditorClassApplier';
|
import EditorClassApplier from '../../components/common/EditorClassApplier';
|
||||||
import PreviewClassApplier from '../components/common/PreviewClassApplier';
|
import PreviewClassApplier from '../../components/common/PreviewClassApplier';
|
||||||
|
|
||||||
let clEditor;
|
let clEditor;
|
||||||
// let discussionIds = {};
|
// let discussionIds = {};
|
@ -1,7 +1,7 @@
|
|||||||
import DiffMatchPatch from 'diff-match-patch';
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
import cledit from './cledit';
|
import cledit from './cledit';
|
||||||
import animationSvc from './animationSvc';
|
import animationSvc from '../animationSvc';
|
||||||
import store from '../store';
|
import store from '../../store';
|
||||||
|
|
||||||
const diffMatchPatch = new DiffMatchPatch();
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
|
114
src/services/editor/sectionUtils.js
Normal file
114
src/services/editor/sectionUtils.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
class SectionDimension {
|
||||||
|
constructor(startOffset, endOffset) {
|
||||||
|
this.startOffset = startOffset;
|
||||||
|
this.endOffset = endOffset;
|
||||||
|
this.height = endOffset - startOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimensionNormalizer = dimensionName => (editorSvc) => {
|
||||||
|
const dimensionList = editorSvc.previewCtx.sectionDescList
|
||||||
|
.map(sectionDesc => sectionDesc[dimensionName]);
|
||||||
|
let dimension;
|
||||||
|
let i;
|
||||||
|
let j;
|
||||||
|
for (i = 0; i < dimensionList.length; i += 1) {
|
||||||
|
dimension = dimensionList[i];
|
||||||
|
if (dimension.height) {
|
||||||
|
for (j = i + 1; j < dimensionList.length && dimensionList[j].height === 0; j += 1) {
|
||||||
|
// Loop
|
||||||
|
}
|
||||||
|
const normalizeFactor = j - i;
|
||||||
|
if (normalizeFactor !== 1) {
|
||||||
|
const normalizedHeight = dimension.height / normalizeFactor;
|
||||||
|
dimension.height = normalizedHeight;
|
||||||
|
dimension.endOffset = dimension.startOffset + dimension.height;
|
||||||
|
for (j = i + 1; j < i + normalizeFactor; j += 1) {
|
||||||
|
const startOffset = dimension.endOffset;
|
||||||
|
dimension = dimensionList[j];
|
||||||
|
dimension.startOffset = startOffset;
|
||||||
|
dimension.height = normalizedHeight;
|
||||||
|
dimension.endOffset = dimension.startOffset + dimension.height;
|
||||||
|
}
|
||||||
|
i = j - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeEditorDimensions = dimensionNormalizer('editorDimension');
|
||||||
|
const normalizePreviewDimensions = dimensionNormalizer('previewDimension');
|
||||||
|
const normalizeTocDimensions = dimensionNormalizer('tocDimension');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
measureSectionDimensions(editorSvc) {
|
||||||
|
let editorSectionOffset = 0;
|
||||||
|
let previewSectionOffset = 0;
|
||||||
|
let tocSectionOffset = 0;
|
||||||
|
let sectionDesc = editorSvc.previewCtx.sectionDescList[0];
|
||||||
|
let nextSectionDesc;
|
||||||
|
let i = 1;
|
||||||
|
for (; i < editorSvc.previewCtx.sectionDescList.length; i += 1) {
|
||||||
|
nextSectionDesc = editorSvc.previewCtx.sectionDescList[i];
|
||||||
|
|
||||||
|
// Measure editor section
|
||||||
|
let newEditorSectionOffset = nextSectionDesc.editorElt
|
||||||
|
? nextSectionDesc.editorElt.offsetTop
|
||||||
|
: editorSectionOffset;
|
||||||
|
newEditorSectionOffset = newEditorSectionOffset > editorSectionOffset
|
||||||
|
? newEditorSectionOffset
|
||||||
|
: editorSectionOffset;
|
||||||
|
sectionDesc.editorDimension = new SectionDimension(
|
||||||
|
editorSectionOffset,
|
||||||
|
newEditorSectionOffset,
|
||||||
|
);
|
||||||
|
editorSectionOffset = newEditorSectionOffset;
|
||||||
|
|
||||||
|
// Measure preview section
|
||||||
|
let newPreviewSectionOffset = nextSectionDesc.previewElt
|
||||||
|
? nextSectionDesc.previewElt.offsetTop
|
||||||
|
: previewSectionOffset;
|
||||||
|
newPreviewSectionOffset = newPreviewSectionOffset > previewSectionOffset
|
||||||
|
? newPreviewSectionOffset
|
||||||
|
: previewSectionOffset;
|
||||||
|
sectionDesc.previewDimension = new SectionDimension(
|
||||||
|
previewSectionOffset,
|
||||||
|
newPreviewSectionOffset,
|
||||||
|
);
|
||||||
|
previewSectionOffset = newPreviewSectionOffset;
|
||||||
|
|
||||||
|
// Measure TOC section
|
||||||
|
let newTocSectionOffset = nextSectionDesc.tocElt
|
||||||
|
? nextSectionDesc.tocElt.offsetTop + (nextSectionDesc.tocElt.offsetHeight / 2)
|
||||||
|
: tocSectionOffset;
|
||||||
|
newTocSectionOffset = newTocSectionOffset > tocSectionOffset
|
||||||
|
? newTocSectionOffset
|
||||||
|
: tocSectionOffset;
|
||||||
|
sectionDesc.tocDimension = new SectionDimension(tocSectionOffset, newTocSectionOffset);
|
||||||
|
tocSectionOffset = newTocSectionOffset;
|
||||||
|
|
||||||
|
sectionDesc = nextSectionDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last section
|
||||||
|
sectionDesc = editorSvc.previewCtx.sectionDescList[i - 1];
|
||||||
|
if (sectionDesc) {
|
||||||
|
sectionDesc.editorDimension = new SectionDimension(
|
||||||
|
editorSectionOffset,
|
||||||
|
editorSvc.editorElt.scrollHeight,
|
||||||
|
);
|
||||||
|
sectionDesc.previewDimension = new SectionDimension(
|
||||||
|
previewSectionOffset,
|
||||||
|
editorSvc.previewElt.scrollHeight,
|
||||||
|
);
|
||||||
|
sectionDesc.tocDimension = new SectionDimension(
|
||||||
|
tocSectionOffset,
|
||||||
|
editorSvc.tocElt.scrollHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeEditorDimensions(editorSvc);
|
||||||
|
normalizePreviewDimensions(editorSvc);
|
||||||
|
normalizeTocDimensions(editorSvc);
|
||||||
|
},
|
||||||
|
};
|
@ -2,15 +2,15 @@ import Vue from 'vue';
|
|||||||
import DiffMatchPatch from 'diff-match-patch';
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import markdownItPandocRenderer from 'markdown-it-pandoc-renderer';
|
import markdownItPandocRenderer from 'markdown-it-pandoc-renderer';
|
||||||
import cledit from './cledit';
|
import cledit from './editor/cledit';
|
||||||
import pagedown from '../libs/pagedown';
|
import pagedown from '../libs/pagedown';
|
||||||
import htmlSanitizer from '../libs/htmlSanitizer';
|
import htmlSanitizer from '../libs/htmlSanitizer';
|
||||||
import markdownConversionSvc from './markdownConversionSvc';
|
import markdownConversionSvc from './markdownConversionSvc';
|
||||||
import markdownGrammarSvc from './markdownGrammarSvc';
|
import markdownGrammarSvc from './markdownGrammarSvc';
|
||||||
import sectionUtils from './sectionUtils';
|
import sectionUtils from './editor/sectionUtils';
|
||||||
import extensionSvc from './extensionSvc';
|
import extensionSvc from './extensionSvc';
|
||||||
import editorSvcDiscussions from './editorSvcDiscussions';
|
import editorSvcDiscussions from './editor/editorSvcDiscussions';
|
||||||
import editorSvcUtils from './editorSvcUtils';
|
import editorSvcUtils from './editor/editorSvcUtils';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import store from '../store';
|
import store from '../store';
|
||||||
import fileSvc from './fileSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
newItem(isFolder = false) {
|
newItem(isFolder = false) {
|
||||||
@ -59,7 +59,7 @@ export default {
|
|||||||
parentId: 'trash',
|
parentId: 'trash',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
fileSvc.deleteFile(id);
|
workspaceSvc.deleteFile(id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import FileSaver from 'file-saver';
|
|||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import welcomeFile from '../data/welcomeFile.md';
|
import welcomeFile from '../data/welcomeFile.md';
|
||||||
import fileSvc from './fileSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
|
|
||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
@ -12,21 +12,13 @@ const resetApp = utils.queryParams.reset;
|
|||||||
const deleteMarkerMaxAge = 1000;
|
const deleteMarkerMaxAge = 1000;
|
||||||
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||||
|
|
||||||
const getDbName = (workspaceId) => {
|
|
||||||
let dbName = 'stackedit-db';
|
|
||||||
if (workspaceId !== 'main') {
|
|
||||||
dbName += `-${workspaceId}`;
|
|
||||||
}
|
|
||||||
return dbName;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.getTxCbs = [];
|
this.getTxCbs = [];
|
||||||
|
|
||||||
// Make the DB name
|
// Make the DB name
|
||||||
const workspaceId = store.getters['workspace/currentWorkspace'].id;
|
const workspaceId = store.getters['workspace/currentWorkspace'].id;
|
||||||
this.dbName = getDbName(workspaceId);
|
this.dbName = utils.getDbName(workspaceId);
|
||||||
|
|
||||||
// Init connection
|
// Init connection
|
||||||
const request = indexedDB.open(this.dbName, dbVersion);
|
const request = indexedDB.open(this.dbName, dbVersion);
|
||||||
@ -142,9 +134,12 @@ const localDbSvc = {
|
|||||||
this.connection.createTx((tx) => {
|
this.connection.createTx((tx) => {
|
||||||
// Look for DB changes and apply them to the store
|
// Look for DB changes and apply them to the store
|
||||||
this.readAll(tx, (storeItemMap) => {
|
this.readAll(tx, (storeItemMap) => {
|
||||||
|
// Sanitize the workspace
|
||||||
|
workspaceSvc.ensureUniquePaths();
|
||||||
|
workspaceSvc.ensureUniqueLocations();
|
||||||
// Persist all the store changes into the DB
|
// Persist all the store changes into the DB
|
||||||
this.writeAll(storeItemMap, tx);
|
this.writeAll(storeItemMap, tx);
|
||||||
// Sync localStorage
|
// Sync the localStorage
|
||||||
this.syncLocalStorage();
|
this.syncLocalStorage();
|
||||||
// Done
|
// Done
|
||||||
resolve();
|
resolve();
|
||||||
@ -177,19 +172,21 @@ const localDbSvc = {
|
|||||||
// Collect change
|
// Collect change
|
||||||
changes.push(item);
|
changes.push(item);
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
} else {
|
return;
|
||||||
const storeItemMap = { ...store.getters.allItemsById };
|
|
||||||
changes.forEach((item) => {
|
|
||||||
this.readDbItem(item, storeItemMap);
|
|
||||||
// If item is an old delete marker, remove it from the DB
|
|
||||||
if (!item.hash && lastTx - item.tx > deleteMarkerMaxAge) {
|
|
||||||
dbStore.delete(item.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fileSvc.ensureUniquePaths();
|
|
||||||
this.lastTx = lastTx;
|
|
||||||
cb(storeItemMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the collected changes
|
||||||
|
const storeItemMap = { ...store.getters.allItemsById };
|
||||||
|
changes.forEach((item) => {
|
||||||
|
this.readDbItem(item, storeItemMap);
|
||||||
|
// If item is an old delete marker, remove it from the DB
|
||||||
|
if (!item.hash && lastTx - item.tx > deleteMarkerMaxAge) {
|
||||||
|
dbStore.delete(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastTx = lastTx;
|
||||||
|
cb(storeItemMap);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -322,40 +319,20 @@ const localDbSvc = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop the database and clean the localStorage for the specified workspaceId.
|
|
||||||
*/
|
|
||||||
async removeWorkspace(id) {
|
|
||||||
const workspacesById = {
|
|
||||||
...store.getters['data/workspacesById'],
|
|
||||||
};
|
|
||||||
delete workspacesById[id];
|
|
||||||
store.dispatch('data/setWorkspacesById', workspacesById);
|
|
||||||
this.syncLocalStorage();
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const dbName = getDbName(id);
|
|
||||||
const request = indexedDB.deleteDatabase(dbName);
|
|
||||||
request.onerror = reject;
|
|
||||||
request.onsuccess = resolve;
|
|
||||||
});
|
|
||||||
localStorage.removeItem(`${id}/lastSyncActivity`);
|
|
||||||
localStorage.removeItem(`${id}/lastWindowFocus`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the connection and start syncing.
|
* Create the connection and start syncing.
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
// Reset the app if reset flag was passed
|
// Reset the app if reset flag was passed
|
||||||
if (resetApp) {
|
if (resetApp) {
|
||||||
await Promise.all(Object.keys(store.getters['data/workspacesById'])
|
await Promise.all(Object.keys(store.getters['workspace/workspacesById'])
|
||||||
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)));
|
.map(workspaceId => workspaceSvc.removeWorkspace(workspaceId)));
|
||||||
utils.localStorageDataIds.forEach((id) => {
|
utils.localStorageDataIds.forEach((id) => {
|
||||||
// Clean data stored in localStorage
|
// Clean data stored in localStorage
|
||||||
localStorage.removeItem(`data/${id}`);
|
localStorage.removeItem(`data/${id}`);
|
||||||
});
|
});
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
throw new Error('reload');
|
throw new Error('RELOAD');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the connection
|
// Create the connection
|
||||||
@ -374,6 +351,13 @@ const localDbSvc = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch workspace deletions and persist them as soon as possible
|
||||||
|
// to make the changes available to reloading workspace tabs.
|
||||||
|
store.watch(
|
||||||
|
() => store.getters['data/workspaces'],
|
||||||
|
() => this.syncLocalStorage(),
|
||||||
|
);
|
||||||
|
|
||||||
// Save welcome file content hash if not done already
|
// Save welcome file content hash if not done already
|
||||||
const hash = utils.hash(welcomeFile);
|
const hash = utils.hash(welcomeFile);
|
||||||
const { welcomeFileHashes } = store.getters['data/localSettings'];
|
const { welcomeFileHashes } = store.getters['data/localSettings'];
|
||||||
@ -393,7 +377,7 @@ const localDbSvc = {
|
|||||||
// Clean files
|
// Clean files
|
||||||
store.getters['file/items']
|
store.getters['file/items']
|
||||||
.filter(file => file.parentId === 'trash') // If file is in the trash
|
.filter(file => file.parentId === 'trash') // If file is in the trash
|
||||||
.forEach(file => fileSvc.deleteFile(file.id));
|
.forEach(file => workspaceSvc.deleteFile(file.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable sponsorship
|
// Enable sponsorship
|
||||||
@ -429,7 +413,7 @@ const localDbSvc = {
|
|||||||
store.commit('file/setCurrentId', recentFile.id);
|
store.commit('file/setCurrentId', recentFile.id);
|
||||||
} else {
|
} else {
|
||||||
// If still no ID, create a new file
|
// If still no ID, create a new file
|
||||||
const newFile = await fileSvc.createFile({
|
const newFile = await workspaceSvc.createFile({
|
||||||
name: 'Welcome file',
|
name: 'Welcome file',
|
||||||
text: welcomeFile,
|
text: welcomeFile,
|
||||||
}, true);
|
}, true);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import cledit from '../cledit';
|
import cledit from '../editor/cledit';
|
||||||
import editorSvc from '../editorSvc';
|
import editorSvc from '../editorSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
|
@ -4,16 +4,15 @@ import Provider from './common/Provider';
|
|||||||
|
|
||||||
export default new Provider({
|
export default new Provider({
|
||||||
id: 'bloggerPage',
|
id: 'bloggerPage',
|
||||||
getToken(location) {
|
getToken({ sub }) {
|
||||||
const token = store.getters['data/googleTokensBySub'][location.sub];
|
const token = store.getters['data/googleTokensBySub'][sub];
|
||||||
return token && token.isBlogger ? token : null;
|
return token && token.isBlogger ? token : null;
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl({ blogId, pageId }) {
|
||||||
return `https://www.blogger.com/blogger.g?blogID=${location.blogId}#editor/target=page;pageID=${location.pageId}`;
|
return `https://www.blogger.com/blogger.g?blogID=${blogId}#editor/target=page;pageID=${pageId}`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ pageId }) {
|
||||||
const token = this.getToken(location);
|
return pageId;
|
||||||
return `${location.pageId} — ${location.blogUrl} — ${token.name}`;
|
|
||||||
},
|
},
|
||||||
async publish(token, html, metadata, publishLocation) {
|
async publish(token, html, metadata, publishLocation) {
|
||||||
const page = await googleHelper.uploadBlogger({
|
const page = await googleHelper.uploadBlogger({
|
||||||
|
@ -4,16 +4,15 @@ import Provider from './common/Provider';
|
|||||||
|
|
||||||
export default new Provider({
|
export default new Provider({
|
||||||
id: 'blogger',
|
id: 'blogger',
|
||||||
getToken(location) {
|
getToken({ sub }) {
|
||||||
const token = store.getters['data/googleTokensBySub'][location.sub];
|
const token = store.getters['data/googleTokensBySub'][sub];
|
||||||
return token && token.isBlogger ? token : null;
|
return token && token.isBlogger ? token : null;
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl({ blogId, postId }) {
|
||||||
return `https://www.blogger.com/blogger.g?blogID=${location.blogId}#editor/target=post;postID=${location.postId}`;
|
return `https://www.blogger.com/blogger.g?blogID=${blogId}#editor/target=post;postID=${postId}`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ postId }) {
|
||||||
const token = this.getToken(location);
|
return postId;
|
||||||
return `${location.postId} — ${location.blogUrl} — ${token.name}`;
|
|
||||||
},
|
},
|
||||||
async publish(token, html, metadata, publishLocation) {
|
async publish(token, html, metadata, publishLocation) {
|
||||||
const post = await googleHelper.uploadBlogger({
|
const post = await googleHelper.uploadBlogger({
|
||||||
|
@ -2,7 +2,7 @@ import providerRegistry from './providerRegistry';
|
|||||||
import emptyContent from '../../../data/emptyContent';
|
import emptyContent from '../../../data/emptyContent';
|
||||||
import utils from '../../utils';
|
import utils from '../../utils';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
import fileSvc from '../../fileSvc';
|
import workspaceSvc from '../../workspaceSvc';
|
||||||
|
|
||||||
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
|
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
|
||||||
|
|
||||||
@ -81,8 +81,8 @@ export default class Provider {
|
|||||||
/**
|
/**
|
||||||
* Find and open a file with location that meets the criteria
|
* Find and open a file with location that meets the criteria
|
||||||
*/
|
*/
|
||||||
static openFileWithLocation(allLocations, criteria) {
|
static openFileWithLocation(criteria) {
|
||||||
const location = utils.search(allLocations, criteria);
|
const location = utils.search(store.getters['syncLocation/items'], criteria);
|
||||||
if (location) {
|
if (location) {
|
||||||
// Found one, open it if it exists
|
// Found one, open it if it exists
|
||||||
const item = store.state.file.itemsById[location.fileId];
|
const item = store.state.file.itemsById[location.fileId];
|
||||||
@ -90,7 +90,7 @@ export default class Provider {
|
|||||||
store.commit('file/setCurrentId', item.id);
|
store.commit('file/setCurrentId', item.id);
|
||||||
// If file is in the trash, restore it
|
// If file is in the trash, restore it
|
||||||
if (item.parentId === 'trash') {
|
if (item.parentId === 'trash') {
|
||||||
fileSvc.setOrPatchItem({
|
workspaceSvc.setOrPatchItem({
|
||||||
...item,
|
...item,
|
||||||
parentId: null,
|
parentId: null,
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
providers: {},
|
providersById: {},
|
||||||
register(provider) {
|
register(provider) {
|
||||||
this.providers[provider.id] = provider;
|
this.providersById[provider.id] = provider;
|
||||||
return provider;
|
return provider;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -10,56 +10,54 @@ export default new Provider({
|
|||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['workspace/syncToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
async initWorkspace() {
|
getWorkspaceParams({ dbUrl }) {
|
||||||
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
|
return {
|
||||||
const workspaceParams = {
|
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
dbUrl,
|
dbUrl,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
getWorkspaceLocationUrl({ dbUrl }) {
|
||||||
|
return dbUrl;
|
||||||
|
},
|
||||||
|
getSyncDataUrl(fileSyncData, { id }) {
|
||||||
|
const { dbUrl } = this.getToken();
|
||||||
|
return `${dbUrl}/${id}/data`;
|
||||||
|
},
|
||||||
|
getSyncDataDescription(fileSyncData, { id }) {
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
async initWorkspace() {
|
||||||
|
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
|
||||||
|
const workspaceParams = this.getWorkspaceParams({ dbUrl });
|
||||||
const workspaceId = utils.makeWorkspaceId(workspaceParams);
|
const workspaceId = utils.makeWorkspaceId(workspaceParams);
|
||||||
const getToken = () => store.getters['data/couchdbTokensBySub'][workspaceId];
|
|
||||||
const getWorkspace = () => store.getters['data/sanitizedWorkspacesById'][workspaceId];
|
|
||||||
|
|
||||||
if (!getToken()) {
|
// Create the token if it doesn't exist
|
||||||
// Create token
|
if (!store.getters['data/couchdbTokensBySub'][workspaceId]) {
|
||||||
store.dispatch('data/addCouchdbToken', {
|
store.dispatch('data/addCouchdbToken', {
|
||||||
sub: workspaceId,
|
sub: workspaceId,
|
||||||
dbUrl,
|
dbUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the workspace
|
// Create the workspace if it doesn't exist
|
||||||
let workspace = getWorkspace();
|
if (!store.getters['workspace/workspacesById'][workspaceId]) {
|
||||||
if (!workspace) {
|
|
||||||
// Make sure the database exists and retrieve its name
|
|
||||||
let db;
|
|
||||||
try {
|
try {
|
||||||
db = await couchdbHelper.getDb(getToken());
|
// Make sure the database exists and retrieve its name
|
||||||
|
const db = await couchdbHelper.getDb(store.getters['data/couchdbTokensBySub'][workspaceId]);
|
||||||
|
store.dispatch('workspace/patchWorkspacesById', {
|
||||||
|
[workspaceId]: {
|
||||||
|
id: workspaceId,
|
||||||
|
name: db.db_name,
|
||||||
|
providerId: this.id,
|
||||||
|
dbUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`${dbUrl} is not accessible. Make sure you have the proper permissions.`);
|
throw new Error(`${dbUrl} is not accessible. Make sure you have the proper permissions.`);
|
||||||
}
|
}
|
||||||
store.dispatch('data/patchWorkspacesById', {
|
|
||||||
[workspaceId]: {
|
|
||||||
id: workspaceId,
|
|
||||||
name: db.db_name,
|
|
||||||
providerId: this.id,
|
|
||||||
dbUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
workspace = getWorkspace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix the URL hash
|
return store.getters['workspace/workspacesById'][workspaceId];
|
||||||
utils.setQueryParams(workspaceParams);
|
|
||||||
if (workspace.url !== window.location.href) {
|
|
||||||
store.dispatch('data/patchWorkspacesById', {
|
|
||||||
[workspace.id]: {
|
|
||||||
...workspace,
|
|
||||||
url: window.location.href,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return getWorkspace();
|
|
||||||
},
|
},
|
||||||
async getChanges() {
|
async getChanges() {
|
||||||
const syncToken = store.getters['workspace/syncToken'];
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
|
@ -2,7 +2,7 @@ import store from '../../store';
|
|||||||
import dropboxHelper from './helpers/dropboxHelper';
|
import dropboxHelper from './helpers/dropboxHelper';
|
||||||
import Provider from './common/Provider';
|
import Provider from './common/Provider';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import fileSvc from '../fileSvc';
|
import workspaceSvc from '../workspaceSvc';
|
||||||
|
|
||||||
const makePathAbsolute = (token, path) => {
|
const makePathAbsolute = (token, path) => {
|
||||||
if (!token.fullAccess) {
|
if (!token.fullAccess) {
|
||||||
@ -19,17 +19,16 @@ const makePathRelative = (token, path) => {
|
|||||||
|
|
||||||
export default new Provider({
|
export default new Provider({
|
||||||
id: 'dropbox',
|
id: 'dropbox',
|
||||||
getToken(location) {
|
getToken({ sub }) {
|
||||||
return store.getters['data/dropboxTokensBySub'][location.sub];
|
return store.getters['data/dropboxTokensBySub'][sub];
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl({ path }) {
|
||||||
const pathComponents = location.path.split('/').map(encodeURIComponent);
|
const pathComponents = path.split('/').map(encodeURIComponent);
|
||||||
const filename = pathComponents.pop();
|
const filename = pathComponents.pop();
|
||||||
return `https://www.dropbox.com/home${pathComponents.join('/')}?preview=${filename}`;
|
return `https://www.dropbox.com/home${pathComponents.join('/')}?preview=${filename}`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ path }) {
|
||||||
const token = this.getToken(location);
|
return path;
|
||||||
return `${location.path} — ${location.dropboxFileId} — ${token.name}`;
|
|
||||||
},
|
},
|
||||||
checkPath(path) {
|
checkPath(path) {
|
||||||
return path && path.match(/^\/[^\\<>:"|?*]+$/);
|
return path && path.match(/^\/[^\\<>:"|?*]+$/);
|
||||||
@ -71,7 +70,7 @@ export default new Provider({
|
|||||||
async openFiles(token, paths) {
|
async openFiles(token, paths) {
|
||||||
await utils.awaitSequence(paths, async (path) => {
|
await utils.awaitSequence(paths, async (path) => {
|
||||||
// Check if the file exists and open it
|
// Check if the file exists and open it
|
||||||
if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], {
|
if (!Provider.openFileWithLocation({
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
path,
|
path,
|
||||||
})) {
|
})) {
|
||||||
@ -99,7 +98,7 @@ export default new Provider({
|
|||||||
if (dotPos > 0 && slashPos < name.length) {
|
if (dotPos > 0 && slashPos < name.length) {
|
||||||
name = name.slice(0, dotPos);
|
name = name.slice(0, dotPos);
|
||||||
}
|
}
|
||||||
const item = await fileSvc.createFile({
|
const item = await workspaceSvc.createFile({
|
||||||
name,
|
name,
|
||||||
parentId: store.getters['file/current'].parentId,
|
parentId: store.getters['file/current'].parentId,
|
||||||
text: content.text,
|
text: content.text,
|
||||||
@ -108,9 +107,8 @@ export default new Provider({
|
|||||||
comments: content.comments,
|
comments: content.comments,
|
||||||
}, true);
|
}, true);
|
||||||
store.commit('file/setCurrentId', item.id);
|
store.commit('file/setCurrentId', item.id);
|
||||||
store.commit('syncLocation/setItem', {
|
workspaceSvc.addSyncLocation({
|
||||||
...syncLocation,
|
...syncLocation,
|
||||||
id: utils.uid(),
|
|
||||||
fileId: item.id,
|
fileId: item.id,
|
||||||
});
|
});
|
||||||
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Dropbox.`);
|
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Dropbox.`);
|
||||||
|
@ -5,15 +5,14 @@ import utils from '../utils';
|
|||||||
|
|
||||||
export default new Provider({
|
export default new Provider({
|
||||||
id: 'gist',
|
id: 'gist',
|
||||||
getToken(location) {
|
getToken({ sub }) {
|
||||||
return store.getters['data/githubTokensBySub'][location.sub];
|
return store.getters['data/githubTokensBySub'][sub];
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl({ gistId }) {
|
||||||
return `https://gist.github.com/${location.gistId}`;
|
return `https://gist.github.com/${gistId}`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ filename }) {
|
||||||
const token = this.getToken(location);
|
return filename;
|
||||||
return `${location.filename} — ${location.gistId} — ${token.name}`;
|
|
||||||
},
|
},
|
||||||
async downloadContent(token, syncLocation) {
|
async downloadContent(token, syncLocation) {
|
||||||
const content = await githubHelper.downloadGist({
|
const content = await githubHelper.downloadGist({
|
||||||
|
@ -2,21 +2,25 @@ import store from '../../store';
|
|||||||
import githubHelper from './helpers/githubHelper';
|
import githubHelper from './helpers/githubHelper';
|
||||||
import Provider from './common/Provider';
|
import Provider from './common/Provider';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import fileSvc from '../fileSvc';
|
import workspaceSvc from '../workspaceSvc';
|
||||||
|
|
||||||
const savedSha = {};
|
const savedSha = {};
|
||||||
|
|
||||||
export default new Provider({
|
export default new Provider({
|
||||||
id: 'github',
|
id: 'github',
|
||||||
getToken(location) {
|
getToken({ sub }) {
|
||||||
return store.getters['data/githubTokensBySub'][location.sub];
|
return store.getters['data/githubTokensBySub'][sub];
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl({
|
||||||
return `https://github.com/${encodeURIComponent(location.owner)}/${encodeURIComponent(location.repo)}/blob/${encodeURIComponent(location.branch)}/${encodeURIComponent(location.path)}`;
|
owner,
|
||||||
|
repo,
|
||||||
|
branch,
|
||||||
|
path,
|
||||||
|
}) {
|
||||||
|
return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(branch)}/${utils.encodeUrlPath(path)}`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ path }) {
|
||||||
const token = this.getToken(location);
|
return path;
|
||||||
return `${location.path} — ${location.owner}/${location.repo} — ${token.name}`;
|
|
||||||
},
|
},
|
||||||
async downloadContent(token, syncLocation) {
|
async downloadContent(token, syncLocation) {
|
||||||
try {
|
try {
|
||||||
@ -59,7 +63,7 @@ export default new Provider({
|
|||||||
},
|
},
|
||||||
async openFile(token, syncLocation) {
|
async openFile(token, syncLocation) {
|
||||||
// Check if the file exists and open it
|
// Check if the file exists and open it
|
||||||
if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], syncLocation)) {
|
if (!Provider.openFileWithLocation(syncLocation)) {
|
||||||
// Download content from GitHub
|
// Download content from GitHub
|
||||||
let content;
|
let content;
|
||||||
try {
|
try {
|
||||||
@ -79,7 +83,7 @@ export default new Provider({
|
|||||||
if (dotPos > 0 && slashPos < name.length) {
|
if (dotPos > 0 && slashPos < name.length) {
|
||||||
name = name.slice(0, dotPos);
|
name = name.slice(0, dotPos);
|
||||||
}
|
}
|
||||||
const item = await fileSvc.createFile({
|
const item = await workspaceSvc.createFile({
|
||||||
name,
|
name,
|
||||||
parentId: store.getters['file/current'].parentId,
|
parentId: store.getters['file/current'].parentId,
|
||||||
text: content.text,
|
text: content.text,
|
||||||
@ -88,21 +92,13 @@ export default new Provider({
|
|||||||
comments: content.comments,
|
comments: content.comments,
|
||||||
}, true);
|
}, true);
|
||||||
store.commit('file/setCurrentId', item.id);
|
store.commit('file/setCurrentId', item.id);
|
||||||
store.commit('syncLocation/setItem', {
|
workspaceSvc.addSyncLocation({
|
||||||
...syncLocation,
|
...syncLocation,
|
||||||
id: utils.uid(),
|
|
||||||
fileId: item.id,
|
fileId: item.id,
|
||||||
});
|
});
|
||||||
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from GitHub.`);
|
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from GitHub.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parseRepoUrl(url) {
|
|
||||||
const parsedRepo = url && url.match(/([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
|
||||||
return parsedRepo && {
|
|
||||||
owner: parsedRepo[1],
|
|
||||||
repo: parsedRepo[2],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
makeLocation(token, owner, repo, branch, path) {
|
makeLocation(token, owner, repo, branch, path) {
|
||||||
return {
|
return {
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
|
@ -4,18 +4,8 @@ import Provider from './common/Provider';
|
|||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import userSvc from '../userSvc';
|
import userSvc from '../userSvc';
|
||||||
|
|
||||||
const getAbsolutePath = syncData =>
|
const getAbsolutePath = ({ id }) =>
|
||||||
`${store.getters['workspace/currentWorkspace'].path || ''}${syncData.id}`;
|
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
||||||
|
|
||||||
const getWorkspaceWithOwner = () => {
|
|
||||||
const workspace = store.getters['workspace/currentWorkspace'];
|
|
||||||
const [owner, repo] = workspace.repo.split('/');
|
|
||||||
return {
|
|
||||||
...workspace,
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let treeShaMap;
|
let treeShaMap;
|
||||||
let treeFolderMap;
|
let treeFolderMap;
|
||||||
@ -31,14 +21,38 @@ export default new Provider({
|
|||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['workspace/syncToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
async initWorkspace() {
|
getWorkspaceParams({
|
||||||
const [owner, repo] = (utils.queryParams.repo || '').split('/');
|
owner,
|
||||||
const { branch } = utils.queryParams;
|
repo,
|
||||||
const workspaceParams = {
|
branch,
|
||||||
|
path,
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
repo: `${owner}/${repo}`,
|
owner,
|
||||||
|
repo,
|
||||||
branch,
|
branch,
|
||||||
|
path,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
getWorkspaceLocationUrl({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branch,
|
||||||
|
path,
|
||||||
|
}) {
|
||||||
|
return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(branch)}/${utils.encodeUrlPath(path)}`;
|
||||||
|
},
|
||||||
|
getSyncDataUrl({ id }) {
|
||||||
|
const { owner, repo, branch } = store.getters['workspace/currentWorkspace'];
|
||||||
|
return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(branch)}/${utils.encodeUrlPath(getAbsolutePath({ id }))}`;
|
||||||
|
},
|
||||||
|
getSyncDataDescription({ id }) {
|
||||||
|
return getAbsolutePath({ id });
|
||||||
|
},
|
||||||
|
async initWorkspace() {
|
||||||
|
const { owner, repo, branch } = utils.queryParams;
|
||||||
|
const workspaceParams = this.getWorkspaceParams({ owner, repo, branch });
|
||||||
const path = (utils.queryParams.path || '')
|
const path = (utils.queryParams.path || '')
|
||||||
.replace(/^\/*/, '') // Remove leading `/`
|
.replace(/^\/*/, '') // Remove leading `/`
|
||||||
.replace(/\/*$/, '/'); // Add trailing `/`
|
.replace(/\/*$/, '/'); // Add trailing `/`
|
||||||
@ -46,7 +60,7 @@ export default new Provider({
|
|||||||
workspaceParams.path = path;
|
workspaceParams.path = path;
|
||||||
}
|
}
|
||||||
const workspaceId = utils.makeWorkspaceId(workspaceParams);
|
const workspaceId = utils.makeWorkspaceId(workspaceParams);
|
||||||
let workspace = store.getters['data/sanitizedWorkspacesById'][workspaceId];
|
const workspace = store.getters['workspace/workspacesById'][workspaceId];
|
||||||
|
|
||||||
// See if we already have a token
|
// See if we already have a token
|
||||||
let token;
|
let token;
|
||||||
@ -62,34 +76,23 @@ export default new Provider({
|
|||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
const pathEntries = (path || '').split('/');
|
const pathEntries = (path || '').split('/');
|
||||||
const name = pathEntries[pathEntries.length - 2] || repo; // path ends with `/`
|
const name = pathEntries[pathEntries.length - 2] || repo; // path ends with `/`
|
||||||
workspace = {
|
store.dispatch('workspace/patchWorkspacesById', {
|
||||||
...workspaceParams,
|
|
||||||
id: workspaceId,
|
|
||||||
sub: token.sub,
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix the URL hash
|
|
||||||
utils.setQueryParams(workspaceParams);
|
|
||||||
if (workspace.url !== window.location.href) {
|
|
||||||
store.dispatch('data/patchWorkspacesById', {
|
|
||||||
[workspaceId]: {
|
[workspaceId]: {
|
||||||
...workspace,
|
...workspaceParams,
|
||||||
url: window.location.href,
|
id: workspaceId,
|
||||||
|
sub: token.sub,
|
||||||
|
name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return store.getters['data/sanitizedWorkspacesById'][workspaceId];
|
|
||||||
|
return store.getters['workspace/workspacesById'][workspaceId];
|
||||||
},
|
},
|
||||||
getChanges() {
|
getChanges() {
|
||||||
const syncToken = store.getters['workspace/syncToken'];
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
const { owner, repo, branch } = getWorkspaceWithOwner();
|
|
||||||
return githubHelper.getTree({
|
return githubHelper.getTree({
|
||||||
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token: syncToken,
|
token: syncToken,
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
branch,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
prepareChanges(tree) {
|
prepareChanges(tree) {
|
||||||
@ -322,7 +325,7 @@ export default new Provider({
|
|||||||
// locations are stored as paths, so we upload an empty file
|
// locations are stored as paths, so we upload an empty file
|
||||||
const syncToken = store.getters['workspace/syncToken'];
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
await githubHelper.uploadFile({
|
await githubHelper.uploadFile({
|
||||||
...getWorkspaceWithOwner(),
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token: syncToken,
|
token: syncToken,
|
||||||
path: getAbsolutePath(syncData),
|
path: getAbsolutePath(syncData),
|
||||||
content: '',
|
content: '',
|
||||||
@ -334,7 +337,7 @@ export default new Provider({
|
|||||||
if (treeShaMap[syncData.id]) {
|
if (treeShaMap[syncData.id]) {
|
||||||
const syncToken = store.getters['workspace/syncToken'];
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
await githubHelper.removeFile({
|
await githubHelper.removeFile({
|
||||||
...getWorkspaceWithOwner(),
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token: syncToken,
|
token: syncToken,
|
||||||
path: getAbsolutePath(syncData),
|
path: getAbsolutePath(syncData),
|
||||||
sha: treeShaMap[syncData.id],
|
sha: treeShaMap[syncData.id],
|
||||||
@ -348,7 +351,7 @@ export default new Provider({
|
|||||||
fileSyncData,
|
fileSyncData,
|
||||||
}) {
|
}) {
|
||||||
const { sha, data } = await githubHelper.downloadFile({
|
const { sha, data } = await githubHelper.downloadFile({
|
||||||
...getWorkspaceWithOwner(),
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token,
|
token,
|
||||||
path: getAbsolutePath(fileSyncData),
|
path: getAbsolutePath(fileSyncData),
|
||||||
});
|
});
|
||||||
@ -369,7 +372,7 @@ export default new Provider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { sha, data } = await githubHelper.downloadFile({
|
const { sha, data } = await githubHelper.downloadFile({
|
||||||
...getWorkspaceWithOwner(),
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token,
|
token,
|
||||||
path: getAbsolutePath(syncData),
|
path: getAbsolutePath(syncData),
|
||||||
});
|
});
|
||||||
@ -388,7 +391,7 @@ export default new Provider({
|
|||||||
const path = store.getters.gitPathsByItemId[file.id];
|
const path = store.getters.gitPathsByItemId[file.id];
|
||||||
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
|
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
|
||||||
const res = await githubHelper.uploadFile({
|
const res = await githubHelper.uploadFile({
|
||||||
...getWorkspaceWithOwner(),
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token,
|
token,
|
||||||
path: absolutePath,
|
path: absolutePath,
|
||||||
content: Provider.serializeContent(content),
|
content: Provider.serializeContent(content),
|
||||||
@ -418,7 +421,7 @@ export default new Provider({
|
|||||||
hash: item.hash,
|
hash: item.hash,
|
||||||
};
|
};
|
||||||
const res = await githubHelper.uploadFile({
|
const res = await githubHelper.uploadFile({
|
||||||
...getWorkspaceWithOwner(),
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token,
|
token,
|
||||||
path: getAbsolutePath(syncData),
|
path: getAbsolutePath(syncData),
|
||||||
content: JSON.stringify(item),
|
content: JSON.stringify(item),
|
||||||
@ -432,17 +435,8 @@ export default new Provider({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onSyncEnd() {
|
|
||||||
// Clean up
|
|
||||||
treeShaMap = null;
|
|
||||||
treeFolderMap = null;
|
|
||||||
treeFileMap = null;
|
|
||||||
treeDataMap = null;
|
|
||||||
treeSyncLocationMap = null;
|
|
||||||
treePublishLocationMap = null;
|
|
||||||
},
|
|
||||||
async listRevisions(token, fileId) {
|
async listRevisions(token, fileId) {
|
||||||
const { owner, repo, branch } = getWorkspaceWithOwner();
|
const { owner, repo, branch } = store.getters['workspace/currentWorkspace'];
|
||||||
const syncData = Provider.getContentSyncData(fileId);
|
const syncData = Provider.getContentSyncData(fileId);
|
||||||
const entries = await githubHelper.getCommits({
|
const entries = await githubHelper.getCommits({
|
||||||
token,
|
token,
|
||||||
@ -478,7 +472,7 @@ export default new Provider({
|
|||||||
async getRevisionContent(token, fileId, revisionId) {
|
async getRevisionContent(token, fileId, revisionId) {
|
||||||
const syncData = Provider.getContentSyncData(fileId);
|
const syncData = Provider.getContentSyncData(fileId);
|
||||||
const { data } = await githubHelper.downloadFile({
|
const { data } = await githubHelper.downloadFile({
|
||||||
...getWorkspaceWithOwner(),
|
...store.getters['workspace/currentWorkspace'],
|
||||||
token,
|
token,
|
||||||
branch: revisionId,
|
branch: revisionId,
|
||||||
path: getAbsolutePath(syncData),
|
path: getAbsolutePath(syncData),
|
||||||
|
@ -10,12 +10,25 @@ export default new Provider({
|
|||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['workspace/syncToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
|
getWorkspaceParams() {
|
||||||
|
// No param as it's the main workspace
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
getWorkspaceLocationUrl() {
|
||||||
|
// No direct link to app data
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getSyncDataUrl() {
|
||||||
|
// No direct link to app data
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getSyncDataDescription({ id }) {
|
||||||
|
return id;
|
||||||
|
},
|
||||||
async initWorkspace() {
|
async initWorkspace() {
|
||||||
// Nothing much to do since the main workspace isn't necessarily synchronized
|
// Nothing much to do since the main workspace isn't necessarily synchronized
|
||||||
// Remove the URL hash
|
|
||||||
utils.setQueryParams();
|
|
||||||
// Return the main workspace
|
// Return the main workspace
|
||||||
return store.getters['data/workspacesById'].main;
|
return store.getters['workspace/workspacesById'].main;
|
||||||
},
|
},
|
||||||
async getChanges() {
|
async getChanges() {
|
||||||
const syncToken = store.getters['workspace/syncToken'];
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
@ -155,7 +168,7 @@ export default new Provider({
|
|||||||
const revisions = await googleHelper.getAppDataFileRevisions(token, syncData.id);
|
const revisions = await googleHelper.getAppDataFileRevisions(token, syncData.id);
|
||||||
return revisions.map(revision => ({
|
return revisions.map(revision => ({
|
||||||
id: revision.id,
|
id: revision.id,
|
||||||
sub: revision.lastModifyingUser && `go:${revision.lastModifyingUser.permissionId}`,
|
sub: `go:${revision.lastModifyingUser.permissionId}`,
|
||||||
created: new Date(revision.modifiedTime).getTime(),
|
created: new Date(revision.modifiedTime).getTime(),
|
||||||
}))
|
}))
|
||||||
.sort((revision1, revision2) => revision2.created - revision1.created);
|
.sort((revision1, revision2) => revision2.created - revision1.created);
|
||||||
|
@ -2,20 +2,19 @@ import store from '../../store';
|
|||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import Provider from './common/Provider';
|
import Provider from './common/Provider';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import fileSvc from '../fileSvc';
|
import workspaceSvc from '../workspaceSvc';
|
||||||
|
|
||||||
export default new Provider({
|
export default new Provider({
|
||||||
id: 'googleDrive',
|
id: 'googleDrive',
|
||||||
getToken(location) {
|
getToken({ sub }) {
|
||||||
const token = store.getters['data/googleTokensBySub'][location.sub];
|
const token = store.getters['data/googleTokensBySub'][sub];
|
||||||
return token && token.isDrive ? token : null;
|
return token && token.isDrive ? token : null;
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl({ driveFileId }) {
|
||||||
return `https://docs.google.com/file/d/${location.driveFileId}/edit`;
|
return `https://docs.google.com/file/d/${driveFileId}/edit`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ driveFileId }) {
|
||||||
const token = this.getToken(location);
|
return driveFileId;
|
||||||
return `${location.driveFileId} — ${token.name}`;
|
|
||||||
},
|
},
|
||||||
async initAction() {
|
async initAction() {
|
||||||
const state = googleHelper.driveState || {};
|
const state = googleHelper.driveState || {};
|
||||||
@ -42,7 +41,7 @@ export default new Provider({
|
|||||||
folderId,
|
folderId,
|
||||||
};
|
};
|
||||||
const workspaceId = utils.makeWorkspaceId(workspaceParams);
|
const workspaceId = utils.makeWorkspaceId(workspaceParams);
|
||||||
const workspace = store.getters['data/sanitizedWorkspacesById'][workspaceId];
|
const workspace = store.getters['workspace/workspacesById'][workspaceId];
|
||||||
// If we have the workspace, open it by changing the current URL
|
// If we have the workspace, open it by changing the current URL
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
utils.setQueryParams(workspaceParams);
|
utils.setQueryParams(workspaceParams);
|
||||||
@ -86,7 +85,7 @@ export default new Provider({
|
|||||||
const token = store.getters['data/googleTokensBySub'][state.userId];
|
const token = store.getters['data/googleTokensBySub'][state.userId];
|
||||||
switch (token && state.action) {
|
switch (token && state.action) {
|
||||||
case 'create': {
|
case 'create': {
|
||||||
const file = await fileSvc.createFile({}, true);
|
const file = await workspaceSvc.createFile({}, true);
|
||||||
store.commit('file/setCurrentId', file.id);
|
store.commit('file/setCurrentId', file.id);
|
||||||
// Return a new syncLocation
|
// Return a new syncLocation
|
||||||
return this.makeLocation(token, null, googleHelper.driveActionFolder.id);
|
return this.makeLocation(token, null, googleHelper.driveActionFolder.id);
|
||||||
@ -142,7 +141,7 @@ export default new Provider({
|
|||||||
async openFiles(token, driveFiles) {
|
async openFiles(token, driveFiles) {
|
||||||
return utils.awaitSequence(driveFiles, async (driveFile) => {
|
return utils.awaitSequence(driveFiles, async (driveFile) => {
|
||||||
// Check if the file exists and open it
|
// Check if the file exists and open it
|
||||||
if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], {
|
if (!Provider.openFileWithLocation({
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
driveFileId: driveFile.id,
|
driveFileId: driveFile.id,
|
||||||
})) {
|
})) {
|
||||||
@ -161,7 +160,7 @@ export default new Provider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the file
|
// Create the file
|
||||||
const item = await fileSvc.createFile({
|
const item = await workspaceSvc.createFile({
|
||||||
name: driveFile.name,
|
name: driveFile.name,
|
||||||
parentId: store.getters['file/current'].parentId,
|
parentId: store.getters['file/current'].parentId,
|
||||||
text: content.text,
|
text: content.text,
|
||||||
@ -170,9 +169,8 @@ export default new Provider({
|
|||||||
comments: content.comments,
|
comments: content.comments,
|
||||||
}, true);
|
}, true);
|
||||||
store.commit('file/setCurrentId', item.id);
|
store.commit('file/setCurrentId', item.id);
|
||||||
store.commit('syncLocation/setItem', {
|
workspaceSvc.addSyncLocation({
|
||||||
...syncLocation,
|
...syncLocation,
|
||||||
id: utils.uid(),
|
|
||||||
fileId: item.id,
|
fileId: item.id,
|
||||||
});
|
});
|
||||||
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Google Drive.`);
|
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Google Drive.`);
|
||||||
|
@ -2,7 +2,7 @@ import store from '../../store';
|
|||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import Provider from './common/Provider';
|
import Provider from './common/Provider';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import fileSvc from '../fileSvc';
|
import workspaceSvc from '../workspaceSvc';
|
||||||
|
|
||||||
let fileIdToOpen;
|
let fileIdToOpen;
|
||||||
let syncStartPageToken;
|
let syncStartPageToken;
|
||||||
@ -12,17 +12,27 @@ export default new Provider({
|
|||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['workspace/syncToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
async initWorkspace() {
|
getWorkspaceParams({ folderId }) {
|
||||||
const makeWorkspaceParams = folderId => ({
|
return {
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
folderId,
|
folderId,
|
||||||
});
|
};
|
||||||
|
},
|
||||||
|
getWorkspaceLocationUrl({ folderId }) {
|
||||||
|
return `https://docs.google.com/folder/d/${folderId}`;
|
||||||
|
},
|
||||||
|
getSyncDataUrl({ id }) {
|
||||||
|
return `https://docs.google.com/file/d/${id}/edit`;
|
||||||
|
},
|
||||||
|
getSyncDataDescription({ id }) {
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
async initWorkspace() {
|
||||||
const makeWorkspaceId = folderId => folderId
|
const makeWorkspaceId = folderId => folderId
|
||||||
&& utils.makeWorkspaceId(makeWorkspaceParams(folderId));
|
&& utils.makeWorkspaceId(this.getWorkspaceParams({ folderId }));
|
||||||
|
|
||||||
const getWorkspace = folderId =>
|
const getWorkspace = folderId =>
|
||||||
store.getters['data/sanitizedWorkspacesById'][makeWorkspaceId(folderId)];
|
store.getters['workspace/workspacesById'][makeWorkspaceId(folderId)];
|
||||||
|
|
||||||
const initFolder = async (token, folder) => {
|
const initFolder = async (token, folder) => {
|
||||||
const appProperties = {
|
const appProperties = {
|
||||||
@ -33,24 +43,26 @@ export default new Provider({
|
|||||||
|
|
||||||
// Make sure data folder exists
|
// Make sure data folder exists
|
||||||
if (!appProperties.dataFolderId) {
|
if (!appProperties.dataFolderId) {
|
||||||
appProperties.dataFolderId = (await googleHelper.uploadFile({
|
const dataFolder = await googleHelper.uploadFile({
|
||||||
token,
|
token,
|
||||||
name: '.stackedit-data',
|
name: '.stackedit-data',
|
||||||
parents: [folder.id],
|
parents: [folder.id],
|
||||||
appProperties: { folderId: folder.id },
|
appProperties: { folderId: folder.id },
|
||||||
mediaType: googleHelper.folderMimeType,
|
mediaType: googleHelper.folderMimeType,
|
||||||
})).id;
|
});
|
||||||
|
appProperties.dataFolderId = dataFolder.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure trash folder exists
|
// Make sure trash folder exists
|
||||||
if (!appProperties.trashFolderId) {
|
if (!appProperties.trashFolderId) {
|
||||||
appProperties.trashFolderId = (await googleHelper.uploadFile({
|
const trashFolder = await googleHelper.uploadFile({
|
||||||
token,
|
token,
|
||||||
name: '.stackedit-trash',
|
name: '.stackedit-trash',
|
||||||
parents: [folder.id],
|
parents: [folder.id],
|
||||||
appProperties: { folderId: folder.id },
|
appProperties: { folderId: folder.id },
|
||||||
mediaType: googleHelper.folderMimeType,
|
mediaType: googleHelper.folderMimeType,
|
||||||
})).id;
|
});
|
||||||
|
appProperties.trashFolderId = trashFolder.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update workspace if some properties are missing
|
// Update workspace if some properties are missing
|
||||||
@ -68,13 +80,12 @@ export default new Provider({
|
|||||||
|
|
||||||
// Update workspace in the store
|
// Update workspace in the store
|
||||||
const workspaceId = makeWorkspaceId(folder.id);
|
const workspaceId = makeWorkspaceId(folder.id);
|
||||||
store.dispatch('data/patchWorkspacesById', {
|
store.dispatch('workspace/patchWorkspacesById', {
|
||||||
[workspaceId]: {
|
[workspaceId]: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
sub: token.sub,
|
sub: token.sub,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
url: window.location.href,
|
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
teamDriveId: folder.teamDriveId,
|
teamDriveId: folder.teamDriveId,
|
||||||
dataFolderId: appProperties.dataFolderId,
|
dataFolderId: appProperties.dataFolderId,
|
||||||
@ -110,11 +121,10 @@ export default new Provider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init workspace
|
// Init workspace
|
||||||
let workspace = getWorkspace(folderId);
|
if (!getWorkspace(folderId)) {
|
||||||
if (!workspace) {
|
|
||||||
let folder;
|
let folder;
|
||||||
try {
|
try {
|
||||||
folder = googleHelper.getFile(token, folderId);
|
folder = await googleHelper.getFile(token, folderId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Folder ${folderId} is not accessible. Make sure you have the right permissions.`);
|
throw new Error(`Folder ${folderId} is not accessible. Make sure you have the right permissions.`);
|
||||||
}
|
}
|
||||||
@ -124,20 +134,9 @@ export default new Provider({
|
|||||||
throw new Error(`Folder ${folderId} is part of another workspace.`);
|
throw new Error(`Folder ${folderId} is part of another workspace.`);
|
||||||
}
|
}
|
||||||
await initFolder(token, folder);
|
await initFolder(token, folder);
|
||||||
workspace = getWorkspace(folderId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix the URL hash
|
return getWorkspace(folderId);
|
||||||
utils.setQueryParams(makeWorkspaceParams(workspace.folderId));
|
|
||||||
if (workspace.url !== window.location.href) {
|
|
||||||
store.dispatch('data/patchWorkspacesById', {
|
|
||||||
[workspace.id]: {
|
|
||||||
...workspace,
|
|
||||||
url: window.location.href,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return store.getters['data/sanitizedWorkspacesById'][workspace.id];
|
|
||||||
},
|
},
|
||||||
async performAction() {
|
async performAction() {
|
||||||
const state = googleHelper.driveState || {};
|
const state = googleHelper.driveState || {};
|
||||||
@ -163,7 +162,7 @@ export default new Provider({
|
|||||||
[syncData.id]: syncData,
|
[syncData.id]: syncData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const file = await fileSvc.createFile({
|
const file = await workspaceSvc.createFile({
|
||||||
parentId: syncData && syncData.itemId,
|
parentId: syncData && syncData.itemId,
|
||||||
}, true);
|
}, true);
|
||||||
store.commit('file/setCurrentId', file.id);
|
store.commit('file/setCurrentId', file.id);
|
||||||
@ -486,6 +485,7 @@ export default new Provider({
|
|||||||
folderId: workspace.folderId,
|
folderId: workspace.folderId,
|
||||||
},
|
},
|
||||||
media: JSON.stringify(item),
|
media: JSON.stringify(item),
|
||||||
|
mediaType: 'application/json',
|
||||||
fileId: syncData && syncData.id,
|
fileId: syncData && syncData.id,
|
||||||
oldParents: syncData && syncData.parentIds,
|
oldParents: syncData && syncData.parentIds,
|
||||||
ifNotTooLate,
|
ifNotTooLate,
|
||||||
|
@ -8,13 +8,16 @@ const getAppKey = (fullAccess) => {
|
|||||||
return 'sw0hlixhr8q1xk0';
|
return 'sw0hlixhr8q1xk0';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const httpHeaderSafeJson = args => args && JSON.stringify(args)
|
||||||
|
.replace(/[\u007f-\uffff]/g, c => `\\u${`000${c.charCodeAt(0).toString(16)}`.slice(-4)}`);
|
||||||
|
|
||||||
const request = (token, options, args) => networkSvc.request({
|
const request = (token, options, args) => networkSvc.request({
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...options.headers || {},
|
...options.headers || {},
|
||||||
'Content-Type': options.body && (typeof options.body === 'string'
|
'Content-Type': options.body && (typeof options.body === 'string'
|
||||||
? 'application/octet-stream' : 'application/json; charset=utf-8'),
|
? 'application/octet-stream' : 'application/json; charset=utf-8'),
|
||||||
'Dropbox-API-Arg': args && JSON.stringify(args),
|
'Dropbox-API-Arg': httpHeaderSafeJson(args),
|
||||||
Authorization: `Bearer ${token.accessToken}`,
|
Authorization: `Bearer ${token.accessToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -413,7 +413,7 @@ export default {
|
|||||||
});
|
});
|
||||||
revisions.forEach((revision) => {
|
revisions.forEach((revision) => {
|
||||||
store.commit('userInfo/addItem', {
|
store.commit('userInfo/addItem', {
|
||||||
id: revision.lastModifyingUser.permissionId,
|
id: `go:${revision.lastModifyingUser.permissionId}`,
|
||||||
name: revision.lastModifyingUser.displayName,
|
name: revision.lastModifyingUser.displayName,
|
||||||
imageUrl: revision.lastModifyingUser.photoLink,
|
imageUrl: revision.lastModifyingUser.photoLink,
|
||||||
});
|
});
|
||||||
|
@ -7,12 +7,11 @@ export default new Provider({
|
|||||||
getToken(location) {
|
getToken(location) {
|
||||||
return store.getters['data/wordpressTokensBySub'][location.sub];
|
return store.getters['data/wordpressTokensBySub'][location.sub];
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl(location) {
|
||||||
return `https://wordpress.com/post/${location.siteId}/${location.postId}`;
|
return `https://wordpress.com/post/${location.siteId}/${location.postId}`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ postId }) {
|
||||||
const token = this.getToken(location);
|
return postId;
|
||||||
return `${location.postId} — ${location.domain} — ${token.name}`;
|
|
||||||
},
|
},
|
||||||
async publish(token, html, metadata, publishLocation) {
|
async publish(token, html, metadata, publishLocation) {
|
||||||
const post = await wordpressHelper.uploadPost({
|
const post = await wordpressHelper.uploadPost({
|
||||||
|
@ -7,13 +7,12 @@ export default new Provider({
|
|||||||
getToken(location) {
|
getToken(location) {
|
||||||
return store.getters['data/zendeskTokensBySub'][location.sub];
|
return store.getters['data/zendeskTokensBySub'][location.sub];
|
||||||
},
|
},
|
||||||
getUrl(location) {
|
getLocationUrl(location) {
|
||||||
const token = this.getToken(location);
|
const token = this.getToken(location);
|
||||||
return `https://${token.subdomain}.zendesk.com/hc/${location.locale}/articles/${location.articleId}`;
|
return `https://${token.subdomain}.zendesk.com/hc/${location.locale}/articles/${location.articleId}`;
|
||||||
},
|
},
|
||||||
getDescription(location) {
|
getLocationDescription({ articleId }) {
|
||||||
const token = this.getToken(location);
|
return articleId;
|
||||||
return `${location.articleId} — ${token.name} — ${token.subdomain}`;
|
|
||||||
},
|
},
|
||||||
async publish(token, html, metadata, publishLocation) {
|
async publish(token, html, metadata, publishLocation) {
|
||||||
const articleId = await zendeskHelper.uploadArticle({
|
const articleId = await zendeskHelper.uploadArticle({
|
||||||
|
@ -4,6 +4,7 @@ import utils from './utils';
|
|||||||
import networkSvc from './networkSvc';
|
import networkSvc from './networkSvc';
|
||||||
import exportSvc from './exportSvc';
|
import exportSvc from './exportSvc';
|
||||||
import providerRegistry from './providers/common/providerRegistry';
|
import providerRegistry from './providers/common/providerRegistry';
|
||||||
|
import workspaceSvc from './workspaceSvc';
|
||||||
|
|
||||||
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ const publish = async (publishLocation) => {
|
|||||||
const content = await localDbSvc.loadItem(`${fileId}/content`);
|
const content = await localDbSvc.loadItem(`${fileId}/content`);
|
||||||
const file = store.state.file.itemsById[fileId];
|
const file = store.state.file.itemsById[fileId];
|
||||||
const properties = utils.computeProperties(content.properties);
|
const properties = utils.computeProperties(content.properties);
|
||||||
const provider = providerRegistry.providers[publishLocation.providerId];
|
const provider = providerRegistry.providersById[publishLocation.providerId];
|
||||||
const token = provider.getToken(publishLocation);
|
const token = provider.getToken(publishLocation);
|
||||||
const metadata = {
|
const metadata = {
|
||||||
title: ensureString(properties.title, file.name),
|
title: ensureString(properties.title, file.name),
|
||||||
@ -122,14 +123,12 @@ const requestPublish = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createPublishLocation = (publishLocation) => {
|
const createPublishLocation = (publishLocation) => {
|
||||||
publishLocation.id = utils.uid();
|
|
||||||
const currentFile = store.getters['file/current'];
|
const currentFile = store.getters['file/current'];
|
||||||
publishLocation.fileId = currentFile.id;
|
publishLocation.fileId = currentFile.id;
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
'queue/enqueue',
|
'queue/enqueue',
|
||||||
async () => {
|
async () => {
|
||||||
const publishLocationToStore = await publish(publishLocation);
|
workspaceSvc.addPublishLocation(await publish(publishLocation));
|
||||||
store.commit('publishLocation/setItem', publishLocationToStore);
|
|
||||||
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
|
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
class SectionDimension {
|
|
||||||
constructor(startOffset, endOffset) {
|
|
||||||
this.startOffset = startOffset;
|
|
||||||
this.endOffset = endOffset;
|
|
||||||
this.height = endOffset - startOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dimensionNormalizer = dimensionName => (editorSvc) => {
|
|
||||||
const dimensionList = editorSvc.previewCtx.sectionDescList
|
|
||||||
.map(sectionDesc => sectionDesc[dimensionName]);
|
|
||||||
let dimension;
|
|
||||||
let i;
|
|
||||||
let j;
|
|
||||||
for (i = 0; i < dimensionList.length; i += 1) {
|
|
||||||
dimension = dimensionList[i];
|
|
||||||
if (dimension.height) {
|
|
||||||
for (j = i + 1; j < dimensionList.length && dimensionList[j].height === 0; j += 1) {
|
|
||||||
// Loop
|
|
||||||
}
|
|
||||||
const normalizeFactor = j - i;
|
|
||||||
if (normalizeFactor !== 1) {
|
|
||||||
const normalizedHeight = dimension.height / normalizeFactor;
|
|
||||||
dimension.height = normalizedHeight;
|
|
||||||
dimension.endOffset = dimension.startOffset + dimension.height;
|
|
||||||
for (j = i + 1; j < i + normalizeFactor; j += 1) {
|
|
||||||
const startOffset = dimension.endOffset;
|
|
||||||
dimension = dimensionList[j];
|
|
||||||
dimension.startOffset = startOffset;
|
|
||||||
dimension.height = normalizedHeight;
|
|
||||||
dimension.endOffset = dimension.startOffset + dimension.height;
|
|
||||||
}
|
|
||||||
i = j - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeEditorDimensions = dimensionNormalizer('editorDimension');
|
|
||||||
const normalizePreviewDimensions = dimensionNormalizer('previewDimension');
|
|
||||||
const normalizeTocDimensions = dimensionNormalizer('tocDimension');
|
|
||||||
|
|
||||||
const measureSectionDimensions = (editorSvc) => {
|
|
||||||
let editorSectionOffset = 0;
|
|
||||||
let previewSectionOffset = 0;
|
|
||||||
let tocSectionOffset = 0;
|
|
||||||
let sectionDesc = editorSvc.previewCtx.sectionDescList[0];
|
|
||||||
let nextSectionDesc;
|
|
||||||
let i = 1;
|
|
||||||
for (; i < editorSvc.previewCtx.sectionDescList.length; i += 1) {
|
|
||||||
nextSectionDesc = editorSvc.previewCtx.sectionDescList[i];
|
|
||||||
|
|
||||||
// Measure editor section
|
|
||||||
let newEditorSectionOffset = nextSectionDesc.editorElt
|
|
||||||
? nextSectionDesc.editorElt.offsetTop
|
|
||||||
: editorSectionOffset;
|
|
||||||
newEditorSectionOffset = newEditorSectionOffset > editorSectionOffset
|
|
||||||
? newEditorSectionOffset
|
|
||||||
: editorSectionOffset;
|
|
||||||
sectionDesc.editorDimension = new SectionDimension(editorSectionOffset, newEditorSectionOffset);
|
|
||||||
editorSectionOffset = newEditorSectionOffset;
|
|
||||||
|
|
||||||
// Measure preview section
|
|
||||||
let newPreviewSectionOffset = nextSectionDesc.previewElt
|
|
||||||
? nextSectionDesc.previewElt.offsetTop
|
|
||||||
: previewSectionOffset;
|
|
||||||
newPreviewSectionOffset = newPreviewSectionOffset > previewSectionOffset
|
|
||||||
? newPreviewSectionOffset
|
|
||||||
: previewSectionOffset;
|
|
||||||
sectionDesc.previewDimension = new SectionDimension(
|
|
||||||
previewSectionOffset,
|
|
||||||
newPreviewSectionOffset,
|
|
||||||
);
|
|
||||||
previewSectionOffset = newPreviewSectionOffset;
|
|
||||||
|
|
||||||
// Measure TOC section
|
|
||||||
let newTocSectionOffset = nextSectionDesc.tocElt
|
|
||||||
? nextSectionDesc.tocElt.offsetTop + (nextSectionDesc.tocElt.offsetHeight / 2)
|
|
||||||
: tocSectionOffset;
|
|
||||||
newTocSectionOffset = newTocSectionOffset > tocSectionOffset
|
|
||||||
? newTocSectionOffset
|
|
||||||
: tocSectionOffset;
|
|
||||||
sectionDesc.tocDimension = new SectionDimension(tocSectionOffset, newTocSectionOffset);
|
|
||||||
tocSectionOffset = newTocSectionOffset;
|
|
||||||
|
|
||||||
sectionDesc = nextSectionDesc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last section
|
|
||||||
sectionDesc = editorSvc.previewCtx.sectionDescList[i - 1];
|
|
||||||
if (sectionDesc) {
|
|
||||||
sectionDesc.editorDimension = new SectionDimension(
|
|
||||||
editorSectionOffset,
|
|
||||||
editorSvc.editorElt.scrollHeight,
|
|
||||||
);
|
|
||||||
sectionDesc.previewDimension = new SectionDimension(
|
|
||||||
previewSectionOffset,
|
|
||||||
editorSvc.previewElt.scrollHeight,
|
|
||||||
);
|
|
||||||
sectionDesc.tocDimension = new SectionDimension(
|
|
||||||
tocSectionOffset,
|
|
||||||
editorSvc.tocElt.scrollHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeEditorDimensions(editorSvc);
|
|
||||||
normalizePreviewDimensions(editorSvc);
|
|
||||||
normalizeTocDimensions(editorSvc);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
measureSectionDimensions,
|
|
||||||
};
|
|
@ -9,12 +9,12 @@ import './providers/couchdbWorkspaceProvider';
|
|||||||
import './providers/githubWorkspaceProvider';
|
import './providers/githubWorkspaceProvider';
|
||||||
import './providers/googleDriveWorkspaceProvider';
|
import './providers/googleDriveWorkspaceProvider';
|
||||||
import tempFileSvc from './tempFileSvc';
|
import tempFileSvc from './tempFileSvc';
|
||||||
import fileSvc from './fileSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
|
|
||||||
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||||
const restartContentSyncAfter = 500; // Restart if an authorize window pops up
|
const restartContentSyncAfter = 1000; // Enough to detect an authorize pop up
|
||||||
const maxContentHistory = 20;
|
const maxContentHistory = 20;
|
||||||
|
|
||||||
const LAST_SEEN = 0;
|
const LAST_SEEN = 0;
|
||||||
@ -112,9 +112,11 @@ const cleanSyncedContent = (syncedContent) => {
|
|||||||
delete syncedContent.syncHistory[syncLocationId];
|
delete syncedContent.syncHistory[syncLocationId];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const allSyncLocationHashSet = new Set([]
|
const allSyncLocationHashSet = new Set([]
|
||||||
.concat(...Object.keys(syncedContent.syncHistory)
|
.concat(...Object.keys(syncedContent.syncHistory)
|
||||||
.map(id => syncedContent.syncHistory[id])));
|
.map(id => syncedContent.syncHistory[id])));
|
||||||
|
|
||||||
// Clean historyData from unused contents
|
// Clean historyData from unused contents
|
||||||
Object.keys(syncedContent.historyData)
|
Object.keys(syncedContent.historyData)
|
||||||
.map(hash => parseInt(hash, 10))
|
.map(hash => parseInt(hash, 10))
|
||||||
@ -131,9 +133,11 @@ const cleanSyncedContent = (syncedContent) => {
|
|||||||
const applyChanges = (changes) => {
|
const applyChanges = (changes) => {
|
||||||
const allItemsById = { ...store.getters.allItemsById };
|
const allItemsById = { ...store.getters.allItemsById };
|
||||||
const syncDataById = { ...store.getters['data/syncDataById'] };
|
const syncDataById = { ...store.getters['data/syncDataById'] };
|
||||||
|
const idsToKeep = {};
|
||||||
|
let saveSyncData = false;
|
||||||
let getExistingItem;
|
let getExistingItem;
|
||||||
if (workspaceProvider.isGit) {
|
if (store.getters['workspace/currentWorkspaceIsGit']) {
|
||||||
const { itemsByGitPath } = store.getters;
|
const itemsByGitPath = { ...store.getters.itemsByGitPath };
|
||||||
getExistingItem = (existingSyncData) => {
|
getExistingItem = (existingSyncData) => {
|
||||||
const items = existingSyncData && itemsByGitPath[existingSyncData.id];
|
const items = existingSyncData && itemsByGitPath[existingSyncData.id];
|
||||||
return items ? items[0] : null;
|
return items ? items[0] : null;
|
||||||
@ -142,9 +146,6 @@ const applyChanges = (changes) => {
|
|||||||
getExistingItem = existingSyncData => existingSyncData && allItemsById[existingSyncData.itemId];
|
getExistingItem = existingSyncData => existingSyncData && allItemsById[existingSyncData.itemId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const idsToKeep = {};
|
|
||||||
let saveSyncData = false;
|
|
||||||
|
|
||||||
// Process each change
|
// Process each change
|
||||||
changes.forEach((change) => {
|
changes.forEach((change) => {
|
||||||
const existingSyncData = syncDataById[change.syncDataId];
|
const existingSyncData = syncDataById[change.syncDataId];
|
||||||
@ -184,7 +185,10 @@ const applyChanges = (changes) => {
|
|||||||
|
|
||||||
if (saveSyncData) {
|
if (saveSyncData) {
|
||||||
store.dispatch('data/setSyncDataById', syncDataById);
|
store.dispatch('data/setSyncDataById', syncDataById);
|
||||||
fileSvc.ensureUniquePaths(idsToKeep);
|
|
||||||
|
// Sanitize the workspace
|
||||||
|
workspaceSvc.ensureUniquePaths(idsToKeep);
|
||||||
|
workspaceSvc.ensureUniqueLocations(idsToKeep);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -192,18 +196,17 @@ const applyChanges = (changes) => {
|
|||||||
* Create a sync location by uploading the current file content.
|
* Create a sync location by uploading the current file content.
|
||||||
*/
|
*/
|
||||||
const createSyncLocation = (syncLocation) => {
|
const createSyncLocation = (syncLocation) => {
|
||||||
syncLocation.id = utils.uid();
|
|
||||||
const currentFile = store.getters['file/current'];
|
const currentFile = store.getters['file/current'];
|
||||||
const fileId = currentFile.id;
|
const fileId = currentFile.id;
|
||||||
syncLocation.fileId = fileId;
|
syncLocation.fileId = fileId;
|
||||||
// Use deepCopy to freeze item
|
// Use deepCopy to freeze the item
|
||||||
const content = utils.deepCopy(store.getters['content/current']);
|
const content = utils.deepCopy(store.getters['content/current']);
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
'queue/enqueue',
|
'queue/enqueue',
|
||||||
async () => {
|
async () => {
|
||||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
const provider = providerRegistry.providersById[syncLocation.providerId];
|
||||||
const token = provider.getToken(syncLocation);
|
const token = provider.getToken(syncLocation);
|
||||||
const syncLocationToStore = await provider.uploadContent(token, {
|
const updatedSyncLocation = await provider.uploadContent(token, {
|
||||||
...content,
|
...content,
|
||||||
history: [content.hash],
|
history: [content.hash],
|
||||||
}, syncLocation);
|
}, syncLocation);
|
||||||
@ -216,7 +219,7 @@ const createSyncLocation = (syncLocation) => {
|
|||||||
newSyncedContent.historyData[content.hash] = content;
|
newSyncedContent.historyData[content.hash] = content;
|
||||||
|
|
||||||
store.commit('syncedContent/patchItem', newSyncedContent);
|
store.commit('syncedContent/patchItem', newSyncedContent);
|
||||||
store.commit('syncLocation/setItem', syncLocationToStore);
|
workspaceSvc.addSyncLocation(updatedSyncLocation);
|
||||||
store.dispatch('notification/info', `A new synchronized location was added to "${currentFile.name}".`);
|
store.dispatch('notification/info', `A new synchronized location was added to "${currentFile.name}".`);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -241,7 +244,7 @@ const tooLateChecker = (timeout) => {
|
|||||||
const isTempFile = (fileId) => {
|
const isTempFile = (fileId) => {
|
||||||
const contentId = `${fileId}/content`;
|
const contentId = `${fileId}/content`;
|
||||||
if (store.getters['data/syncDataByItemId'][contentId]) {
|
if (store.getters['data/syncDataByItemId'][contentId]) {
|
||||||
// If file has already been synced, it's not a temp file
|
// If file has already been synced, let's not consider it a temp file
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const file = store.state.file.itemsById[fileId];
|
const file = store.state.file.itemsById[fileId];
|
||||||
@ -271,8 +274,11 @@ const isTempFile = (fileId) => {
|
|||||||
* Patch sync data if some have changed in the result.
|
* Patch sync data if some have changed in the result.
|
||||||
*/
|
*/
|
||||||
const updateSyncData = (result) => {
|
const updateSyncData = (result) => {
|
||||||
['syncData', 'contentSyncData', 'fileSyncData'].forEach((field) => {
|
[
|
||||||
const syncData = result[field];
|
result.syncData,
|
||||||
|
result.contentSyncData,
|
||||||
|
result.fileSyncData,
|
||||||
|
].forEach((syncData) => {
|
||||||
if (syncData) {
|
if (syncData) {
|
||||||
const oldSyncData = store.getters['data/syncDataById'][syncData.id];
|
const oldSyncData = store.getters['data/syncDataById'][syncData.id];
|
||||||
if (utils.serializeObject(oldSyncData) !== utils.serializeObject(syncData)) {
|
if (utils.serializeObject(oldSyncData) !== utils.serializeObject(syncData)) {
|
||||||
@ -286,7 +292,7 @@ const updateSyncData = (result) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class SyncContext {
|
class SyncContext {
|
||||||
restart = false;
|
restartSkipContents = false;
|
||||||
attempted = {};
|
attempted = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,7 +326,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await utils.awaitSequence(syncLocations, async (syncLocation) => {
|
await utils.awaitSequence(syncLocations, async (syncLocation) => {
|
||||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
const provider = providerRegistry.providersById[syncLocation.providerId];
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -491,7 +497,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
|||||||
if (provider === workspaceProvider &&
|
if (provider === workspaceProvider &&
|
||||||
!store.getters['data/syncDataByItemId'][fileId]
|
!store.getters['data/syncDataByItemId'][fileId]
|
||||||
) {
|
) {
|
||||||
syncContext.restart = true;
|
syncContext.restartSkipContents = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -514,15 +520,16 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
|||||||
if (err && err.message === 'TOO_LATE') {
|
if (err && err.message === 'TOO_LATE') {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
await syncFile(fileId, syncContext);
|
await syncFile(fileId, syncContext);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
throw err;
|
|
||||||
} finally {
|
} finally {
|
||||||
await localDbSvc.unloadContents();
|
await localDbSvc.unloadContents();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync a data item, typically settings, workspaces and templates.
|
* Sync a data item, typically settings, templates or workspaces.
|
||||||
*/
|
*/
|
||||||
const syncDataItem = async (dataId) => {
|
const syncDataItem = async (dataId) => {
|
||||||
const getItem = () => store.state.data.itemsById[dataId]
|
const getItem = () => store.state.data.itemsById[dataId]
|
||||||
@ -543,8 +550,8 @@ const syncDataItem = async (dataId) => {
|
|||||||
|
|
||||||
const serverItem = item;
|
const serverItem = item;
|
||||||
const dataSyncData = store.getters['data/dataSyncDataById'][dataId];
|
const dataSyncData = store.getters['data/dataSyncDataById'][dataId];
|
||||||
|
const clientItem = utils.deepCopy(getItem());
|
||||||
let mergedItem = (() => {
|
let mergedItem = (() => {
|
||||||
const clientItem = utils.deepCopy(getItem());
|
|
||||||
if (!clientItem) {
|
if (!clientItem) {
|
||||||
return serverItem;
|
return serverItem;
|
||||||
}
|
}
|
||||||
@ -572,6 +579,13 @@ const syncDataItem = async (dataId) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clientItem && dataId === 'workspaces') {
|
||||||
|
// Clean deleted workspaces
|
||||||
|
await Promise.all(Object.keys(clientItem.data)
|
||||||
|
.filter(id => !mergedItem.data[id])
|
||||||
|
.map(id => workspaceSvc.removeWorkspace(id)));
|
||||||
|
}
|
||||||
|
|
||||||
// Update item in store
|
// Update item in store
|
||||||
store.commit('data/setItem', {
|
store.commit('data/setItem', {
|
||||||
id: dataId,
|
id: dataId,
|
||||||
@ -581,19 +595,17 @@ const syncDataItem = async (dataId) => {
|
|||||||
// Retrieve item with new `hash` and freeze it
|
// Retrieve item with new `hash` and freeze it
|
||||||
mergedItem = utils.deepCopy(getItem());
|
mergedItem = utils.deepCopy(getItem());
|
||||||
|
|
||||||
if (serverItem && serverItem.hash === mergedItem.hash) {
|
// Upload merged data item if out of sync
|
||||||
return;
|
if (!serverItem || serverItem.hash !== mergedItem.hash) {
|
||||||
|
updateSyncData(await workspaceProvider.uploadWorkspaceData({
|
||||||
|
token,
|
||||||
|
item: mergedItem,
|
||||||
|
syncData: store.getters['data/syncDataByItemId'][dataId],
|
||||||
|
ifNotTooLate: tooLateChecker(restartContentSyncAfter),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload merged data item
|
// Copy sync data into data sync data
|
||||||
updateSyncData(await workspaceProvider.uploadWorkspaceData({
|
|
||||||
token,
|
|
||||||
item: mergedItem,
|
|
||||||
syncData: store.getters['data/syncDataByItemId'][dataId],
|
|
||||||
ifNotTooLate: tooLateChecker(restartContentSyncAfter),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Update data sync data
|
|
||||||
store.dispatch('data/patchDataSyncDataById', {
|
store.dispatch('data/patchDataSyncDataById', {
|
||||||
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
|
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
|
||||||
});
|
});
|
||||||
@ -602,7 +614,7 @@ const syncDataItem = async (dataId) => {
|
|||||||
/**
|
/**
|
||||||
* Sync the whole workspace with the main provider and the current file explicit locations.
|
* Sync the whole workspace with the main provider and the current file explicit locations.
|
||||||
*/
|
*/
|
||||||
const syncWorkspace = async () => {
|
const syncWorkspace = async (skipContents = false) => {
|
||||||
try {
|
try {
|
||||||
const workspace = store.getters['workspace/currentWorkspace'];
|
const workspace = store.getters['workspace/currentWorkspace'];
|
||||||
const syncContext = new SyncContext();
|
const syncContext = new SyncContext();
|
||||||
@ -627,8 +639,8 @@ const syncWorkspace = async () => {
|
|||||||
// Prevent from sending items too long after changes have been retrieved
|
// Prevent from sending items too long after changes have been retrieved
|
||||||
const ifNotTooLate = tooLateChecker(restartSyncAfter);
|
const ifNotTooLate = tooLateChecker(restartSyncAfter);
|
||||||
|
|
||||||
// Called until no item to save
|
// Find and save one item to save
|
||||||
const saveNextItem = () => ifNotTooLate(async () => {
|
await utils.awaitSome(() => ifNotTooLate(async () => {
|
||||||
const storeItemMap = {
|
const storeItemMap = {
|
||||||
...store.state.file.itemsById,
|
...store.state.file.itemsById,
|
||||||
...store.state.folder.itemsById,
|
...store.state.folder.itemsById,
|
||||||
@ -654,27 +666,26 @@ const syncWorkspace = async () => {
|
|||||||
},
|
},
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
if (changedItem) {
|
if (!changedItem) return false;
|
||||||
const resultSyncData = await workspaceProvider
|
|
||||||
.saveWorkspaceItem({
|
|
||||||
// Use deepCopy to freeze objects
|
|
||||||
item: utils.deepCopy(changedItem),
|
|
||||||
syncData: utils.deepCopy(syncDataToUpdate),
|
|
||||||
ifNotTooLate,
|
|
||||||
});
|
|
||||||
store.dispatch('data/patchSyncDataById', {
|
|
||||||
[resultSyncData.id]: resultSyncData,
|
|
||||||
});
|
|
||||||
await saveNextItem();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await saveNextItem();
|
|
||||||
|
|
||||||
// Called until no item to remove
|
const resultSyncData = await workspaceProvider
|
||||||
const removeNextItem = () => ifNotTooLate(async () => {
|
.saveWorkspaceItem({
|
||||||
|
// Use deepCopy to freeze objects
|
||||||
|
item: utils.deepCopy(changedItem),
|
||||||
|
syncData: utils.deepCopy(syncDataToUpdate),
|
||||||
|
ifNotTooLate,
|
||||||
|
});
|
||||||
|
store.dispatch('data/patchSyncDataById', {
|
||||||
|
[resultSyncData.id]: resultSyncData,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Find and remove one item to remove
|
||||||
|
await utils.awaitSome(() => ifNotTooLate(async () => {
|
||||||
let getItem;
|
let getItem;
|
||||||
let getFileItem;
|
let getFileItem;
|
||||||
if (workspaceProvider.isGit) {
|
if (store.getters['workspace/currentWorkspaceIsGit']) {
|
||||||
const { itemsByGitPath } = store.getters;
|
const { itemsByGitPath } = store.getters;
|
||||||
getItem = syncData => itemsByGitPath[syncData.id];
|
getItem = syncData => itemsByGitPath[syncData.id];
|
||||||
getFileItem = syncData => itemsByGitPath[syncData.id.slice(1)]; // Remove leading /
|
getFileItem = syncData => itemsByGitPath[syncData.id.slice(1)]; // Remove leading /
|
||||||
@ -701,18 +712,17 @@ const syncWorkspace = async () => {
|
|||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
if (syncDataToRemove) {
|
if (!syncDataToRemove) return false;
|
||||||
await workspaceProvider.removeWorkspaceItem({
|
|
||||||
syncData: syncDataToRemove,
|
await workspaceProvider.removeWorkspaceItem({
|
||||||
ifNotTooLate,
|
syncData: syncDataToRemove,
|
||||||
});
|
ifNotTooLate,
|
||||||
const syncDataCopy = { ...store.getters['data/syncDataById'] };
|
});
|
||||||
delete syncDataCopy[syncDataToRemove.id];
|
const syncDataCopy = { ...store.getters['data/syncDataById'] };
|
||||||
store.dispatch('data/setSyncDataById', syncDataCopy);
|
delete syncDataCopy[syncDataToRemove.id];
|
||||||
await removeNextItem();
|
store.dispatch('data/setSyncDataById', syncDataCopy);
|
||||||
}
|
return true;
|
||||||
});
|
}));
|
||||||
await removeNextItem();
|
|
||||||
|
|
||||||
// Sync settings and workspaces only in the main workspace
|
// Sync settings and workspaces only in the main workspace
|
||||||
if (workspace.id === 'main') {
|
if (workspace.id === 'main') {
|
||||||
@ -721,63 +731,61 @@ const syncWorkspace = async () => {
|
|||||||
}
|
}
|
||||||
await syncDataItem('templates');
|
await syncDataItem('templates');
|
||||||
|
|
||||||
const getOneFileIdToSync = () => {
|
if (!skipContents) {
|
||||||
let getSyncData;
|
const currentFileId = store.getters['file/current'].id;
|
||||||
if (workspaceProvider.isGit) {
|
if (currentFileId) {
|
||||||
const { gitPathsByItemId } = store.getters;
|
// Sync current file first
|
||||||
const syncDataById = store.getters['data/syncDataById'];
|
await syncFile(currentFileId, syncContext);
|
||||||
// Use file git path as content may not exist or not be loaded
|
|
||||||
getSyncData = fileId => syncDataById[`/${gitPathsByItemId[fileId]}`];
|
|
||||||
} else {
|
|
||||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
|
||||||
getSyncData = (fileId, contentId) => syncDataByItemId[contentId];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all [fileId, contentId]
|
// Find and sync one file out of sync
|
||||||
const ids = [
|
await utils.awaitSome(async () => {
|
||||||
...Object.keys(localDbSvc.hashMap.content)
|
let getSyncData;
|
||||||
.map(contentId => [contentId.split('/')[0], contentId]),
|
if (store.getters['workspace/currentWorkspaceIsGit']) {
|
||||||
...store.getters['file/items']
|
const { gitPathsByItemId } = store.getters;
|
||||||
.map(file => [file.id, `${file.id}/content`]),
|
const syncDataById = store.getters['data/syncDataById'];
|
||||||
];
|
getSyncData = contentId => syncDataById[gitPathsByItemId[contentId]];
|
||||||
|
} else {
|
||||||
// Find the first content out of sync
|
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||||
const contentMap = store.state.content.itemsById;
|
getSyncData = contentId => syncDataByItemId[contentId];
|
||||||
return utils.someResult(ids, ([fileId, contentId]) => {
|
|
||||||
// Get content hash from itemsById or from localDbSvc if not loaded
|
|
||||||
const loadedContent = contentMap[contentId];
|
|
||||||
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
|
|
||||||
const syncData = getSyncData(fileId, contentId);
|
|
||||||
if (
|
|
||||||
// Sync if content syncing was not attempted yet
|
|
||||||
!syncContext.attempted[contentId] &&
|
|
||||||
// And if syncData does not exist or if content hash and syncData hash are inconsistent
|
|
||||||
(!syncData || syncData.hash !== hash)
|
|
||||||
) {
|
|
||||||
return fileId;
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
// Collect all [fileId, contentId]
|
||||||
|
const ids = [
|
||||||
|
...Object.keys(localDbSvc.hashMap.content)
|
||||||
|
.map(contentId => [contentId.split('/')[0], contentId]),
|
||||||
|
...store.getters['file/items']
|
||||||
|
.map(file => [file.id, `${file.id}/content`]),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Find the first content out of sync
|
||||||
|
const contentMap = store.state.content.itemsById;
|
||||||
|
const fileIdToSync = utils.someResult(ids, ([fileId, contentId]) => {
|
||||||
|
// Get the content hash from itemsById or from localDbSvc if not loaded
|
||||||
|
const loadedContent = contentMap[contentId];
|
||||||
|
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
|
||||||
|
const syncData = getSyncData(contentId);
|
||||||
|
if (
|
||||||
|
// Sync if content syncing was not attempted yet
|
||||||
|
!syncContext.attempted[contentId] &&
|
||||||
|
// And if syncData does not exist or if content hash and syncData hash are inconsistent
|
||||||
|
(!syncData || syncData.hash !== hash)
|
||||||
|
) {
|
||||||
|
return fileId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fileIdToSync) return false;
|
||||||
|
|
||||||
|
await syncFile(fileIdToSync, syncContext);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const syncNextFile = async () => {
|
|
||||||
const fileId = getOneFileIdToSync();
|
|
||||||
if (fileId) {
|
|
||||||
await syncFile(fileId, syncContext);
|
|
||||||
await syncNextFile();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentFileId = store.getters['file/current'].id;
|
|
||||||
if (currentFileId) {
|
|
||||||
// Sync current file first
|
|
||||||
await syncFile(currentFileId, syncContext);
|
|
||||||
}
|
}
|
||||||
await syncNextFile();
|
|
||||||
|
|
||||||
if (syncContext.restart) {
|
// Restart sync if requested
|
||||||
// Restart sync
|
if (syncContext.restartSkipContents) {
|
||||||
await syncWorkspace();
|
await syncWorkspace(true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && err.message === 'TOO_LATE') {
|
if (err && err.message === 'TOO_LATE') {
|
||||||
@ -786,10 +794,6 @@ const syncWorkspace = async () => {
|
|||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (workspaceProvider.onSyncEnd) {
|
|
||||||
workspaceProvider.onSyncEnd();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -835,9 +839,9 @@ const requestSync = () => {
|
|||||||
if (isWorkspaceSyncPossible()) {
|
if (isWorkspaceSyncPossible()) {
|
||||||
await syncWorkspace();
|
await syncWorkspace();
|
||||||
} else if (hasCurrentFileSyncLocations()) {
|
} else if (hasCurrentFileSyncLocations()) {
|
||||||
// Only sync current file if workspace sync is unavailable.
|
// Only sync the current file if workspace sync is unavailable
|
||||||
// We could sync all files that are out-of-sync but it would
|
// as we don't want to look for out-of-sync files by loading
|
||||||
// require to load all the syncedContent objects from the DB.
|
// all the syncedContent objects.
|
||||||
await syncFile(store.getters['file/current'].id);
|
await syncFile(store.getters['file/current'].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -845,7 +849,7 @@ const requestSync = () => {
|
|||||||
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
|
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
|
||||||
const file = store.state.file.itemsById[fileId];
|
const file = store.state.file.itemsById[fileId];
|
||||||
if (file && file.hash === fileHash) {
|
if (file && file.hash === fileHash) {
|
||||||
fileSvc.deleteFile(fileId);
|
workspaceSvc.deleteFile(fileId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@ -865,22 +869,25 @@ export default {
|
|||||||
localDbSvc.syncLocalStorage();
|
localDbSvc.syncLocalStorage();
|
||||||
|
|
||||||
// Try to find a suitable action provider
|
// Try to find a suitable action provider
|
||||||
actionProvider = providerRegistry.providers[utils.queryParams.providerId];
|
actionProvider = providerRegistry.providersById[utils.queryParams.providerId];
|
||||||
if (actionProvider && actionProvider.initAction) {
|
if (actionProvider && actionProvider.initAction) {
|
||||||
await actionProvider.initAction();
|
await actionProvider.initAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find a suitable workspace sync provider
|
// Try to find a suitable workspace sync provider
|
||||||
workspaceProvider = providerRegistry.providers[utils.queryParams.providerId];
|
workspaceProvider = providerRegistry.providersById[utils.queryParams.providerId];
|
||||||
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
|
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
|
||||||
workspaceProvider = googleDriveAppDataProvider;
|
workspaceProvider = googleDriveAppDataProvider;
|
||||||
}
|
}
|
||||||
const workspace = await workspaceProvider.initWorkspace();
|
const workspace = await workspaceProvider.initWorkspace();
|
||||||
|
// Fix the URL hash
|
||||||
|
utils.setQueryParams(workspaceProvider.getWorkspaceParams(workspace));
|
||||||
|
|
||||||
store.dispatch('workspace/setCurrentWorkspaceId', workspace.id);
|
store.dispatch('workspace/setCurrentWorkspaceId', workspace.id);
|
||||||
await localDbSvc.init();
|
await localDbSvc.init();
|
||||||
|
|
||||||
// Try to find a suitable action provider
|
// Try to find a suitable action provider
|
||||||
actionProvider = providerRegistry.providers[utils.queryParams.providerId] || actionProvider;
|
actionProvider = providerRegistry.providersById[utils.queryParams.providerId] || actionProvider;
|
||||||
if (actionProvider && actionProvider.performAction) {
|
if (actionProvider && actionProvider.performAction) {
|
||||||
const newSyncLocation = await actionProvider.performAction();
|
const newSyncLocation = await actionProvider.performAction();
|
||||||
if (newSyncLocation) {
|
if (newSyncLocation) {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import cledit from './cledit';
|
import cledit from './editor/cledit';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import editorSvc from './editorSvc';
|
import editorSvc from './editorSvc';
|
||||||
import fileSvc from './fileSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
origin,
|
origin,
|
||||||
@ -31,7 +31,7 @@ export default {
|
|||||||
}
|
}
|
||||||
store.commit('setLight', true);
|
store.commit('setLight', true);
|
||||||
|
|
||||||
const file = await fileSvc.createFile({
|
const file = await workspaceSvc.createFile({
|
||||||
name: fileName || utils.getHostname(origin),
|
name: fileName || utils.getHostname(origin),
|
||||||
text: contentText || '\n',
|
text: contentText || '\n',
|
||||||
properties: contentProperties,
|
properties: contentProperties,
|
||||||
@ -58,7 +58,7 @@ export default {
|
|||||||
.splice(10)
|
.splice(10)
|
||||||
.forEach(([id]) => {
|
.forEach(([id]) => {
|
||||||
delete lastCreated[id];
|
delete lastCreated[id];
|
||||||
fileSvc.deleteFile(id);
|
workspaceSvc.deleteFile(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store file creations and open the file
|
// Store file creations and open the file
|
||||||
|
@ -170,6 +170,13 @@ export default {
|
|||||||
makeWorkspaceId(params) {
|
makeWorkspaceId(params) {
|
||||||
return Math.abs(this.hash(this.serializeObject(params))).toString(36);
|
return Math.abs(this.hash(this.serializeObject(params))).toString(36);
|
||||||
},
|
},
|
||||||
|
getDbName(workspaceId) {
|
||||||
|
let dbName = 'stackedit-db';
|
||||||
|
if (workspaceId !== 'main') {
|
||||||
|
dbName += `-${workspaceId}`;
|
||||||
|
}
|
||||||
|
return dbName;
|
||||||
|
},
|
||||||
encodeBase64(str, urlSafe = false) {
|
encodeBase64(str, urlSafe = false) {
|
||||||
const uriEncodedStr = encodeURIComponent(str);
|
const uriEncodedStr = encodeURIComponent(str);
|
||||||
const utf8Str = uriEncodedStr.replace(
|
const utf8Str = uriEncodedStr.replace(
|
||||||
@ -227,6 +234,12 @@ export default {
|
|||||||
};
|
};
|
||||||
return runWithNextValue();
|
return runWithNextValue();
|
||||||
},
|
},
|
||||||
|
async awaitSome(asyncFunc) {
|
||||||
|
if (await asyncFunc()) {
|
||||||
|
return this.awaitSome(asyncFunc);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
someResult(values, func) {
|
someResult(values, func) {
|
||||||
let result;
|
let result;
|
||||||
values.some((value) => {
|
values.some((value) => {
|
||||||
@ -279,6 +292,16 @@ export default {
|
|||||||
urlParser.href = url;
|
urlParser.href = url;
|
||||||
return urlParser.hostname;
|
return urlParser.hostname;
|
||||||
},
|
},
|
||||||
|
encodeUrlPath(path) {
|
||||||
|
return path ? path.split('/').map(encodeURIComponent).join('/') : '';
|
||||||
|
},
|
||||||
|
parseGithubRepoUrl(url) {
|
||||||
|
const parsedRepo = url && url.match(/([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||||
|
return parsedRepo && {
|
||||||
|
owner: parsedRepo[1],
|
||||||
|
repo: parsedRepo[2],
|
||||||
|
};
|
||||||
|
},
|
||||||
createHiddenIframe(url) {
|
createHiddenIframe(url) {
|
||||||
const iframeElt = document.createElement('iframe');
|
const iframeElt = document.createElement('iframe');
|
||||||
iframeElt.style.position = 'absolute';
|
iframeElt.style.position = 'absolute';
|
||||||
|
@ -4,6 +4,7 @@ import utils from './utils';
|
|||||||
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a file in the store with the specified fields.
|
* Create a file in the store with the specified fields.
|
||||||
*/
|
*/
|
||||||
@ -29,7 +30,7 @@ export default {
|
|||||||
discussions: discussions || {},
|
discussions: discussions || {},
|
||||||
comments: comments || {},
|
comments: comments || {},
|
||||||
};
|
};
|
||||||
const workspaceUniquePaths = store.getters['workspace/hasUniquePaths'];
|
const workspaceUniquePaths = store.getters['workspace/currentWorkspaceHasUniquePaths'];
|
||||||
|
|
||||||
// Show warning dialogs
|
// Show warning dialogs
|
||||||
if (!background) {
|
if (!background) {
|
||||||
@ -90,7 +91,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is a path conflict
|
// Check if there is a path conflict
|
||||||
if (store.getters['workspace/hasUniquePaths']) {
|
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
||||||
const parentPath = store.getters.pathsByItemId[item.parentId] || '';
|
const parentPath = store.getters.pathsByItemId[item.parentId] || '';
|
||||||
const path = parentPath + sanitizedName;
|
const path = parentPath + sanitizedName;
|
||||||
const items = store.getters.itemsByPath[path] || [];
|
const items = store.getters.itemsByPath[path] || [];
|
||||||
@ -133,7 +134,7 @@ export default {
|
|||||||
store.commit(`${item.type}/setItem`, item);
|
store.commit(`${item.type}/setItem`, item);
|
||||||
|
|
||||||
// Ensure path uniqueness
|
// Ensure path uniqueness
|
||||||
if (store.getters['workspace/hasUniquePaths']) {
|
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
||||||
this.makePathUnique(item.id);
|
this.makePathUnique(item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +165,7 @@ export default {
|
|||||||
* Ensure two files/folders don't have the same path if the workspace doesn't allow it.
|
* Ensure two files/folders don't have the same path if the workspace doesn't allow it.
|
||||||
*/
|
*/
|
||||||
ensureUniquePaths(idsToKeep = {}) {
|
ensureUniquePaths(idsToKeep = {}) {
|
||||||
if (store.getters['workspace/hasUniquePaths']) {
|
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
||||||
if (Object.keys(store.getters.pathsByItemId)
|
if (Object.keys(store.getters.pathsByItemId)
|
||||||
.some(id => !idsToKeep[id] && this.makePathUnique(id))
|
.some(id => !idsToKeep[id] && this.makePathUnique(id))
|
||||||
) {
|
) {
|
||||||
@ -207,4 +208,59 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addSyncLocation(location) {
|
||||||
|
store.commit('syncLocation/setItem', {
|
||||||
|
...location,
|
||||||
|
id: utils.uid(),
|
||||||
|
});
|
||||||
|
// Sanitize the workspace
|
||||||
|
this.ensureUniqueLocations();
|
||||||
|
},
|
||||||
|
|
||||||
|
addPublishLocation(location) {
|
||||||
|
store.commit('publishLocation/setItem', {
|
||||||
|
...location,
|
||||||
|
id: utils.uid(),
|
||||||
|
});
|
||||||
|
// Sanitize the workspace
|
||||||
|
this.ensureUniqueLocations();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure two sync/publish locations of the same file don't have the same hash.
|
||||||
|
*/
|
||||||
|
ensureUniqueLocations(idsToKeep = {}) {
|
||||||
|
['syncLocation', 'publishLocation'].forEach((type) => {
|
||||||
|
store.getters[`${type}/items`].forEach((item) => {
|
||||||
|
if (!idsToKeep[item.id]
|
||||||
|
&& store.getters[`${type}/groupedByFileIdAndHash`][item.fileId][item.hash].length > 1
|
||||||
|
) {
|
||||||
|
store.commit(`${item.type}/deleteItem`, item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop the database and clean the localStorage for the specified workspaceId.
|
||||||
|
*/
|
||||||
|
async removeWorkspace(id) {
|
||||||
|
// Remove from the store first as workspace tabs will reload.
|
||||||
|
// Workspace deletion will be persisted as soon as possible
|
||||||
|
// by the store.getters['data/workspaces'] watcher in localDbSvc.
|
||||||
|
store.dispatch('workspace/removeWorkspace', id);
|
||||||
|
|
||||||
|
// Drop the database
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const dbName = utils.getDbName(id);
|
||||||
|
const request = indexedDB.deleteDatabase(dbName);
|
||||||
|
request.onerror = resolve; // Ignore errors
|
||||||
|
request.onsuccess = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean the local storage
|
||||||
|
localStorage.removeItem(`${id}/lastSyncActivity`);
|
||||||
|
localStorage.removeItem(`${id}/lastWindowFocus`);
|
||||||
|
},
|
||||||
};
|
};
|
@ -2,7 +2,7 @@ import DiffMatchPatch from 'diff-match-patch';
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../data/emptyContent';
|
import empty from '../data/emptyContent';
|
||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
import cledit from '../services/cledit';
|
import cledit from '../services/editor/cledit';
|
||||||
|
|
||||||
const diffMatchPatch = new DiffMatchPatch();
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
|
||||||
|
@ -92,9 +92,6 @@ const tokenAdder = providerId => ({ getters, dispatch }, token) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// For workspaces
|
|
||||||
const urlParser = window.document.createElement('a');
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@ -126,25 +123,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
workspacesById: getter('workspaces'),
|
workspaces: getter('workspaces'), // Not to be used, prefer workspace/workspacesById
|
||||||
sanitizedWorkspacesById: (state, { workspacesById }, rootState, rootGetters) => {
|
|
||||||
const sanitizedWorkspacesById = {};
|
|
||||||
const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken'];
|
|
||||||
Object.entries(workspacesById).forEach(([id, workspace]) => {
|
|
||||||
const sanitizedWorkspace = {
|
|
||||||
id,
|
|
||||||
providerId: mainWorkspaceToken && 'googleDriveAppData',
|
|
||||||
sub: mainWorkspaceToken && mainWorkspaceToken.sub,
|
|
||||||
...workspace,
|
|
||||||
};
|
|
||||||
// Rebuild the url with current hostname
|
|
||||||
urlParser.href = workspace.url || 'app';
|
|
||||||
const params = utils.parseQueryParams(urlParser.hash.slice(1));
|
|
||||||
sanitizedWorkspace.url = utils.addQueryParams('app', params, true);
|
|
||||||
sanitizedWorkspacesById[id] = sanitizedWorkspace;
|
|
||||||
});
|
|
||||||
return sanitizedWorkspacesById;
|
|
||||||
},
|
|
||||||
settings: getter('settings'),
|
settings: getter('settings'),
|
||||||
computedSettings: (state, { settings }) => {
|
computedSettings: (state, { settings }) => {
|
||||||
const customSettings = yaml.safeLoad(settings);
|
const customSettings = yaml.safeLoad(settings);
|
||||||
@ -207,18 +186,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
syncDataByType: (state, { syncDataById }) => {
|
|
||||||
const result = {};
|
|
||||||
utils.types.forEach((type) => {
|
|
||||||
result[type] = {};
|
|
||||||
});
|
|
||||||
Object.entries(syncDataById).forEach(([, item]) => {
|
|
||||||
if (result[item.type]) {
|
|
||||||
result[item.type][item.itemId] = item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
dataSyncDataById: getter('dataSyncData'),
|
dataSyncDataById: getter('dataSyncData'),
|
||||||
tokensByProviderId: getter('tokens'),
|
tokensByProviderId: getter('tokens'),
|
||||||
googleTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.google || {},
|
googleTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.google || {},
|
||||||
@ -229,8 +196,6 @@ export default {
|
|||||||
zendeskTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.zendesk || {},
|
zendeskTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.zendesk || {},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setWorkspacesById: setter('workspaces'),
|
|
||||||
patchWorkspacesById: patcher('workspaces'),
|
|
||||||
setSettings: setter('settings'),
|
setSettings: setter('settings'),
|
||||||
patchLocalSettings: patcher('localSettings'),
|
patchLocalSettings: patcher('localSettings'),
|
||||||
patchLayoutSettings: patcher('layoutSettings'),
|
patchLayoutSettings: patcher('layoutSettings'),
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user