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 sponsorSvc from '../services/sponsorSvc';
|
||||
import tempFileSvc from '../services/tempFileSvc';
|
||||
import './common/globals';
|
||||
import './common/vueGlobals';
|
||||
|
||||
const themeClasses = {
|
||||
light: ['app--light'],
|
||||
@ -53,7 +53,7 @@ export default {
|
||||
this.ready = true;
|
||||
tempFileSvc.setReady();
|
||||
} catch (err) {
|
||||
if (err && err.message !== 'reload') {
|
||||
if (err && err.message !== 'RELOAD') {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
this.$store.dispatch('notification/error', err);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<script>
|
||||
import Prism from 'prismjs';
|
||||
import cledit from '../services/cledit';
|
||||
import cledit from '../services/editor/cledit';
|
||||
|
||||
export default {
|
||||
props: ['value', 'lang', 'disabled'],
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapActions } from 'vuex';
|
||||
import fileSvc from '../services/fileSvc';
|
||||
import workspaceSvc from '../services/workspaceSvc';
|
||||
import explorerSvc from '../services/explorerSvc';
|
||||
|
||||
export default {
|
||||
@ -102,10 +102,10 @@ export default {
|
||||
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
|
||||
try {
|
||||
if (newChildNode.isFolder) {
|
||||
const item = await fileSvc.storeItem(newChildNode.item);
|
||||
const item = await workspaceSvc.storeItem(newChildNode.item);
|
||||
this.select(item.id);
|
||||
} else {
|
||||
const item = await fileSvc.createFile(newChildNode.item);
|
||||
const item = await workspaceSvc.createFile(newChildNode.item);
|
||||
this.select(item.id);
|
||||
}
|
||||
} catch (e) {
|
||||
@ -120,7 +120,7 @@ export default {
|
||||
this.setEditingId(null);
|
||||
if (!cancel && item.id && value) {
|
||||
try {
|
||||
await fileSvc.storeItem({
|
||||
await workspaceSvc.storeItem({
|
||||
...item,
|
||||
name: value,
|
||||
});
|
||||
@ -147,7 +147,7 @@ export default {
|
||||
&& !targetNode.isNil
|
||||
&& sourceNode.item.id !== targetNode.item.id
|
||||
) {
|
||||
fileSvc.storeItem({
|
||||
workspaceSvc.storeItem({
|
||||
...sourceNode.item,
|
||||
parentId: targetNode.item.id,
|
||||
});
|
||||
|
@ -34,7 +34,7 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import cledit from '../services/cledit';
|
||||
import cledit from '../services/editor/cledit';
|
||||
import store from '../store';
|
||||
import EditorClassApplier from './common/EditorClassApplier';
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</div>
|
||||
@ -250,7 +250,7 @@ export default {
|
||||
}
|
||||
|
||||
.modal__sub-title {
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@ -278,8 +278,10 @@ export default {
|
||||
}
|
||||
|
||||
.modal__button-bar {
|
||||
margin-top: 1.75rem;
|
||||
text-align: right;
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.form-entry {
|
||||
|
@ -56,7 +56,7 @@ import tempFileSvc from '../services/tempFileSvc';
|
||||
import utils from '../services/utils';
|
||||
import pagedownButtons from '../data/pagedownButtons';
|
||||
import store from '../store';
|
||||
import fileSvc from '../services/fileSvc';
|
||||
import workspaceSvc from '../services/workspaceSvc';
|
||||
|
||||
// According to mousetrap
|
||||
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;
|
||||
if (title) {
|
||||
try {
|
||||
await fileSvc.storeItem({
|
||||
await workspaceSvc.storeItem({
|
||||
...this.$store.getters['file/current'],
|
||||
name: title,
|
||||
});
|
||||
|
@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="side-bar__inner">
|
||||
<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>
|
||||
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||
@ -75,7 +75,11 @@ export default {
|
||||
}),
|
||||
computed: {
|
||||
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() {
|
||||
return panelNames[this.panel];
|
||||
@ -164,7 +168,7 @@ export default {
|
||||
padding: 10px;
|
||||
margin: -10px -10px 10px;
|
||||
background-color: $info-bg;
|
||||
font-size: 0.95em;
|
||||
font-size: 0.9em;
|
||||
|
||||
p {
|
||||
margin: 10px;
|
||||
|
@ -64,7 +64,7 @@ export default {
|
||||
const updateMaskY = () => {
|
||||
const scrollPosition = editorSvc.getScrollPosition();
|
||||
if (scrollPosition) {
|
||||
const sectionDesc = editorSvc.previewCtx.sectionDescList[scrollPosition.sectionIdx];
|
||||
const sectionDesc = editorSvc.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
|
||||
this.maskY = sectionDesc.tocDimension.startOffset +
|
||||
(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 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 utils from '../../services/utils';
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Vue from 'vue';
|
||||
import Clipboard from 'clipboard';
|
||||
import timeSvc from '../../services/timeSvc';
|
||||
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', {
|
||||
bind(el, { value }) {
|
||||
el.title = value;
|
||||
el.setAttribute('aria-label', value);
|
||||
setElTitle(el, 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>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
|
||||
import editorSvc from '../../services/editorSvc';
|
||||
import animationSvc from '../../services/animationSvc';
|
||||
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||
@ -67,6 +67,9 @@ export default {
|
||||
...mapMutations('discussion', [
|
||||
'setCurrentDiscussionId',
|
||||
]),
|
||||
...mapActions('notification', [
|
||||
'info',
|
||||
]),
|
||||
goToDiscussion(discussionId = this.currentDiscussionId) {
|
||||
this.setCurrentDiscussionId(discussionId);
|
||||
const layoutSettings = this.$store.getters['data/layoutSettings'];
|
||||
@ -75,7 +78,7 @@ export default {
|
||||
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
||||
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
||||
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 {
|
||||
const scrollerElt = layoutSettings.showEditor
|
||||
? editorSvc.editorElt.parentNode
|
||||
|
@ -24,7 +24,7 @@
|
||||
import { mapGetters, mapMutations, mapActions } from 'vuex';
|
||||
import Prism from 'prismjs';
|
||||
import UserImage from '../UserImage';
|
||||
import cledit from '../../services/cledit';
|
||||
import cledit from '../../services/editor/cledit';
|
||||
import editorSvc from '../../services/editorSvc';
|
||||
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||
import utils from '../../services/utils';
|
||||
|
@ -12,18 +12,19 @@
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="exportPdf">
|
||||
<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>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="exportPandoc">
|
||||
<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>
|
||||
</menu-entry>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
|
||||
@ -31,6 +32,7 @@ export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
},
|
||||
computed: mapGetters(['isSponsor']),
|
||||
methods: {
|
||||
exportMarkdown() {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
|
@ -169,7 +169,7 @@ export default {
|
||||
created() {
|
||||
// Find the workspace provider
|
||||
const workspace = this.$store.getters['workspace/currentWorkspace'];
|
||||
this.workspaceProvider = providerRegistry.providers[workspace.providerId];
|
||||
this.workspaceProvider = providerRegistry.providersById[workspace.providerId];
|
||||
|
||||
// Watch file changes
|
||||
this.$watch(
|
||||
|
@ -29,7 +29,7 @@ import htmlSanitizer from '../../libs/htmlSanitizer';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import Provider from '../../services/providers/common/Provider';
|
||||
import store from '../../store';
|
||||
import fileSvc from '../../services/fileSvc';
|
||||
import workspaceSvc from '../../services/workspaceSvc';
|
||||
|
||||
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
||||
|
||||
@ -56,7 +56,7 @@ export default {
|
||||
async onImportMarkdown(evt) {
|
||||
const file = evt.target.files[0];
|
||||
const content = await readFile(file);
|
||||
const item = await fileSvc.createFile({
|
||||
const item = await workspaceSvc.createFile({
|
||||
...Provider.parseContent(content),
|
||||
name: file.name,
|
||||
});
|
||||
@ -67,7 +67,7 @@ export default {
|
||||
const content = await readFile(file);
|
||||
const sanitizedContent = htmlSanitizer.sanitizeHtml(content)
|
||||
.replace(/ /g, ' '); // Replace non-breaking spaces with classic spaces
|
||||
const item = await fileSvc.createFile({
|
||||
const item = await workspaceSvc.createFile({
|
||||
...Provider.parseContent(turndownService.turndown(sanitizedContent)),
|
||||
name: file.name,
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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__icon menu-entry__icon--image">
|
||||
<user-image :user-id="userId"></user-image>
|
||||
@ -11,7 +11,15 @@
|
||||
<div class="menu-entry__icon menu-entry__icon--image">
|
||||
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
|
||||
</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 class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
|
||||
<div class="menu-entry__icon menu-entry__icon--disabled">
|
||||
@ -27,7 +35,7 @@
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('workspaces')">
|
||||
<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>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
@ -83,6 +91,7 @@
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import providerRegistry from '../../services/providers/common/providerRegistry';
|
||||
import UserImage from '../UserImage';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
@ -99,6 +108,10 @@ export default {
|
||||
'loginToken',
|
||||
'userId',
|
||||
]),
|
||||
currentWorkspaceUrl() {
|
||||
const provider = providerRegistry.providersById[this.currentWorkspace.providerId];
|
||||
return provider.getWorkspaceLocationUrl(this.currentWorkspace);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', {
|
||||
|
@ -16,7 +16,7 @@
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="managePublish">
|
||||
<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>
|
||||
</menu-entry>
|
||||
</div>
|
||||
@ -113,8 +113,7 @@ import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
||||
import publishSvc from '../../services/publishSvc';
|
||||
import store from '../../store';
|
||||
|
||||
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
|
||||
.map(sub => tokens[sub])
|
||||
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
||||
.filter(token => filter(token))
|
||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||
|
||||
@ -142,6 +141,9 @@ export default {
|
||||
...mapGetters('publishLocation', {
|
||||
publishLocations: 'current',
|
||||
}),
|
||||
locationCount() {
|
||||
return Object.keys(this.publishLocations).length;
|
||||
},
|
||||
currentFileName() {
|
||||
return this.$store.getters['file/current'].name;
|
||||
},
|
||||
@ -178,8 +180,10 @@ export default {
|
||||
publishSvc.requestPublish();
|
||||
}
|
||||
},
|
||||
managePublish() {
|
||||
return this.$store.dispatch('modal/open', 'publishManagement');
|
||||
async managePublish() {
|
||||
try {
|
||||
await this.$store.dispatch('modal/open', 'publishManagement');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGoogleDriveAccount() {
|
||||
try {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||
<p><b>{{currentFileName}}</b> can not be 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 v-else>
|
||||
<div class="side-bar__info" v-if="noToken">
|
||||
@ -16,7 +16,7 @@
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="manageSync">
|
||||
<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>
|
||||
</menu-entry>
|
||||
</div>
|
||||
@ -91,8 +91,7 @@ import githubProvider from '../../services/providers/githubProvider';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import store from '../../store';
|
||||
|
||||
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
|
||||
.map(sub => tokens[sub])
|
||||
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
||||
.filter(token => filter(token))
|
||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||
|
||||
@ -116,8 +115,11 @@ export default {
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('syncLocation', {
|
||||
syncLocations: 'current',
|
||||
syncLocations: 'currentWithWorkspaceSyncLocation',
|
||||
}),
|
||||
locationCount() {
|
||||
return Object.keys(this.syncLocations).length;
|
||||
},
|
||||
currentFileName() {
|
||||
return this.$store.getters['file/current'].name;
|
||||
},
|
||||
@ -142,8 +144,10 @@ export default {
|
||||
syncSvc.requestSync();
|
||||
}
|
||||
},
|
||||
manageSync() {
|
||||
return this.$store.dispatch('modal/open', 'syncManagement');
|
||||
async manageSync() {
|
||||
try {
|
||||
await this.$store.dispatch('modal/open', 'syncManagement');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGoogleDriveAccount() {
|
||||
try {
|
||||
@ -180,16 +184,12 @@ export default {
|
||||
async saveGoogleDrive(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'googleDriveSave');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveDropbox(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'dropboxSave');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGithub(token) {
|
||||
try {
|
||||
@ -201,23 +201,17 @@ export default {
|
||||
'queue/enqueue',
|
||||
() => githubProvider.openFile(token, syncLocation),
|
||||
);
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveGithub(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'githubSave');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveGist(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'gistSync');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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">
|
||||
<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>
|
||||
@ -9,19 +9,22 @@
|
||||
<hr>
|
||||
<menu-entry @click.native="addCouchdbWorkspace">
|
||||
<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 @click.native="addGithubWorkspace">
|
||||
<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 @click.native="addGoogleDriveWorkspace">
|
||||
<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 @click.native="manageWorkspaces">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
@ -36,12 +39,13 @@ export default {
|
||||
MenuEntry,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('data', [
|
||||
'sanitizedWorkspaceById',
|
||||
]),
|
||||
...mapGetters('workspace', [
|
||||
'workspacesById',
|
||||
'currentWorkspace',
|
||||
]),
|
||||
workspaceCount() {
|
||||
return Object.keys(this.workspacesById).length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async addCouchdbWorkspace() {
|
||||
|
@ -24,9 +24,13 @@
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
line-height: 1.3;
|
||||
|
||||
.menu-entry__label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
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 {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
@ -70,10 +68,22 @@
|
||||
float: right;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
padding: 0.05em 0.25em;
|
||||
background-color: darken($error-color, 10);
|
||||
line-height: 1;
|
||||
padding: 0.15em 0.25em;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.menu-entry__label--warning {
|
||||
color: #fff;
|
||||
background-color: darken($error-color, 10);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-entry__label--count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.menu-entry__text {
|
||||
|
@ -24,7 +24,7 @@
|
||||
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.resolve()">Close</button>
|
||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -78,7 +78,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -14,15 +14,15 @@
|
||||
</form-entry>
|
||||
</div>
|
||||
<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="resolve()">Ok</button>
|
||||
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Clipboard from 'clipboard';
|
||||
import { mapActions } from 'vuex';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
|
||||
@ -48,14 +48,11 @@ export default modalTemplate({
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
this.clipboard = new Clipboard(this.$el.querySelector('.button--copy'), {
|
||||
text: () => this.result,
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
this.clipboard.destroy();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('notification', [
|
||||
'info',
|
||||
]),
|
||||
resolve() {
|
||||
const { config } = this;
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
|
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -36,9 +36,8 @@ export default modalTemplate({
|
||||
}),
|
||||
computed: {
|
||||
googlePhotosTokens() {
|
||||
const googleTokens = this.$store.getters['data/googleTokensBySub'];
|
||||
return Object.entries(googleTokens)
|
||||
.map(([, token]) => token)
|
||||
const googleTokensBySub = this.$store.getters['data/googleTokensBySub'];
|
||||
return Object.values(googleTokensBySub)
|
||||
.filter(token => token.isPhotos)
|
||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||
},
|
||||
|
@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations">
|
||||
<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-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||
<div>
|
||||
@ -12,21 +15,21 @@
|
||||
{{location.description}}
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 class="modal__button-bar">
|
||||
<button class="button" @click="config.resolve()">Close</button>
|
||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -1,38 +1,53 @@
|
||||
<template>
|
||||
<modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations">
|
||||
<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-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
||||
<div>
|
||||
<div class="sync-entry flex flex--row flex--align-center" v-for="location in syncLocations" :key="location.id">
|
||||
<div class="sync-entry__icon flex flex--column flex--center">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
<div class="sync-entry flex flex--column" v-for="location in syncLocations" :key="location.id">
|
||||
<div class="sync-entry__header flex flex--row flex--align-center">
|
||||
<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 class="sync-entry__description">
|
||||
{{location.description}}
|
||||
</div>
|
||||
<div class="sync-entry__buttons flex flex--row flex--center">
|
||||
<a class="sync-entry__button button" :href="location.url" target="_blank">
|
||||
<icon-open-in-new></icon-open-in-new>
|
||||
</a>
|
||||
<button class="sync-entry__button button" @click="remove(location)">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
<div class="sync-entry__row flex flex--row flex--align-center">
|
||||
<div class="sync-entry__url">
|
||||
{{location.url || 'Workspace location'}}
|
||||
</div>
|
||||
<div class="sync-entry__buttons flex flex--row flex--center" v-if="location.url">
|
||||
<button class="sync-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'Copy URL'">
|
||||
<icon-content-copy></icon-content-copy>
|
||||
</button>
|
||||
<a class="sync-entry__button button" v-if="location.url" :href="location.url" target="_blank" v-title="'Open location'">
|
||||
<icon-open-in-new></icon-open-in-new>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 class="modal__button-bar">
|
||||
<button class="button" @click="config.resolve()">Close</button>
|
||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
|
||||
export default {
|
||||
@ -44,15 +59,22 @@ export default {
|
||||
'config',
|
||||
]),
|
||||
...mapGetters('syncLocation', {
|
||||
syncLocations: 'current',
|
||||
syncLocations: 'currentWithWorkspaceSyncLocation',
|
||||
}),
|
||||
currentFileName() {
|
||||
return this.$store.getters['file/current'].name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('notification', [
|
||||
'info',
|
||||
]),
|
||||
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 {
|
||||
padding: 0.5rem 0.25rem;
|
||||
border-bottom: 1px solid $hr-color;
|
||||
margin: 1.5em 0;
|
||||
height: auto;
|
||||
font-size: 17px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
$button-size: 30px;
|
||||
$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 {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
margin-right: 0.75rem;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.sync-entry__description {
|
||||
opacity: 0.5;
|
||||
line-height: 1.4;
|
||||
font-size: 0.9em;
|
||||
width: 100%;
|
||||
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 {
|
||||
margin-left: 0.75rem;
|
||||
|
||||
.sync-entry__row & {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sync-entry__button {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 6px;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
|
||||
.sync-entry__row & {
|
||||
width: $small-button-size;
|
||||
height: $small-button-size;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -1,39 +1,69 @@
|
||||
<template>
|
||||
<modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces">
|
||||
<div class="modal__content">
|
||||
<div class="workspace-entry flex flex--row flex--align-center" v-for="(workspace, id) in sanitizedWorkspacesById" :key="id">
|
||||
<div class="workspace-entry__icon flex flex--column flex--center">
|
||||
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
||||
</div>
|
||||
<div class="workspace-entry__description flex flex--column">
|
||||
<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 class="modal__image">
|
||||
<icon-database></icon-database>
|
||||
</div>
|
||||
<p>The following workspaces are locally available:</p>
|
||||
<div class="workspace-entry flex flex--column" v-for="(workspace, id) in workspacesById" :key="id">
|
||||
<div class="flex flex--column">
|
||||
<div class="workspace-entry__header flex flex--row flex--align-center">
|
||||
<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 class="workspace-entry__url">
|
||||
{{workspace.url}}
|
||||
<div class="workspace-entry__row flex flex--row flex--align-center">
|
||||
<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 class="workspace-entry__buttons flex flex--row flex--center">
|
||||
<button class="workspace-entry__button button" @click="edit(id)">
|
||||
<icon-pen></icon-pen>
|
||||
</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 class="modal__info">
|
||||
<b>ProTip:</b> Workspaces are accessible <b>offline</b> after their first use.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.resolve()">Close</button>
|
||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import localDbSvc from '../../services/localDbSvc';
|
||||
import workspaceSvc from '../../services/workspaceSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -47,40 +77,46 @@ export default {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'workspacesById',
|
||||
'sanitizedWorkspacesById',
|
||||
]),
|
||||
...mapGetters('workspace', [
|
||||
'workspacesById',
|
||||
'mainWorkspace',
|
||||
'currentWorkspace',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('notification', [
|
||||
'info',
|
||||
]),
|
||||
edit(id) {
|
||||
this.editedId = id;
|
||||
this.editingName = this.workspacesById[id].name;
|
||||
},
|
||||
submitEdit(cancel) {
|
||||
const workspace = this.workspacesById[this.editedId];
|
||||
if (workspace && !cancel && this.editingName) {
|
||||
this.$store.dispatch('data/patchWorkspacesById', {
|
||||
[this.editedId]: {
|
||||
...workspace,
|
||||
name: this.editingName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.editingName = workspace.name;
|
||||
if (workspace) {
|
||||
if (!cancel && this.editingName) {
|
||||
this.$store.dispatch('workspace/patchWorkspacesById', {
|
||||
[this.editedId]: {
|
||||
...workspace,
|
||||
name: this.editingName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.editingName = workspace.name;
|
||||
}
|
||||
}
|
||||
this.editedId = null;
|
||||
},
|
||||
async remove(id) {
|
||||
try {
|
||||
await this.$store.dispatch('modal/open', 'removeWorkspace');
|
||||
localDbSvc.removeWorkspace(id);
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
if (id === this.mainWorkspace.id) {
|
||||
this.info('Your main workspace can not be removed.');
|
||||
} else if (id === this.currentWorkspace.id) {
|
||||
this.info('Please close the workspace before removing it.');
|
||||
} else {
|
||||
try {
|
||||
await this.$store.dispatch('modal/open', 'removeWorkspace');
|
||||
workspaceSvc.removeWorkspace(id);
|
||||
} catch (e) { /* Cancel */ }
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -95,58 +131,78 @@ export default {
|
||||
}
|
||||
|
||||
.workspace-entry {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
margin: 15px 0;
|
||||
margin: 1.75em 0;
|
||||
height: auto;
|
||||
font-size: 17px;
|
||||
line-height: 1.5;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
$button-size: 30px;
|
||||
$small-button-size: 22px;
|
||||
|
||||
span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
.workspace-entry__header {
|
||||
line-height: $button-size;
|
||||
|
||||
.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 {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 12px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
margin-right: 0.75rem;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.workspace-entry__description {
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workspace-entry__name {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.workspace-entry__url {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.5;
|
||||
font-size: 0.75em;
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
.workspace-entry__buttons {
|
||||
margin-left: 0.75rem;
|
||||
|
||||
.workspace-entry__row & {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-entry__button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 6px;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
|
||||
.workspace-entry__row & {
|
||||
width: $small-button-size;
|
||||
height: $small-button-size;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
|
@ -30,7 +30,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -4,20 +4,20 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="couchdb"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> https://instance.smileupps.com/stackedit-workspace
|
||||
</div>
|
||||
<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>
|
||||
</form-entry>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="gist"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
||||
</form-entry>
|
||||
@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -34,6 +34,7 @@
|
||||
<script>
|
||||
import githubProvider from '../../../services/providers/githubProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
import utils from '../../../services/utils';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
@ -52,7 +53,7 @@ export default modalTemplate({
|
||||
this.setError('path');
|
||||
}
|
||||
if (this.repoUrl && this.path) {
|
||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
||||
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
|
||||
if (!parsedRepo) {
|
||||
this.setError('repoUrl');
|
||||
} else {
|
||||
|
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -35,6 +35,7 @@
|
||||
<script>
|
||||
import githubProvider from '../../../services/providers/githubProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
import utils from '../../../services/utils';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
@ -49,7 +50,7 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
||||
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
|
||||
if (!parsedRepo) {
|
||||
this.setError('repoUrl');
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
@ -26,13 +26,12 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import githubProvider from '../../../services/providers/githubProvider';
|
||||
import utils from '../../../services/utils';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
@ -46,14 +45,14 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
||||
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
|
||||
if (!parsedRepo) {
|
||||
this.setError('repoUrl');
|
||||
} else {
|
||||
const path = this.path && this.path.replace(/^\//, '');
|
||||
const url = utils.addQueryParams('app', {
|
||||
...parsedRepo,
|
||||
providerId: 'githubWorkspace',
|
||||
repo: `${parsedRepo.owner}/${parsedRepo.repo}`,
|
||||
branch: this.branch || 'master',
|
||||
path: path || undefined,
|
||||
}, true);
|
||||
|
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -48,7 +48,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -73,9 +73,11 @@ export default modalTemplate({
|
||||
'modal/hideUntil',
|
||||
googleHelper.openPicker(this.config.token, 'folder')
|
||||
.then((folders) => {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveFolderId: folders[0].id,
|
||||
});
|
||||
if (folders[0]) {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveFolderId: folders[0].id,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -46,9 +46,11 @@ export default modalTemplate({
|
||||
'modal/hideUntil',
|
||||
googleHelper.openPicker(this.config.token, 'folder')
|
||||
.then((folders) => {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveFolderId: folders[0].id,
|
||||
});
|
||||
if (folders[0]) {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveFolderId: folders[0].id,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||
</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">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -37,9 +37,11 @@ export default modalTemplate({
|
||||
'modal/hideUntil',
|
||||
googleHelper.openPicker(this.config.token, 'folder')
|
||||
.then((folders) => {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveWorkspaceFolderId: folders[0].id,
|
||||
});
|
||||
if (folders[0]) {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveWorkspaceFolderId: folders[0].id,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
<button class="button button--resolve" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
export default () => ({
|
||||
main: {
|
||||
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.
|
||||
|
||||
**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?**
|
||||
|
||||
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 FormatListChecks from './FormatListChecks';
|
||||
import CloseCircle from './CloseCircle';
|
||||
import ContentCopy from './ContentCopy';
|
||||
|
||||
Vue.component('iconProvider', Provider);
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
@ -102,3 +103,4 @@ Vue.component('iconDatabase', Database);
|
||||
Vue.component('iconMagnify', Magnify);
|
||||
Vue.component('iconFormatListChecks', FormatListChecks);
|
||||
Vue.component('iconCloseCircle', CloseCircle);
|
||||
Vue.component('iconContentCopy', ContentCopy);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import fileSvc from './fileSvc';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
import utils from './utils';
|
||||
|
||||
export default {
|
||||
@ -51,7 +51,7 @@ export default {
|
||||
|
||||
await utils.awaitSequence(
|
||||
Object.keys(folderNameMap),
|
||||
async externalId => fileSvc.setOrPatchItem({
|
||||
async externalId => workspaceSvc.setOrPatchItem({
|
||||
id: folderIdMap[externalId],
|
||||
type: 'folder',
|
||||
name: folderNameMap[externalId],
|
||||
@ -61,7 +61,7 @@ export default {
|
||||
|
||||
await utils.awaitSequence(
|
||||
Object.keys(fileNameMap),
|
||||
async externalId => fileSvc.createFile({
|
||||
async externalId => workspaceSvc.createFile({
|
||||
name: fileNameMap[externalId],
|
||||
parentId: folderIdMap[parentIdMap[externalId]],
|
||||
text: textMap[externalId],
|
||||
|
@ -1,7 +1,7 @@
|
||||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import TurndownService from 'turndown/lib/turndown.browser.umd';
|
||||
import htmlSanitizer from '../../libs/htmlSanitizer';
|
||||
import store from '../../store';
|
||||
import htmlSanitizer from '../../../libs/htmlSanitizer';
|
||||
import store from '../../../store';
|
||||
|
||||
function cledit(contentElt, scrollEltOpt, isMarkdown = false) {
|
||||
const scrollElt = scrollEltOpt || contentElt;
|
@ -29,10 +29,14 @@ function SelectionMgr(editor) {
|
||||
|
||||
this.createRange = (start, end) => {
|
||||
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;
|
||||
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.setEnd(endContainer.container, endContainer.offsetInContainer);
|
@ -1,4 +1,4 @@
|
||||
import '../../libs/clunderscore';
|
||||
import '../../../libs/clunderscore';
|
||||
import cledit from './cleditCore';
|
||||
import './cleditHighlighter';
|
||||
import './cleditKeystroke';
|
@ -1,10 +1,10 @@
|
||||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import cledit from './cledit';
|
||||
import utils from './utils';
|
||||
import diffUtils from './diffUtils';
|
||||
import store from '../store';
|
||||
import EditorClassApplier from '../components/common/EditorClassApplier';
|
||||
import PreviewClassApplier from '../components/common/PreviewClassApplier';
|
||||
import utils from '../utils';
|
||||
import diffUtils from '../diffUtils';
|
||||
import store from '../../store';
|
||||
import EditorClassApplier from '../../components/common/EditorClassApplier';
|
||||
import PreviewClassApplier from '../../components/common/PreviewClassApplier';
|
||||
|
||||
let clEditor;
|
||||
// let discussionIds = {};
|
@ -1,7 +1,7 @@
|
||||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import cledit from './cledit';
|
||||
import animationSvc from './animationSvc';
|
||||
import store from '../store';
|
||||
import animationSvc from '../animationSvc';
|
||||
import store from '../../store';
|
||||
|
||||
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 Prism from 'prismjs';
|
||||
import markdownItPandocRenderer from 'markdown-it-pandoc-renderer';
|
||||
import cledit from './cledit';
|
||||
import cledit from './editor/cledit';
|
||||
import pagedown from '../libs/pagedown';
|
||||
import htmlSanitizer from '../libs/htmlSanitizer';
|
||||
import markdownConversionSvc from './markdownConversionSvc';
|
||||
import markdownGrammarSvc from './markdownGrammarSvc';
|
||||
import sectionUtils from './sectionUtils';
|
||||
import sectionUtils from './editor/sectionUtils';
|
||||
import extensionSvc from './extensionSvc';
|
||||
import editorSvcDiscussions from './editorSvcDiscussions';
|
||||
import editorSvcUtils from './editorSvcUtils';
|
||||
import editorSvcDiscussions from './editor/editorSvcDiscussions';
|
||||
import editorSvcUtils from './editor/editorSvcUtils';
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import store from '../store';
|
||||
import fileSvc from './fileSvc';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
|
||||
export default {
|
||||
newItem(isFolder = false) {
|
||||
@ -59,7 +59,7 @@ export default {
|
||||
parentId: 'trash',
|
||||
});
|
||||
} else {
|
||||
fileSvc.deleteFile(id);
|
||||
workspaceSvc.deleteFile(id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ import FileSaver from 'file-saver';
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
import welcomeFile from '../data/welcomeFile.md';
|
||||
import fileSvc from './fileSvc';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
|
||||
const dbVersion = 1;
|
||||
const dbStoreName = 'objects';
|
||||
@ -12,21 +12,13 @@ const resetApp = utils.queryParams.reset;
|
||||
const deleteMarkerMaxAge = 1000;
|
||||
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 {
|
||||
constructor() {
|
||||
this.getTxCbs = [];
|
||||
|
||||
// Make the DB name
|
||||
const workspaceId = store.getters['workspace/currentWorkspace'].id;
|
||||
this.dbName = getDbName(workspaceId);
|
||||
this.dbName = utils.getDbName(workspaceId);
|
||||
|
||||
// Init connection
|
||||
const request = indexedDB.open(this.dbName, dbVersion);
|
||||
@ -142,9 +134,12 @@ const localDbSvc = {
|
||||
this.connection.createTx((tx) => {
|
||||
// Look for DB changes and apply them to the store
|
||||
this.readAll(tx, (storeItemMap) => {
|
||||
// Sanitize the workspace
|
||||
workspaceSvc.ensureUniquePaths();
|
||||
workspaceSvc.ensureUniqueLocations();
|
||||
// Persist all the store changes into the DB
|
||||
this.writeAll(storeItemMap, tx);
|
||||
// Sync localStorage
|
||||
// Sync the localStorage
|
||||
this.syncLocalStorage();
|
||||
// Done
|
||||
resolve();
|
||||
@ -177,19 +172,21 @@ const localDbSvc = {
|
||||
// Collect change
|
||||
changes.push(item);
|
||||
cursor.continue();
|
||||
} else {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.
|
||||
*/
|
||||
async init() {
|
||||
// Reset the app if reset flag was passed
|
||||
if (resetApp) {
|
||||
await Promise.all(Object.keys(store.getters['data/workspacesById'])
|
||||
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)));
|
||||
await Promise.all(Object.keys(store.getters['workspace/workspacesById'])
|
||||
.map(workspaceId => workspaceSvc.removeWorkspace(workspaceId)));
|
||||
utils.localStorageDataIds.forEach((id) => {
|
||||
// Clean data stored in localStorage
|
||||
localStorage.removeItem(`data/${id}`);
|
||||
});
|
||||
window.location.reload();
|
||||
throw new Error('reload');
|
||||
throw new Error('RELOAD');
|
||||
}
|
||||
|
||||
// Create the connection
|
||||
@ -374,6 +351,13 @@ const localDbSvc = {
|
||||
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
|
||||
const hash = utils.hash(welcomeFile);
|
||||
const { welcomeFileHashes } = store.getters['data/localSettings'];
|
||||
@ -393,7 +377,7 @@ const localDbSvc = {
|
||||
// Clean files
|
||||
store.getters['file/items']
|
||||
.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
|
||||
@ -429,7 +413,7 @@ const localDbSvc = {
|
||||
store.commit('file/setCurrentId', recentFile.id);
|
||||
} else {
|
||||
// If still no ID, create a new file
|
||||
const newFile = await fileSvc.createFile({
|
||||
const newFile = await workspaceSvc.createFile({
|
||||
name: 'Welcome file',
|
||||
text: welcomeFile,
|
||||
}, true);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import cledit from '../cledit';
|
||||
import cledit from '../editor/cledit';
|
||||
import editorSvc from '../editorSvc';
|
||||
import store from '../../store';
|
||||
|
||||
|
@ -4,16 +4,15 @@ import Provider from './common/Provider';
|
||||
|
||||
export default new Provider({
|
||||
id: 'bloggerPage',
|
||||
getToken(location) {
|
||||
const token = store.getters['data/googleTokensBySub'][location.sub];
|
||||
getToken({ sub }) {
|
||||
const token = store.getters['data/googleTokensBySub'][sub];
|
||||
return token && token.isBlogger ? token : null;
|
||||
},
|
||||
getUrl(location) {
|
||||
return `https://www.blogger.com/blogger.g?blogID=${location.blogId}#editor/target=page;pageID=${location.pageId}`;
|
||||
getLocationUrl({ blogId, pageId }) {
|
||||
return `https://www.blogger.com/blogger.g?blogID=${blogId}#editor/target=page;pageID=${pageId}`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.pageId} — ${location.blogUrl} — ${token.name}`;
|
||||
getLocationDescription({ pageId }) {
|
||||
return pageId;
|
||||
},
|
||||
async publish(token, html, metadata, publishLocation) {
|
||||
const page = await googleHelper.uploadBlogger({
|
||||
|
@ -4,16 +4,15 @@ import Provider from './common/Provider';
|
||||
|
||||
export default new Provider({
|
||||
id: 'blogger',
|
||||
getToken(location) {
|
||||
const token = store.getters['data/googleTokensBySub'][location.sub];
|
||||
getToken({ sub }) {
|
||||
const token = store.getters['data/googleTokensBySub'][sub];
|
||||
return token && token.isBlogger ? token : null;
|
||||
},
|
||||
getUrl(location) {
|
||||
return `https://www.blogger.com/blogger.g?blogID=${location.blogId}#editor/target=post;postID=${location.postId}`;
|
||||
getLocationUrl({ blogId, postId }) {
|
||||
return `https://www.blogger.com/blogger.g?blogID=${blogId}#editor/target=post;postID=${postId}`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.postId} — ${location.blogUrl} — ${token.name}`;
|
||||
getLocationDescription({ postId }) {
|
||||
return postId;
|
||||
},
|
||||
async publish(token, html, metadata, publishLocation) {
|
||||
const post = await googleHelper.uploadBlogger({
|
||||
|
@ -2,7 +2,7 @@ import providerRegistry from './providerRegistry';
|
||||
import emptyContent from '../../../data/emptyContent';
|
||||
import utils from '../../utils';
|
||||
import store from '../../../store';
|
||||
import fileSvc from '../../fileSvc';
|
||||
import workspaceSvc from '../../workspaceSvc';
|
||||
|
||||
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
|
||||
*/
|
||||
static openFileWithLocation(allLocations, criteria) {
|
||||
const location = utils.search(allLocations, criteria);
|
||||
static openFileWithLocation(criteria) {
|
||||
const location = utils.search(store.getters['syncLocation/items'], criteria);
|
||||
if (location) {
|
||||
// Found one, open it if it exists
|
||||
const item = store.state.file.itemsById[location.fileId];
|
||||
@ -90,7 +90,7 @@ export default class Provider {
|
||||
store.commit('file/setCurrentId', item.id);
|
||||
// If file is in the trash, restore it
|
||||
if (item.parentId === 'trash') {
|
||||
fileSvc.setOrPatchItem({
|
||||
workspaceSvc.setOrPatchItem({
|
||||
...item,
|
||||
parentId: null,
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default {
|
||||
providers: {},
|
||||
providersById: {},
|
||||
register(provider) {
|
||||
this.providers[provider.id] = provider;
|
||||
this.providersById[provider.id] = provider;
|
||||
return provider;
|
||||
},
|
||||
};
|
||||
|
@ -10,56 +10,54 @@ export default new Provider({
|
||||
getToken() {
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
async initWorkspace() {
|
||||
const dbUrl = (utils.queryParams.dbUrl || '').replace(/\/?$/, ''); // Remove trailing /
|
||||
const workspaceParams = {
|
||||
getWorkspaceParams({ dbUrl }) {
|
||||
return {
|
||||
providerId: this.id,
|
||||
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 getToken = () => store.getters['data/couchdbTokensBySub'][workspaceId];
|
||||
const getWorkspace = () => store.getters['data/sanitizedWorkspacesById'][workspaceId];
|
||||
|
||||
if (!getToken()) {
|
||||
// Create token
|
||||
// Create the token if it doesn't exist
|
||||
if (!store.getters['data/couchdbTokensBySub'][workspaceId]) {
|
||||
store.dispatch('data/addCouchdbToken', {
|
||||
sub: workspaceId,
|
||||
dbUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// Create the workspace
|
||||
let workspace = getWorkspace();
|
||||
if (!workspace) {
|
||||
// Make sure the database exists and retrieve its name
|
||||
let db;
|
||||
// Create the workspace if it doesn't exist
|
||||
if (!store.getters['workspace/workspacesById'][workspaceId]) {
|
||||
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) {
|
||||
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
|
||||
utils.setQueryParams(workspaceParams);
|
||||
if (workspace.url !== window.location.href) {
|
||||
store.dispatch('data/patchWorkspacesById', {
|
||||
[workspace.id]: {
|
||||
...workspace,
|
||||
url: window.location.href,
|
||||
},
|
||||
});
|
||||
}
|
||||
return getWorkspace();
|
||||
return store.getters['workspace/workspacesById'][workspaceId];
|
||||
},
|
||||
async getChanges() {
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
|
@ -2,7 +2,7 @@ import store from '../../store';
|
||||
import dropboxHelper from './helpers/dropboxHelper';
|
||||
import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import fileSvc from '../fileSvc';
|
||||
import workspaceSvc from '../workspaceSvc';
|
||||
|
||||
const makePathAbsolute = (token, path) => {
|
||||
if (!token.fullAccess) {
|
||||
@ -19,17 +19,16 @@ const makePathRelative = (token, path) => {
|
||||
|
||||
export default new Provider({
|
||||
id: 'dropbox',
|
||||
getToken(location) {
|
||||
return store.getters['data/dropboxTokensBySub'][location.sub];
|
||||
getToken({ sub }) {
|
||||
return store.getters['data/dropboxTokensBySub'][sub];
|
||||
},
|
||||
getUrl(location) {
|
||||
const pathComponents = location.path.split('/').map(encodeURIComponent);
|
||||
getLocationUrl({ path }) {
|
||||
const pathComponents = path.split('/').map(encodeURIComponent);
|
||||
const filename = pathComponents.pop();
|
||||
return `https://www.dropbox.com/home${pathComponents.join('/')}?preview=${filename}`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.path} — ${location.dropboxFileId} — ${token.name}`;
|
||||
getLocationDescription({ path }) {
|
||||
return path;
|
||||
},
|
||||
checkPath(path) {
|
||||
return path && path.match(/^\/[^\\<>:"|?*]+$/);
|
||||
@ -71,7 +70,7 @@ export default new Provider({
|
||||
async openFiles(token, paths) {
|
||||
await utils.awaitSequence(paths, async (path) => {
|
||||
// Check if the file exists and open it
|
||||
if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], {
|
||||
if (!Provider.openFileWithLocation({
|
||||
providerId: this.id,
|
||||
path,
|
||||
})) {
|
||||
@ -99,7 +98,7 @@ export default new Provider({
|
||||
if (dotPos > 0 && slashPos < name.length) {
|
||||
name = name.slice(0, dotPos);
|
||||
}
|
||||
const item = await fileSvc.createFile({
|
||||
const item = await workspaceSvc.createFile({
|
||||
name,
|
||||
parentId: store.getters['file/current'].parentId,
|
||||
text: content.text,
|
||||
@ -108,9 +107,8 @@ export default new Provider({
|
||||
comments: content.comments,
|
||||
}, true);
|
||||
store.commit('file/setCurrentId', item.id);
|
||||
store.commit('syncLocation/setItem', {
|
||||
workspaceSvc.addSyncLocation({
|
||||
...syncLocation,
|
||||
id: utils.uid(),
|
||||
fileId: item.id,
|
||||
});
|
||||
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({
|
||||
id: 'gist',
|
||||
getToken(location) {
|
||||
return store.getters['data/githubTokensBySub'][location.sub];
|
||||
getToken({ sub }) {
|
||||
return store.getters['data/githubTokensBySub'][sub];
|
||||
},
|
||||
getUrl(location) {
|
||||
return `https://gist.github.com/${location.gistId}`;
|
||||
getLocationUrl({ gistId }) {
|
||||
return `https://gist.github.com/${gistId}`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.filename} — ${location.gistId} — ${token.name}`;
|
||||
getLocationDescription({ filename }) {
|
||||
return filename;
|
||||
},
|
||||
async downloadContent(token, syncLocation) {
|
||||
const content = await githubHelper.downloadGist({
|
||||
|
@ -2,21 +2,25 @@ import store from '../../store';
|
||||
import githubHelper from './helpers/githubHelper';
|
||||
import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import fileSvc from '../fileSvc';
|
||||
import workspaceSvc from '../workspaceSvc';
|
||||
|
||||
const savedSha = {};
|
||||
|
||||
export default new Provider({
|
||||
id: 'github',
|
||||
getToken(location) {
|
||||
return store.getters['data/githubTokensBySub'][location.sub];
|
||||
getToken({ sub }) {
|
||||
return store.getters['data/githubTokensBySub'][sub];
|
||||
},
|
||||
getUrl(location) {
|
||||
return `https://github.com/${encodeURIComponent(location.owner)}/${encodeURIComponent(location.repo)}/blob/${encodeURIComponent(location.branch)}/${encodeURIComponent(location.path)}`;
|
||||
getLocationUrl({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
path,
|
||||
}) {
|
||||
return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(branch)}/${utils.encodeUrlPath(path)}`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.path} — ${location.owner}/${location.repo} — ${token.name}`;
|
||||
getLocationDescription({ path }) {
|
||||
return path;
|
||||
},
|
||||
async downloadContent(token, syncLocation) {
|
||||
try {
|
||||
@ -59,7 +63,7 @@ export default new Provider({
|
||||
},
|
||||
async openFile(token, syncLocation) {
|
||||
// Check if the file exists and open it
|
||||
if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], syncLocation)) {
|
||||
if (!Provider.openFileWithLocation(syncLocation)) {
|
||||
// Download content from GitHub
|
||||
let content;
|
||||
try {
|
||||
@ -79,7 +83,7 @@ export default new Provider({
|
||||
if (dotPos > 0 && slashPos < name.length) {
|
||||
name = name.slice(0, dotPos);
|
||||
}
|
||||
const item = await fileSvc.createFile({
|
||||
const item = await workspaceSvc.createFile({
|
||||
name,
|
||||
parentId: store.getters['file/current'].parentId,
|
||||
text: content.text,
|
||||
@ -88,21 +92,13 @@ export default new Provider({
|
||||
comments: content.comments,
|
||||
}, true);
|
||||
store.commit('file/setCurrentId', item.id);
|
||||
store.commit('syncLocation/setItem', {
|
||||
workspaceSvc.addSyncLocation({
|
||||
...syncLocation,
|
||||
id: utils.uid(),
|
||||
fileId: item.id,
|
||||
});
|
||||
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) {
|
||||
return {
|
||||
providerId: this.id,
|
||||
|
@ -4,18 +4,8 @@ import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import userSvc from '../userSvc';
|
||||
|
||||
const getAbsolutePath = syncData =>
|
||||
`${store.getters['workspace/currentWorkspace'].path || ''}${syncData.id}`;
|
||||
|
||||
const getWorkspaceWithOwner = () => {
|
||||
const workspace = store.getters['workspace/currentWorkspace'];
|
||||
const [owner, repo] = workspace.repo.split('/');
|
||||
return {
|
||||
...workspace,
|
||||
owner,
|
||||
repo,
|
||||
};
|
||||
};
|
||||
const getAbsolutePath = ({ id }) =>
|
||||
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
||||
|
||||
let treeShaMap;
|
||||
let treeFolderMap;
|
||||
@ -31,14 +21,38 @@ export default new Provider({
|
||||
getToken() {
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
async initWorkspace() {
|
||||
const [owner, repo] = (utils.queryParams.repo || '').split('/');
|
||||
const { branch } = utils.queryParams;
|
||||
const workspaceParams = {
|
||||
getWorkspaceParams({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
path,
|
||||
}) {
|
||||
return {
|
||||
providerId: this.id,
|
||||
repo: `${owner}/${repo}`,
|
||||
owner,
|
||||
repo,
|
||||
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 || '')
|
||||
.replace(/^\/*/, '') // Remove leading `/`
|
||||
.replace(/\/*$/, '/'); // Add trailing `/`
|
||||
@ -46,7 +60,7 @@ export default new Provider({
|
||||
workspaceParams.path = path;
|
||||
}
|
||||
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
|
||||
let token;
|
||||
@ -62,34 +76,23 @@ export default new Provider({
|
||||
if (!workspace) {
|
||||
const pathEntries = (path || '').split('/');
|
||||
const name = pathEntries[pathEntries.length - 2] || repo; // path ends with `/`
|
||||
workspace = {
|
||||
...workspaceParams,
|
||||
id: workspaceId,
|
||||
sub: token.sub,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
// Fix the URL hash
|
||||
utils.setQueryParams(workspaceParams);
|
||||
if (workspace.url !== window.location.href) {
|
||||
store.dispatch('data/patchWorkspacesById', {
|
||||
store.dispatch('workspace/patchWorkspacesById', {
|
||||
[workspaceId]: {
|
||||
...workspace,
|
||||
url: window.location.href,
|
||||
...workspaceParams,
|
||||
id: workspaceId,
|
||||
sub: token.sub,
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
return store.getters['data/sanitizedWorkspacesById'][workspaceId];
|
||||
|
||||
return store.getters['workspace/workspacesById'][workspaceId];
|
||||
},
|
||||
getChanges() {
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
const { owner, repo, branch } = getWorkspaceWithOwner();
|
||||
return githubHelper.getTree({
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token: syncToken,
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
});
|
||||
},
|
||||
prepareChanges(tree) {
|
||||
@ -322,7 +325,7 @@ export default new Provider({
|
||||
// locations are stored as paths, so we upload an empty file
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
await githubHelper.uploadFile({
|
||||
...getWorkspaceWithOwner(),
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token: syncToken,
|
||||
path: getAbsolutePath(syncData),
|
||||
content: '',
|
||||
@ -334,7 +337,7 @@ export default new Provider({
|
||||
if (treeShaMap[syncData.id]) {
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
await githubHelper.removeFile({
|
||||
...getWorkspaceWithOwner(),
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token: syncToken,
|
||||
path: getAbsolutePath(syncData),
|
||||
sha: treeShaMap[syncData.id],
|
||||
@ -348,7 +351,7 @@ export default new Provider({
|
||||
fileSyncData,
|
||||
}) {
|
||||
const { sha, data } = await githubHelper.downloadFile({
|
||||
...getWorkspaceWithOwner(),
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token,
|
||||
path: getAbsolutePath(fileSyncData),
|
||||
});
|
||||
@ -369,7 +372,7 @@ export default new Provider({
|
||||
}
|
||||
|
||||
const { sha, data } = await githubHelper.downloadFile({
|
||||
...getWorkspaceWithOwner(),
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token,
|
||||
path: getAbsolutePath(syncData),
|
||||
});
|
||||
@ -388,7 +391,7 @@ export default new Provider({
|
||||
const path = store.getters.gitPathsByItemId[file.id];
|
||||
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
|
||||
const res = await githubHelper.uploadFile({
|
||||
...getWorkspaceWithOwner(),
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token,
|
||||
path: absolutePath,
|
||||
content: Provider.serializeContent(content),
|
||||
@ -418,7 +421,7 @@ export default new Provider({
|
||||
hash: item.hash,
|
||||
};
|
||||
const res = await githubHelper.uploadFile({
|
||||
...getWorkspaceWithOwner(),
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token,
|
||||
path: getAbsolutePath(syncData),
|
||||
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) {
|
||||
const { owner, repo, branch } = getWorkspaceWithOwner();
|
||||
const { owner, repo, branch } = store.getters['workspace/currentWorkspace'];
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const entries = await githubHelper.getCommits({
|
||||
token,
|
||||
@ -478,7 +472,7 @@ export default new Provider({
|
||||
async getRevisionContent(token, fileId, revisionId) {
|
||||
const syncData = Provider.getContentSyncData(fileId);
|
||||
const { data } = await githubHelper.downloadFile({
|
||||
...getWorkspaceWithOwner(),
|
||||
...store.getters['workspace/currentWorkspace'],
|
||||
token,
|
||||
branch: revisionId,
|
||||
path: getAbsolutePath(syncData),
|
||||
|
@ -10,12 +10,25 @@ export default new Provider({
|
||||
getToken() {
|
||||
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() {
|
||||
// Nothing much to do since the main workspace isn't necessarily synchronized
|
||||
// Remove the URL hash
|
||||
utils.setQueryParams();
|
||||
// Return the main workspace
|
||||
return store.getters['data/workspacesById'].main;
|
||||
return store.getters['workspace/workspacesById'].main;
|
||||
},
|
||||
async getChanges() {
|
||||
const syncToken = store.getters['workspace/syncToken'];
|
||||
@ -155,7 +168,7 @@ export default new Provider({
|
||||
const revisions = await googleHelper.getAppDataFileRevisions(token, syncData.id);
|
||||
return revisions.map(revision => ({
|
||||
id: revision.id,
|
||||
sub: revision.lastModifyingUser && `go:${revision.lastModifyingUser.permissionId}`,
|
||||
sub: `go:${revision.lastModifyingUser.permissionId}`,
|
||||
created: new Date(revision.modifiedTime).getTime(),
|
||||
}))
|
||||
.sort((revision1, revision2) => revision2.created - revision1.created);
|
||||
|
@ -2,20 +2,19 @@ import store from '../../store';
|
||||
import googleHelper from './helpers/googleHelper';
|
||||
import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import fileSvc from '../fileSvc';
|
||||
import workspaceSvc from '../workspaceSvc';
|
||||
|
||||
export default new Provider({
|
||||
id: 'googleDrive',
|
||||
getToken(location) {
|
||||
const token = store.getters['data/googleTokensBySub'][location.sub];
|
||||
getToken({ sub }) {
|
||||
const token = store.getters['data/googleTokensBySub'][sub];
|
||||
return token && token.isDrive ? token : null;
|
||||
},
|
||||
getUrl(location) {
|
||||
return `https://docs.google.com/file/d/${location.driveFileId}/edit`;
|
||||
getLocationUrl({ driveFileId }) {
|
||||
return `https://docs.google.com/file/d/${driveFileId}/edit`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.driveFileId} — ${token.name}`;
|
||||
getLocationDescription({ driveFileId }) {
|
||||
return driveFileId;
|
||||
},
|
||||
async initAction() {
|
||||
const state = googleHelper.driveState || {};
|
||||
@ -42,7 +41,7 @@ export default new Provider({
|
||||
folderId,
|
||||
};
|
||||
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 (workspace) {
|
||||
utils.setQueryParams(workspaceParams);
|
||||
@ -86,7 +85,7 @@ export default new Provider({
|
||||
const token = store.getters['data/googleTokensBySub'][state.userId];
|
||||
switch (token && state.action) {
|
||||
case 'create': {
|
||||
const file = await fileSvc.createFile({}, true);
|
||||
const file = await workspaceSvc.createFile({}, true);
|
||||
store.commit('file/setCurrentId', file.id);
|
||||
// Return a new syncLocation
|
||||
return this.makeLocation(token, null, googleHelper.driveActionFolder.id);
|
||||
@ -142,7 +141,7 @@ export default new Provider({
|
||||
async openFiles(token, driveFiles) {
|
||||
return utils.awaitSequence(driveFiles, async (driveFile) => {
|
||||
// Check if the file exists and open it
|
||||
if (!Provider.openFileWithLocation(store.getters['syncLocation/items'], {
|
||||
if (!Provider.openFileWithLocation({
|
||||
providerId: this.id,
|
||||
driveFileId: driveFile.id,
|
||||
})) {
|
||||
@ -161,7 +160,7 @@ export default new Provider({
|
||||
}
|
||||
|
||||
// Create the file
|
||||
const item = await fileSvc.createFile({
|
||||
const item = await workspaceSvc.createFile({
|
||||
name: driveFile.name,
|
||||
parentId: store.getters['file/current'].parentId,
|
||||
text: content.text,
|
||||
@ -170,9 +169,8 @@ export default new Provider({
|
||||
comments: content.comments,
|
||||
}, true);
|
||||
store.commit('file/setCurrentId', item.id);
|
||||
store.commit('syncLocation/setItem', {
|
||||
workspaceSvc.addSyncLocation({
|
||||
...syncLocation,
|
||||
id: utils.uid(),
|
||||
fileId: item.id,
|
||||
});
|
||||
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 Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import fileSvc from '../fileSvc';
|
||||
import workspaceSvc from '../workspaceSvc';
|
||||
|
||||
let fileIdToOpen;
|
||||
let syncStartPageToken;
|
||||
@ -12,17 +12,27 @@ export default new Provider({
|
||||
getToken() {
|
||||
return store.getters['workspace/syncToken'];
|
||||
},
|
||||
async initWorkspace() {
|
||||
const makeWorkspaceParams = folderId => ({
|
||||
getWorkspaceParams({ folderId }) {
|
||||
return {
|
||||
providerId: this.id,
|
||||
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
|
||||
&& utils.makeWorkspaceId(makeWorkspaceParams(folderId));
|
||||
&& utils.makeWorkspaceId(this.getWorkspaceParams({ folderId }));
|
||||
|
||||
const getWorkspace = folderId =>
|
||||
store.getters['data/sanitizedWorkspacesById'][makeWorkspaceId(folderId)];
|
||||
store.getters['workspace/workspacesById'][makeWorkspaceId(folderId)];
|
||||
|
||||
const initFolder = async (token, folder) => {
|
||||
const appProperties = {
|
||||
@ -33,24 +43,26 @@ export default new Provider({
|
||||
|
||||
// Make sure data folder exists
|
||||
if (!appProperties.dataFolderId) {
|
||||
appProperties.dataFolderId = (await googleHelper.uploadFile({
|
||||
const dataFolder = await googleHelper.uploadFile({
|
||||
token,
|
||||
name: '.stackedit-data',
|
||||
parents: [folder.id],
|
||||
appProperties: { folderId: folder.id },
|
||||
mediaType: googleHelper.folderMimeType,
|
||||
})).id;
|
||||
});
|
||||
appProperties.dataFolderId = dataFolder.id;
|
||||
}
|
||||
|
||||
// Make sure trash folder exists
|
||||
if (!appProperties.trashFolderId) {
|
||||
appProperties.trashFolderId = (await googleHelper.uploadFile({
|
||||
const trashFolder = await googleHelper.uploadFile({
|
||||
token,
|
||||
name: '.stackedit-trash',
|
||||
parents: [folder.id],
|
||||
appProperties: { folderId: folder.id },
|
||||
mediaType: googleHelper.folderMimeType,
|
||||
})).id;
|
||||
});
|
||||
appProperties.trashFolderId = trashFolder.id;
|
||||
}
|
||||
|
||||
// Update workspace if some properties are missing
|
||||
@ -68,13 +80,12 @@ export default new Provider({
|
||||
|
||||
// Update workspace in the store
|
||||
const workspaceId = makeWorkspaceId(folder.id);
|
||||
store.dispatch('data/patchWorkspacesById', {
|
||||
store.dispatch('workspace/patchWorkspacesById', {
|
||||
[workspaceId]: {
|
||||
id: workspaceId,
|
||||
sub: token.sub,
|
||||
name: folder.name,
|
||||
providerId: this.id,
|
||||
url: window.location.href,
|
||||
folderId: folder.id,
|
||||
teamDriveId: folder.teamDriveId,
|
||||
dataFolderId: appProperties.dataFolderId,
|
||||
@ -110,11 +121,10 @@ export default new Provider({
|
||||
}
|
||||
|
||||
// Init workspace
|
||||
let workspace = getWorkspace(folderId);
|
||||
if (!workspace) {
|
||||
if (!getWorkspace(folderId)) {
|
||||
let folder;
|
||||
try {
|
||||
folder = googleHelper.getFile(token, folderId);
|
||||
folder = await googleHelper.getFile(token, folderId);
|
||||
} catch (err) {
|
||||
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.`);
|
||||
}
|
||||
await initFolder(token, folder);
|
||||
workspace = getWorkspace(folderId);
|
||||
}
|
||||
|
||||
// Fix the URL hash
|
||||
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];
|
||||
return getWorkspace(folderId);
|
||||
},
|
||||
async performAction() {
|
||||
const state = googleHelper.driveState || {};
|
||||
@ -163,7 +162,7 @@ export default new Provider({
|
||||
[syncData.id]: syncData,
|
||||
});
|
||||
}
|
||||
const file = await fileSvc.createFile({
|
||||
const file = await workspaceSvc.createFile({
|
||||
parentId: syncData && syncData.itemId,
|
||||
}, true);
|
||||
store.commit('file/setCurrentId', file.id);
|
||||
@ -486,6 +485,7 @@ export default new Provider({
|
||||
folderId: workspace.folderId,
|
||||
},
|
||||
media: JSON.stringify(item),
|
||||
mediaType: 'application/json',
|
||||
fileId: syncData && syncData.id,
|
||||
oldParents: syncData && syncData.parentIds,
|
||||
ifNotTooLate,
|
||||
|
@ -8,13 +8,16 @@ const getAppKey = (fullAccess) => {
|
||||
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({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
'Content-Type': options.body && (typeof options.body === 'string'
|
||||
? 'application/octet-stream' : 'application/json; charset=utf-8'),
|
||||
'Dropbox-API-Arg': args && JSON.stringify(args),
|
||||
'Dropbox-API-Arg': httpHeaderSafeJson(args),
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
@ -413,7 +413,7 @@ export default {
|
||||
});
|
||||
revisions.forEach((revision) => {
|
||||
store.commit('userInfo/addItem', {
|
||||
id: revision.lastModifyingUser.permissionId,
|
||||
id: `go:${revision.lastModifyingUser.permissionId}`,
|
||||
name: revision.lastModifyingUser.displayName,
|
||||
imageUrl: revision.lastModifyingUser.photoLink,
|
||||
});
|
||||
|
@ -7,12 +7,11 @@ export default new Provider({
|
||||
getToken(location) {
|
||||
return store.getters['data/wordpressTokensBySub'][location.sub];
|
||||
},
|
||||
getUrl(location) {
|
||||
getLocationUrl(location) {
|
||||
return `https://wordpress.com/post/${location.siteId}/${location.postId}`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.postId} — ${location.domain} — ${token.name}`;
|
||||
getLocationDescription({ postId }) {
|
||||
return postId;
|
||||
},
|
||||
async publish(token, html, metadata, publishLocation) {
|
||||
const post = await wordpressHelper.uploadPost({
|
||||
|
@ -7,13 +7,12 @@ export default new Provider({
|
||||
getToken(location) {
|
||||
return store.getters['data/zendeskTokensBySub'][location.sub];
|
||||
},
|
||||
getUrl(location) {
|
||||
getLocationUrl(location) {
|
||||
const token = this.getToken(location);
|
||||
return `https://${token.subdomain}.zendesk.com/hc/${location.locale}/articles/${location.articleId}`;
|
||||
},
|
||||
getDescription(location) {
|
||||
const token = this.getToken(location);
|
||||
return `${location.articleId} — ${token.name} — ${token.subdomain}`;
|
||||
getLocationDescription({ articleId }) {
|
||||
return articleId;
|
||||
},
|
||||
async publish(token, html, metadata, publishLocation) {
|
||||
const articleId = await zendeskHelper.uploadArticle({
|
||||
|
@ -4,6 +4,7 @@ import utils from './utils';
|
||||
import networkSvc from './networkSvc';
|
||||
import exportSvc from './exportSvc';
|
||||
import providerRegistry from './providers/common/providerRegistry';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
|
||||
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
||||
|
||||
@ -45,7 +46,7 @@ const publish = async (publishLocation) => {
|
||||
const content = await localDbSvc.loadItem(`${fileId}/content`);
|
||||
const file = store.state.file.itemsById[fileId];
|
||||
const properties = utils.computeProperties(content.properties);
|
||||
const provider = providerRegistry.providers[publishLocation.providerId];
|
||||
const provider = providerRegistry.providersById[publishLocation.providerId];
|
||||
const token = provider.getToken(publishLocation);
|
||||
const metadata = {
|
||||
title: ensureString(properties.title, file.name),
|
||||
@ -122,14 +123,12 @@ const requestPublish = () => {
|
||||
};
|
||||
|
||||
const createPublishLocation = (publishLocation) => {
|
||||
publishLocation.id = utils.uid();
|
||||
const currentFile = store.getters['file/current'];
|
||||
publishLocation.fileId = currentFile.id;
|
||||
store.dispatch(
|
||||
'queue/enqueue',
|
||||
async () => {
|
||||
const publishLocationToStore = await publish(publishLocation);
|
||||
store.commit('publishLocation/setItem', publishLocationToStore);
|
||||
workspaceSvc.addPublishLocation(await publish(publishLocation));
|
||||
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/googleDriveWorkspaceProvider';
|
||||
import tempFileSvc from './tempFileSvc';
|
||||
import fileSvc from './fileSvc';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
|
||||
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||
const inactivityThreshold = 3 * 1000; // 3 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 LAST_SEEN = 0;
|
||||
@ -112,9 +112,11 @@ const cleanSyncedContent = (syncedContent) => {
|
||||
delete syncedContent.syncHistory[syncLocationId];
|
||||
}
|
||||
});
|
||||
|
||||
const allSyncLocationHashSet = new Set([]
|
||||
.concat(...Object.keys(syncedContent.syncHistory)
|
||||
.map(id => syncedContent.syncHistory[id])));
|
||||
|
||||
// Clean historyData from unused contents
|
||||
Object.keys(syncedContent.historyData)
|
||||
.map(hash => parseInt(hash, 10))
|
||||
@ -131,9 +133,11 @@ const cleanSyncedContent = (syncedContent) => {
|
||||
const applyChanges = (changes) => {
|
||||
const allItemsById = { ...store.getters.allItemsById };
|
||||
const syncDataById = { ...store.getters['data/syncDataById'] };
|
||||
const idsToKeep = {};
|
||||
let saveSyncData = false;
|
||||
let getExistingItem;
|
||||
if (workspaceProvider.isGit) {
|
||||
const { itemsByGitPath } = store.getters;
|
||||
if (store.getters['workspace/currentWorkspaceIsGit']) {
|
||||
const itemsByGitPath = { ...store.getters.itemsByGitPath };
|
||||
getExistingItem = (existingSyncData) => {
|
||||
const items = existingSyncData && itemsByGitPath[existingSyncData.id];
|
||||
return items ? items[0] : null;
|
||||
@ -142,9 +146,6 @@ const applyChanges = (changes) => {
|
||||
getExistingItem = existingSyncData => existingSyncData && allItemsById[existingSyncData.itemId];
|
||||
}
|
||||
|
||||
const idsToKeep = {};
|
||||
let saveSyncData = false;
|
||||
|
||||
// Process each change
|
||||
changes.forEach((change) => {
|
||||
const existingSyncData = syncDataById[change.syncDataId];
|
||||
@ -184,7 +185,10 @@ const applyChanges = (changes) => {
|
||||
|
||||
if (saveSyncData) {
|
||||
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.
|
||||
*/
|
||||
const createSyncLocation = (syncLocation) => {
|
||||
syncLocation.id = utils.uid();
|
||||
const currentFile = store.getters['file/current'];
|
||||
const fileId = currentFile.id;
|
||||
syncLocation.fileId = fileId;
|
||||
// Use deepCopy to freeze item
|
||||
// Use deepCopy to freeze the item
|
||||
const content = utils.deepCopy(store.getters['content/current']);
|
||||
store.dispatch(
|
||||
'queue/enqueue',
|
||||
async () => {
|
||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||
const provider = providerRegistry.providersById[syncLocation.providerId];
|
||||
const token = provider.getToken(syncLocation);
|
||||
const syncLocationToStore = await provider.uploadContent(token, {
|
||||
const updatedSyncLocation = await provider.uploadContent(token, {
|
||||
...content,
|
||||
history: [content.hash],
|
||||
}, syncLocation);
|
||||
@ -216,7 +219,7 @@ const createSyncLocation = (syncLocation) => {
|
||||
newSyncedContent.historyData[content.hash] = content;
|
||||
|
||||
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}".`);
|
||||
},
|
||||
);
|
||||
@ -241,7 +244,7 @@ const tooLateChecker = (timeout) => {
|
||||
const isTempFile = (fileId) => {
|
||||
const contentId = `${fileId}/content`;
|
||||
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;
|
||||
}
|
||||
const file = store.state.file.itemsById[fileId];
|
||||
@ -271,8 +274,11 @@ const isTempFile = (fileId) => {
|
||||
* Patch sync data if some have changed in the result.
|
||||
*/
|
||||
const updateSyncData = (result) => {
|
||||
['syncData', 'contentSyncData', 'fileSyncData'].forEach((field) => {
|
||||
const syncData = result[field];
|
||||
[
|
||||
result.syncData,
|
||||
result.contentSyncData,
|
||||
result.fileSyncData,
|
||||
].forEach((syncData) => {
|
||||
if (syncData) {
|
||||
const oldSyncData = store.getters['data/syncDataById'][syncData.id];
|
||||
if (utils.serializeObject(oldSyncData) !== utils.serializeObject(syncData)) {
|
||||
@ -286,7 +292,7 @@ const updateSyncData = (result) => {
|
||||
};
|
||||
|
||||
class SyncContext {
|
||||
restart = false;
|
||||
restartSkipContents = false;
|
||||
attempted = {};
|
||||
}
|
||||
|
||||
@ -320,7 +326,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
||||
}
|
||||
|
||||
await utils.awaitSequence(syncLocations, async (syncLocation) => {
|
||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||
const provider = providerRegistry.providersById[syncLocation.providerId];
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
@ -491,7 +497,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
||||
if (provider === workspaceProvider &&
|
||||
!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') {
|
||||
// Restart sync
|
||||
await syncFile(fileId, syncContext);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
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 getItem = () => store.state.data.itemsById[dataId]
|
||||
@ -543,8 +550,8 @@ const syncDataItem = async (dataId) => {
|
||||
|
||||
const serverItem = item;
|
||||
const dataSyncData = store.getters['data/dataSyncDataById'][dataId];
|
||||
const clientItem = utils.deepCopy(getItem());
|
||||
let mergedItem = (() => {
|
||||
const clientItem = utils.deepCopy(getItem());
|
||||
if (!clientItem) {
|
||||
return serverItem;
|
||||
}
|
||||
@ -572,6 +579,13 @@ const syncDataItem = async (dataId) => {
|
||||
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
|
||||
store.commit('data/setItem', {
|
||||
id: dataId,
|
||||
@ -581,19 +595,17 @@ const syncDataItem = async (dataId) => {
|
||||
// Retrieve item with new `hash` and freeze it
|
||||
mergedItem = utils.deepCopy(getItem());
|
||||
|
||||
if (serverItem && serverItem.hash === mergedItem.hash) {
|
||||
return;
|
||||
// Upload merged data item if out of sync
|
||||
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
|
||||
updateSyncData(await workspaceProvider.uploadWorkspaceData({
|
||||
token,
|
||||
item: mergedItem,
|
||||
syncData: store.getters['data/syncDataByItemId'][dataId],
|
||||
ifNotTooLate: tooLateChecker(restartContentSyncAfter),
|
||||
}));
|
||||
|
||||
// Update data sync data
|
||||
// Copy sync data into data sync data
|
||||
store.dispatch('data/patchDataSyncDataById', {
|
||||
[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.
|
||||
*/
|
||||
const syncWorkspace = async () => {
|
||||
const syncWorkspace = async (skipContents = false) => {
|
||||
try {
|
||||
const workspace = store.getters['workspace/currentWorkspace'];
|
||||
const syncContext = new SyncContext();
|
||||
@ -627,8 +639,8 @@ const syncWorkspace = async () => {
|
||||
// Prevent from sending items too long after changes have been retrieved
|
||||
const ifNotTooLate = tooLateChecker(restartSyncAfter);
|
||||
|
||||
// Called until no item to save
|
||||
const saveNextItem = () => ifNotTooLate(async () => {
|
||||
// Find and save one item to save
|
||||
await utils.awaitSome(() => ifNotTooLate(async () => {
|
||||
const storeItemMap = {
|
||||
...store.state.file.itemsById,
|
||||
...store.state.folder.itemsById,
|
||||
@ -654,27 +666,26 @@ const syncWorkspace = async () => {
|
||||
},
|
||||
) || [];
|
||||
|
||||
if (changedItem) {
|
||||
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();
|
||||
if (!changedItem) return false;
|
||||
|
||||
// Called until no item to remove
|
||||
const removeNextItem = () => ifNotTooLate(async () => {
|
||||
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,
|
||||
});
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Find and remove one item to remove
|
||||
await utils.awaitSome(() => ifNotTooLate(async () => {
|
||||
let getItem;
|
||||
let getFileItem;
|
||||
if (workspaceProvider.isGit) {
|
||||
if (store.getters['workspace/currentWorkspaceIsGit']) {
|
||||
const { itemsByGitPath } = store.getters;
|
||||
getItem = syncData => itemsByGitPath[syncData.id];
|
||||
getFileItem = syncData => itemsByGitPath[syncData.id.slice(1)]; // Remove leading /
|
||||
@ -701,18 +712,17 @@ const syncWorkspace = async () => {
|
||||
},
|
||||
));
|
||||
|
||||
if (syncDataToRemove) {
|
||||
await workspaceProvider.removeWorkspaceItem({
|
||||
syncData: syncDataToRemove,
|
||||
ifNotTooLate,
|
||||
});
|
||||
const syncDataCopy = { ...store.getters['data/syncDataById'] };
|
||||
delete syncDataCopy[syncDataToRemove.id];
|
||||
store.dispatch('data/setSyncDataById', syncDataCopy);
|
||||
await removeNextItem();
|
||||
}
|
||||
});
|
||||
await removeNextItem();
|
||||
if (!syncDataToRemove) return false;
|
||||
|
||||
await workspaceProvider.removeWorkspaceItem({
|
||||
syncData: syncDataToRemove,
|
||||
ifNotTooLate,
|
||||
});
|
||||
const syncDataCopy = { ...store.getters['data/syncDataById'] };
|
||||
delete syncDataCopy[syncDataToRemove.id];
|
||||
store.dispatch('data/setSyncDataById', syncDataCopy);
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Sync settings and workspaces only in the main workspace
|
||||
if (workspace.id === 'main') {
|
||||
@ -721,63 +731,61 @@ const syncWorkspace = async () => {
|
||||
}
|
||||
await syncDataItem('templates');
|
||||
|
||||
const getOneFileIdToSync = () => {
|
||||
let getSyncData;
|
||||
if (workspaceProvider.isGit) {
|
||||
const { gitPathsByItemId } = store.getters;
|
||||
const syncDataById = store.getters['data/syncDataById'];
|
||||
// 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];
|
||||
if (!skipContents) {
|
||||
const currentFileId = store.getters['file/current'].id;
|
||||
if (currentFileId) {
|
||||
// Sync current file first
|
||||
await syncFile(currentFileId, syncContext);
|
||||
}
|
||||
|
||||
// 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;
|
||||
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;
|
||||
// Find and sync one file out of sync
|
||||
await utils.awaitSome(async () => {
|
||||
let getSyncData;
|
||||
if (store.getters['workspace/currentWorkspaceIsGit']) {
|
||||
const { gitPathsByItemId } = store.getters;
|
||||
const syncDataById = store.getters['data/syncDataById'];
|
||||
getSyncData = contentId => syncDataById[gitPathsByItemId[contentId]];
|
||||
} else {
|
||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||
getSyncData = contentId => syncDataByItemId[contentId];
|
||||
}
|
||||
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
|
||||
await syncWorkspace();
|
||||
// Restart sync if requested
|
||||
if (syncContext.restartSkipContents) {
|
||||
await syncWorkspace(true);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.message === 'TOO_LATE') {
|
||||
@ -786,10 +794,6 @@ const syncWorkspace = async () => {
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (workspaceProvider.onSyncEnd) {
|
||||
workspaceProvider.onSyncEnd();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -835,9 +839,9 @@ const requestSync = () => {
|
||||
if (isWorkspaceSyncPossible()) {
|
||||
await syncWorkspace();
|
||||
} else if (hasCurrentFileSyncLocations()) {
|
||||
// Only sync current file if workspace sync is unavailable.
|
||||
// We could sync all files that are out-of-sync but it would
|
||||
// require to load all the syncedContent objects from the DB.
|
||||
// Only sync the current file if workspace sync is unavailable
|
||||
// as we don't want to look for out-of-sync files by loading
|
||||
// all the syncedContent objects.
|
||||
await syncFile(store.getters['file/current'].id);
|
||||
}
|
||||
|
||||
@ -845,7 +849,7 @@ const requestSync = () => {
|
||||
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
|
||||
const file = store.state.file.itemsById[fileId];
|
||||
if (file && file.hash === fileHash) {
|
||||
fileSvc.deleteFile(fileId);
|
||||
workspaceSvc.deleteFile(fileId);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
@ -865,22 +869,25 @@ export default {
|
||||
localDbSvc.syncLocalStorage();
|
||||
|
||||
// Try to find a suitable action provider
|
||||
actionProvider = providerRegistry.providers[utils.queryParams.providerId];
|
||||
actionProvider = providerRegistry.providersById[utils.queryParams.providerId];
|
||||
if (actionProvider && actionProvider.initAction) {
|
||||
await actionProvider.initAction();
|
||||
}
|
||||
|
||||
// Try to find a suitable workspace sync provider
|
||||
workspaceProvider = providerRegistry.providers[utils.queryParams.providerId];
|
||||
workspaceProvider = providerRegistry.providersById[utils.queryParams.providerId];
|
||||
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
|
||||
workspaceProvider = googleDriveAppDataProvider;
|
||||
}
|
||||
const workspace = await workspaceProvider.initWorkspace();
|
||||
// Fix the URL hash
|
||||
utils.setQueryParams(workspaceProvider.getWorkspaceParams(workspace));
|
||||
|
||||
store.dispatch('workspace/setCurrentWorkspaceId', workspace.id);
|
||||
await localDbSvc.init();
|
||||
|
||||
// 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) {
|
||||
const newSyncLocation = await actionProvider.performAction();
|
||||
if (newSyncLocation) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import cledit from './cledit';
|
||||
import cledit from './editor/cledit';
|
||||
import store from '../store';
|
||||
import utils from './utils';
|
||||
import editorSvc from './editorSvc';
|
||||
import fileSvc from './fileSvc';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
|
||||
const {
|
||||
origin,
|
||||
@ -31,7 +31,7 @@ export default {
|
||||
}
|
||||
store.commit('setLight', true);
|
||||
|
||||
const file = await fileSvc.createFile({
|
||||
const file = await workspaceSvc.createFile({
|
||||
name: fileName || utils.getHostname(origin),
|
||||
text: contentText || '\n',
|
||||
properties: contentProperties,
|
||||
@ -58,7 +58,7 @@ export default {
|
||||
.splice(10)
|
||||
.forEach(([id]) => {
|
||||
delete lastCreated[id];
|
||||
fileSvc.deleteFile(id);
|
||||
workspaceSvc.deleteFile(id);
|
||||
});
|
||||
|
||||
// Store file creations and open the file
|
||||
|
@ -170,6 +170,13 @@ export default {
|
||||
makeWorkspaceId(params) {
|
||||
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) {
|
||||
const uriEncodedStr = encodeURIComponent(str);
|
||||
const utf8Str = uriEncodedStr.replace(
|
||||
@ -227,6 +234,12 @@ export default {
|
||||
};
|
||||
return runWithNextValue();
|
||||
},
|
||||
async awaitSome(asyncFunc) {
|
||||
if (await asyncFunc()) {
|
||||
return this.awaitSome(asyncFunc);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
someResult(values, func) {
|
||||
let result;
|
||||
values.some((value) => {
|
||||
@ -279,6 +292,16 @@ export default {
|
||||
urlParser.href = url;
|
||||
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) {
|
||||
const iframeElt = document.createElement('iframe');
|
||||
iframeElt.style.position = 'absolute';
|
||||
|
@ -4,6 +4,7 @@ import utils from './utils';
|
||||
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Create a file in the store with the specified fields.
|
||||
*/
|
||||
@ -29,7 +30,7 @@ export default {
|
||||
discussions: discussions || {},
|
||||
comments: comments || {},
|
||||
};
|
||||
const workspaceUniquePaths = store.getters['workspace/hasUniquePaths'];
|
||||
const workspaceUniquePaths = store.getters['workspace/currentWorkspaceHasUniquePaths'];
|
||||
|
||||
// Show warning dialogs
|
||||
if (!background) {
|
||||
@ -90,7 +91,7 @@ export default {
|
||||
}
|
||||
|
||||
// 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 path = parentPath + sanitizedName;
|
||||
const items = store.getters.itemsByPath[path] || [];
|
||||
@ -133,7 +134,7 @@ export default {
|
||||
store.commit(`${item.type}/setItem`, item);
|
||||
|
||||
// Ensure path uniqueness
|
||||
if (store.getters['workspace/hasUniquePaths']) {
|
||||
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
||||
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.
|
||||
*/
|
||||
ensureUniquePaths(idsToKeep = {}) {
|
||||
if (store.getters['workspace/hasUniquePaths']) {
|
||||
if (store.getters['workspace/currentWorkspaceHasUniquePaths']) {
|
||||
if (Object.keys(store.getters.pathsByItemId)
|
||||
.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 empty from '../data/emptyContent';
|
||||
import utils from '../services/utils';
|
||||
import cledit from '../services/cledit';
|
||||
import cledit from '../services/editor/cledit';
|
||||
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
|
||||
|
@ -92,9 +92,6 @@ const tokenAdder = providerId => ({ getters, dispatch }, token) => {
|
||||
});
|
||||
};
|
||||
|
||||
// For workspaces
|
||||
const urlParser = window.document.createElement('a');
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
@ -126,25 +123,7 @@ export default {
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
workspacesById: getter('workspaces'),
|
||||
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;
|
||||
},
|
||||
workspaces: getter('workspaces'), // Not to be used, prefer workspace/workspacesById
|
||||
settings: getter('settings'),
|
||||
computedSettings: (state, { settings }) => {
|
||||
const customSettings = yaml.safeLoad(settings);
|
||||
@ -207,18 +186,6 @@ export default {
|
||||
}
|
||||
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'),
|
||||
tokensByProviderId: getter('tokens'),
|
||||
googleTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.google || {},
|
||||
@ -229,8 +196,6 @@ export default {
|
||||
zendeskTokensBySub: (state, { tokensByProviderId }) => tokensByProviderId.zendesk || {},
|
||||
},
|
||||
actions: {
|
||||
setWorkspacesById: setter('workspaces'),
|
||||
patchWorkspacesById: patcher('workspaces'),
|
||||
setSettings: setter('settings'),
|
||||
patchLocalSettings: patcher('localSettings'),
|
||||
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