Github workspace (part 2)

This commit is contained in:
Benoit Schweblin 2018-05-04 19:07:28 +01:00
parent 53ccee0d84
commit b896a2e086
46 changed files with 588 additions and 319 deletions

12
package-lock.json generated
View File

@ -14498,9 +14498,9 @@
}
},
"vue": {
"version": "2.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.5.13.tgz",
"integrity": "sha512-3D+lY7HTkKbtswDM4BBHgqyq+qo8IAEE8lz8va1dz3LLmttjgo0FxairO4r1iN2OBqk8o1FyL4hvzzTFEdQSEw=="
"version": "2.5.16",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.5.16.tgz",
"integrity": "sha512-/ffmsiVuPC8PsWcFkZngdpas19ABm5mh2wA7iDqcltyCTwlgZjHGeJYOXkBMo422iPwIcviOtrTCUpSfXmToLQ=="
},
"vue-hot-reload-api": {
"version": "2.2.4",
@ -14588,9 +14588,9 @@
"dev": true
},
"vuex": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-2.5.0.tgz",
"integrity": "sha512-5oJPOJySBgSgSzoeO+gZB/BbN/XsapgIF6tz34UwJqnGZMQurzIO3B4KIBf862gfc9ya+oduY5sSkq+5/oOilQ=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.0.1.tgz",
"integrity": "sha512-wLoqz0B7DSZtgbWL1ShIBBCjv22GV5U+vcBFox658g6V0s4wZV9P4YjCNyoHSyIBpj1f29JBoNQIqD82cR4O3w=="
},
"w3c-hr-time": {
"version": "1.0.1",

View File

@ -52,8 +52,8 @@
"serve-static": "^1.12.6",
"tmp": "^0.0.33",
"turndown": "^4.0.1",
"vue": "^2.3.3",
"vuex": "^2.3.1"
"vue": "^2.5.16",
"vuex": "^3.0.1"
},
"devDependencies": {
"autoprefixer": "^6.7.2",

View File

@ -28,6 +28,7 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import ExplorerNode from './ExplorerNode';
import explorerSvc from '../services/explorerSvc';
export default {
components: {
@ -49,10 +50,8 @@ export default {
...mapActions('data', [
'toggleExplorer',
]),
...mapActions('explorer', [
'newItem',
'deleteItem',
]),
newItem: isFolder => explorerSvc.newItem(isFolder),
deleteItem: () => explorerSvc.deleteItem(),
editItem() {
const node = this.selectedNode;
if (!node.isTrash && !node.isTemp) {

View File

@ -19,7 +19,8 @@
<script>
import { mapMutations, mapActions } from 'vuex';
import utils from '../services/utils';
import fileSvc from '../services/fileSvc';
import explorerSvc from '../services/explorerSvc';
export default {
name: 'explorer-node', // Required for recursivity
@ -77,8 +78,6 @@ export default {
]),
...mapActions('explorer', [
'setDragTarget',
'newItem',
'deleteItem',
]),
select(id = this.node.item.id, doOpen = true) {
const node = this.$store.getters['explorer/nodeMap'][id];
@ -102,31 +101,26 @@ export default {
const newChildNode = this.$store.state.explorer.newChildNode;
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
if (newChildNode.isFolder) {
const id = utils.uid();
this.$store.commit('folder/setItem', {
...newChildNode.item,
id,
name: utils.sanitizeName(newChildNode.item.name),
});
this.select(id);
fileSvc.storeItem(newChildNode.item)
.then(item => this.select(item.id), () => { /* cancel */ });
} else {
this.$store.dispatch('createFile', newChildNode.item)
.then(file => this.select(file.id));
fileSvc.createFile(newChildNode.item)
.then(item => this.select(item.id), () => { /* cancel */ });
}
}
this.$store.commit('explorer/setNewItem', null);
},
submitEdit(cancel) {
const editingNode = this.$store.getters['explorer/editingNode'];
const id = editingNode.item.id;
const item = this.$store.getters['explorer/editingNode'].item;
const value = this.editingValue;
if (!cancel && id && value) {
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
id,
name: utils.sanitizeName(value),
});
}
this.setEditingId(null);
if (!cancel && item.id && value) {
fileSvc.storeItem({
...item,
name: value,
})
.catch(() => { /* cancel */ });
}
},
setDragSourceId(evt) {
if (this.node.noDrag) {
@ -169,11 +163,11 @@ export default {
items: [{
name: 'New file',
disabled: !this.node.isFolder || this.node.isTrash,
perform: () => this.newItem(false),
perform: () => explorerSvc.newItem(false),
}, {
name: 'New folder',
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
perform: () => this.newItem(true),
perform: () => explorerSvc.newItem(true),
}, {
type: 'separator',
}, {
@ -182,7 +176,7 @@ export default {
perform: () => this.setEditingId(this.node.item.id),
}, {
name: 'Delete',
perform: () => this.deleteItem(),
perform: () => explorerSvc.deleteItem(),
}],
})
.then(item => item.perform());

View File

@ -19,7 +19,7 @@
<!-- Title -->
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keydown.enter="submitTitle()" @keydown.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keydown.enter="submitTitle(false)" @keydown.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<!-- Sync/Publish -->
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Synchronized location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
@ -56,6 +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';
// According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
@ -151,6 +152,13 @@ export default {
}
return result;
},
editCancelTrigger() {
const current = this.$store.getters['file/current'];
return utils.serializeObject([
current.id,
current.name,
]);
},
},
methods: {
...mapMutations('content', [
@ -190,10 +198,13 @@ export default {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
} else {
const title = this.title.trim();
this.title = this.$store.getters['file/current'].name;
if (title) {
this.$store.dispatch('file/patchCurrent', { name: utils.sanitizeName(title) });
} else {
this.title = this.$store.getters['file/current'].name;
fileSvc.storeItem({
...this.$store.getters['file/current'],
name: title,
})
.catch(() => { /* Cancel */ });
}
}
},
@ -209,9 +220,10 @@ export default {
},
created() {
this.$watch(
() => this.$store.getters['file/current'].name,
(name) => {
this.title = name;
() => this.editCancelTrigger,
() => {
this.title = '';
this.editTitle(false);
}, { immediate: true });
},
mounted() {

View File

@ -3,8 +3,6 @@
</template>
<script>
import userSvc from '../services/userSvc';
export default {
props: ['providerId'],
computed: {
@ -15,8 +13,5 @@ export default {
}
},
},
created() {
userSvc.getInfo(this.userId);
},
};
</script>

View File

@ -51,7 +51,7 @@ export default {
this.$store.dispatch('modal/commentDeletion')
.then(
() => this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment }),
() => {}); // Cancel
() => { /* Cancel */ });
},
},
mounted() {

View File

@ -99,7 +99,7 @@ export default {
() => this.$store.dispatch('discussion/cleanCurrentFile', {
filterDiscussion: this.currentDiscussion,
}),
() => {}); // Cancel
() => { /* Cancel */ });
},
},
};

View File

@ -3,7 +3,7 @@
<div class="comment__header flex flex--row flex--space-between flex--align-center">
<div class="comment__user flex flex--row flex--align-center">
<div class="comment__user-image">
<user-image :user-id="loginToken.sub"></user-image>
<user-image :user-id="userId"></user-image>
</div>
<span class="user-name">{{loginToken.name}}</span>
</div>
@ -33,9 +33,12 @@ export default {
components: {
UserImage,
},
computed: mapGetters('workspace', [
'loginToken',
]),
computed: {
...mapGetters('workspace', [
'loginToken',
'userId',
]),
},
methods: {
...mapMutations('discussion', [
'setNewCommentFocus',
@ -53,7 +56,7 @@ export default {
const discussionId = this.$store.state.discussion.currentDiscussionId;
const comment = {
discussionId,
sub: this.loginToken.sub,
sub: this.userId,
text,
created: Date.now(),
};

View File

@ -35,19 +35,19 @@ export default {
exportMarkdown() {
const currentFile = this.$store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
exportHtml() {
return this.$store.dispatch('modal/open', 'htmlExport')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
exportPdf() {
return this.$store.dispatch('modal/open', 'pdfExport')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
exportPandoc() {
return this.$store.dispatch('modal/open', 'pandocExport')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
},
};

View File

@ -100,7 +100,7 @@ export default {
return googleHelper.signin()
.then(
() => syncSvc.requestSync(),
() => {}, // Cancel
() => { /* Cancel */ },
);
},
close() {

View File

@ -29,6 +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';
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
@ -55,16 +56,18 @@ export default {
onImportMarkdown(evt) {
const file = evt.target.files[0];
readFile(file)
.then(content => this.$store.dispatch('createFile', {
.then(content => fileSvc.createFile({
...Provider.parseContent(content),
name: file.name,
})
.then(item => this.$store.commit('file/setCurrentId', item.id)));
.then(
item => this.$store.commit('file/setCurrentId', item.id)),
() => { /* Cancel */ });
},
onImportHtml(evt) {
const file = evt.target.files[0];
readFile(file)
.then(content => this.$store.dispatch('createFile', {
.then(content => fileSvc.createFile({
...Provider.parseContent(
turndownService.turndown(
htmlSanitizer.sanitizeHtml(content)
@ -72,7 +75,9 @@ export default {
)),
name: file.name,
}))
.then(item => this.$store.commit('file/setCurrentId', item.id));
.then(
item => this.$store.commit('file/setCurrentId', item.id),
() => { /* Cancel */ });
},
},
};

View File

@ -3,7 +3,7 @@
<div class="menu-info-entries">
<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="loginToken.sub"></user-image>
<user-image :user-id="userId"></user-image>
</div>
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
</div>
@ -97,6 +97,7 @@ export default {
'currentWorkspace',
'syncToken',
'loginToken',
'userId',
]),
},
methods: {
@ -107,12 +108,12 @@ export default {
return googleHelper.signin()
.then(
() => syncSvc.requestSync(),
() => {}, // Cancel
() => { /* Cancel */ },
);
},
fileProperties() {
return this.$store.dispatch('modal/open', 'fileProperties')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
print() {
print();

View File

@ -82,14 +82,14 @@ export default {
return this.$store.dispatch('modal/open', 'settings')
.then(
settings => this.$store.dispatch('data/setSettings', settings),
() => {}, // Cancel
() => { /* Cancel */ },
);
},
templates() {
return this.$store.dispatch('modal/open', 'templates')
.then(
({ templates }) => this.$store.dispatch('data/setTemplates', templates),
() => {}, // Cancel
() => { /* Cancel */ },
);
},
reset() {

View File

@ -123,6 +123,8 @@ const openPublishModal = (token, type) => store.dispatch('modal/open', {
token,
}).then(publishLocation => publishSvc.createPublishLocation(publishLocation));
const onCancel = () => {};
export default {
components: {
MenuEntry,
@ -181,68 +183,68 @@ export default {
type: 'googleDriveAccount',
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
addDropboxAccount() {
return this.$store.dispatch('modal/open', {
type: 'dropboxAccount',
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess),
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
addGithubAccount() {
return this.$store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
addWordpressAccount() {
return wordpressHelper.addAccount()
.catch(() => {}); // Cancel
.catch(onCancel);
},
addBloggerAccount() {
return googleHelper.addBloggerAccount()
.catch(() => {}); // Cancel
.catch(onCancel);
},
addZendeskAccount() {
return this.$store.dispatch('modal/open', {
type: 'zendeskAccount',
onResolve: ({ subdomain, clientId }) => zendeskHelper.addAccount(subdomain, clientId),
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishGoogleDrive(token) {
return openPublishModal(token, 'googleDrivePublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishDropbox(token) {
return openPublishModal(token, 'dropboxPublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishGithub(token) {
return openPublishModal(token, 'githubPublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishGist(token) {
return openPublishModal(token, 'gistPublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishWordpress(token) {
return openPublishModal(token, 'wordpressPublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishBlogger(token) {
return openPublishModal(token, 'bloggerPublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishBloggerPage(token) {
return openPublishModal(token, 'bloggerPagePublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
publishZendesk(token) {
return openPublishModal(token, 'zendeskPublish')
.catch(() => {}); // Cancel
.catch(onCancel);
},
},
};

View File

@ -101,6 +101,8 @@ const openSyncModal = (token, type) => store.dispatch('modal/open', {
token,
}).then(syncLocation => syncSvc.createSyncLocation(syncLocation));
const onCancel = () => {};
export default {
components: {
MenuEntry,
@ -150,21 +152,21 @@ export default {
type: 'googleDriveAccount',
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
addDropboxAccount() {
return this.$store.dispatch('modal/open', {
type: 'dropboxAccount',
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess),
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
addGithubAccount() {
return this.$store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
openGoogleDrive(token) {
return googleHelper.openPicker(token, 'doc')
@ -178,11 +180,11 @@ export default {
},
saveGoogleDrive(token) {
return openSyncModal(token, 'googleDriveSave')
.catch(() => {}); // Cancel
.catch(onCancel);
},
saveDropbox(token) {
return openSyncModal(token, 'dropboxSave')
.catch(() => {}); // Cancel
.catch(onCancel);
},
openGithub(token) {
return store.dispatch('modal/open', {
@ -194,11 +196,11 @@ export default {
},
saveGithub(token) {
return openSyncModal(token, 'githubSave')
.catch(() => {}); // Cancel
.catch(onCancel);
},
saveGist(token) {
return openSyncModal(token, 'gistSync')
.catch(() => {}); // Cancel
.catch(onCancel);
},
},
};

View File

@ -31,6 +31,8 @@ import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper';
const onCancel = () => {};
export default {
components: {
MenuEntry,
@ -48,13 +50,13 @@ export default {
return this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace',
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
addGithubWorkspace() {
return this.$store.dispatch('modal/open', {
type: 'githubWorkspace',
})
.catch(() => {}); // Cancel
.catch(onCancel);
},
addGoogleDriveWorkspace() {
return googleHelper.addDriveAccount(true)
@ -62,7 +64,7 @@ export default {
type: 'googleDriveWorkspace',
token,
}))
.catch(() => {}); // Cancel
.catch(onCancel);
},
manageWorkspaces() {
return this.$store.dispatch('modal/open', 'workspaceManagement');

View File

@ -2,7 +2,7 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content">
<div class="logo-background"></div>
<small>v{{version}} © 2018 Dock5 Software</small>
<small>v{{version}}<br>© 2013-2018 Dock5 Software</small>
<hr>
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
<br>

View File

@ -70,7 +70,8 @@ export default modalTemplate({
if (err.status !== 401) {
throw err;
}
this.$store.dispatch('modal/sponsorOnly');
this.$store.dispatch('modal/sponsorOnly')
.catch(() => { /* Cancel */ });
}))
.catch((err) => {
console.error(err); // eslint-disable-line no-console

View File

@ -63,7 +63,8 @@ export default modalTemplate({
if (err.status !== 401) {
throw err;
}
this.$store.dispatch('modal/sponsorOnly');
this.$store.dispatch('modal/sponsorOnly')
.catch(() => { /* Cancel */ });
}))
.catch((err) => {
console.error(err); // eslint-disable-line no-console

View File

@ -79,7 +79,7 @@ export default {
return this.$store.dispatch('modal/removeWorkspace')
.then(
() => localDbSvc.removeWorkspace(id),
() => {}, // Cancel
() => { /* Cancel */ },
);
},
},

View File

@ -42,7 +42,7 @@ export default {
this.$store.dispatch('modal/open', 'sponsor');
}
})
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
},
};

View File

@ -1,9 +1,10 @@
import store from '../store';
import fileSvc from './fileSvc';
import utils from './utils';
export default {
importBackup(jsonValue) {
const nameMap = {};
async importBackup(jsonValue) {
const fileNameMap = {};
const folderNameMap = {};
const parentIdMap = {};
const textMap = {};
const propertiesMap = {};
@ -22,24 +23,18 @@ export default {
// StackEdit v4 format
const [, v4Id, type] = v4Match;
if (type === 'title') {
nameMap[v4Id] = value;
fileNameMap[v4Id] = value;
} else if (type === 'content') {
textMap[v4Id] = value;
}
} else if (value.type === 'folder') {
// StackEdit v5 folder
const folderId = utils.uid();
const name = utils.sanitizeName(value.name);
const parentId = `${value.parentId || ''}` || null;
store.commit('folder/setItem', {
id: folderId,
name,
parentId,
});
folderIdMap[id] = folderId;
folderIdMap[id] = utils.uid();
folderNameMap[id] = value.name;
parentIdMap[id] = `${value.parentId || ''}`;
} else if (value.type === 'file') {
// StackEdit v5 file
nameMap[id] = utils.sanitizeName(value.name);
fileNameMap[id] = value.name;
parentIdMap[id] = `${value.parentId || ''}`;
} else if (value.type === 'content') {
// StackEdit v5 content
@ -54,14 +49,20 @@ export default {
}
});
// Go through the maps
Object.entries(nameMap).forEach(([externalId, name]) => store.dispatch('createFile', {
name,
await utils.awaitSequence(Object.keys(folderNameMap), async externalId => fileSvc.storeItem({
id: folderIdMap[externalId],
type: 'folder',
name: folderNameMap[externalId],
parentId: folderIdMap[parentIdMap[externalId]],
}, true));
await utils.awaitSequence(Object.keys(fileNameMap), async externalId => fileSvc.createFile({
name: fileNameMap[externalId],
parentId: folderIdMap[parentIdMap[externalId]],
text: textMap[externalId],
properties: propertiesMap[externalId],
discussions: discussionsMap[externalId],
comments: commentsMap[externalId],
}));
}, true));
},
};

View File

@ -0,0 +1,85 @@
import store from '../store';
import fileSvc from './fileSvc';
export default {
newItem(isFolder = false) {
let parentId = store.getters['explorer/selectedNodeFolder'].item.id;
if (parentId === 'trash' // Not allowed to create new items in the trash
|| (isFolder && parentId === 'temp') // Not allowed to create new folders in the temp folder
) {
parentId = null;
}
store.dispatch('explorer/openNode', parentId);
store.commit('explorer/setNewItem', {
type: isFolder ? 'folder' : 'file',
parentId,
});
},
deleteItem() {
const selectedNode = store.getters['explorer/selectedNode'];
if (selectedNode.isNil) {
return Promise.resolve();
}
if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
return store.dispatch('modal/trashDeletion').catch(() => { /* Cancel */ });
}
// See if we have a dialog to show
let modalAction;
let moveToTrash = true;
if (selectedNode.isTemp) {
modalAction = 'modal/tempFolderDeletion';
moveToTrash = false;
} else if (selectedNode.item.parentId === 'temp') {
modalAction = 'modal/tempFileDeletion';
moveToTrash = false;
} else if (selectedNode.isFolder) {
modalAction = 'modal/folderDeletion';
}
return (modalAction
? store.dispatch(modalAction, selectedNode.item)
: Promise.resolve())
.then(() => {
const deleteFile = (id) => {
if (moveToTrash) {
store.commit('file/patchItem', {
id,
parentId: 'trash',
});
} else {
fileSvc.deleteFile(id);
}
};
if (selectedNode === store.getters['explorer/selectedNode']) {
const currentFileId = store.getters['file/current'].id;
let doClose = selectedNode.item.id === currentFileId;
if (selectedNode.isFolder) {
const recursiveDelete = (folderNode) => {
folderNode.folders.forEach(recursiveDelete);
folderNode.files.forEach((fileNode) => {
doClose = doClose || fileNode.item.id === currentFileId;
deleteFile(fileNode.item.id);
});
store.commit('folder/deleteItem', folderNode.item.id);
};
recursiveDelete(selectedNode);
} else {
deleteFile(selectedNode.item.id);
}
if (doClose) {
// Close the current file by opening the last opened, not deleted one
store.getters['data/lastOpenedIds'].some((id) => {
const file = store.state.file.itemMap[id];
if (file.parentId === 'trash') {
return false;
}
store.commit('file/setCurrentId', id);
return true;
});
}
}
}, () => { /* Cancel */ });
},
};

162
src/services/fileSvc.js Normal file
View File

@ -0,0 +1,162 @@
import store from '../store';
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.
*/
createFile(fields = {}, background = false) {
const id = utils.uid();
const file = {
id,
name: utils.sanitizeName(fields.name),
parentId: fields.parentId || null,
};
const content = {
id: `${id}/content`,
text: utils.sanitizeText(fields.text || store.getters['data/computedSettings'].newFileContent),
properties: utils.sanitizeText(
fields.properties || store.getters['data/computedSettings'].newFileProperties),
discussions: fields.discussions || {},
comments: fields.comments || {},
};
const nameStripped = file.name !== utils.defaultName && file.name !== fields.name;
// Check if there is a path conflict
const workspaceUniquePaths = store.getters['workspace/hasUniquePaths'];
let pathConflict;
if (workspaceUniquePaths) {
const parentPath = store.getters.itemPaths[file.parentId] || '';
const path = parentPath + file.name;
pathConflict = !!store.getters.pathItems[path];
}
// Show warning dialogs and then save in the store
return Promise.resolve()
.then(() => !background && nameStripped && store.dispatch('modal/stripName', fields.name))
.then(() => !background && pathConflict && store.dispatch('modal/pathConflict', fields.name))
.then(() => {
store.commit('content/setItem', content);
store.commit('file/setItem', file);
if (workspaceUniquePaths) {
this.makePathUnique(id);
}
return store.state.file.itemMap[id];
});
},
/**
* Make sanity checks and then create/update the folder/file in the store.
*/
async storeItem(item, background = false) {
const id = item.id || utils.uid();
const sanitizedName = utils.sanitizeName(item.name);
if (item.type === 'folder' && forbiddenFolderNameMatcher.exec(sanitizedName)) {
if (background) {
return null;
}
await store.dispatch('modal/unauthorizedName', item.name);
throw new Error('Unauthorized name.');
}
const workspaceUniquePaths = store.getters['workspace/hasUniquePaths'];
// Show warning dialogs
if (!background) {
// If name has been stripped
if (sanitizedName !== utils.defaultName && sanitizedName !== item.name) {
await store.dispatch('modal/stripName', item.name);
}
// Check if there is a path conflict
if (workspaceUniquePaths) {
const parentPath = store.getters.itemPaths[item.parentId] || '';
const path = parentPath + sanitizedName;
const pathItems = store.getters.pathItems[path] || [];
if (pathItems.some(itemWithSamePath => itemWithSamePath.id !== id)) {
await store.dispatch('modal/pathConflict', item.name);
}
}
}
// Save item in the store
store.commit(`${item.type}/setItem`, {
id,
parentId: item.parentId || null,
name: sanitizedName,
});
// Ensure path uniqueness
if (workspaceUniquePaths) {
this.makePathUnique(id);
}
return store.getters.allItemMap[id];
},
/**
* Delete a file in the store and all its related items.
*/
deleteFile(fileId) {
// Delete the file
store.commit('file/deleteItem', fileId);
// Delete the content
store.commit('content/deleteItem', `${fileId}/content`);
// Delete the syncedContent
store.commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
// Delete the contentState
store.commit('contentState/deleteItem', `${fileId}/contentState`);
// Delete sync locations
(store.getters['syncLocation/groupedByFileId'][fileId] || [])
.forEach(item => store.commit('syncLocation/deleteItem', item.id));
// Delete publish locations
(store.getters['publishLocation/groupedByFileId'][fileId] || [])
.forEach(item => store.commit('publishLocation/deleteItem', item.id));
},
/**
* Ensure two files/folders don't have the same path if the workspace doesn't support it.
*/
ensureUniquePaths() {
if (store.getters['workspace/hasUniquePaths']) {
if (Object.keys(store.getters.itemPaths).some(id => this.makePathUnique(id))) {
this.ensureUniquePaths();
}
}
},
/**
* Return false if the file/folder path is unique.
* Add a prefix to its name and return true otherwise.
*/
makePathUnique(id) {
const item = store.getters.allItemMap[id];
if (!item) {
return false;
}
let path = store.getters.itemPaths[id];
const pathItems = store.getters.pathItems;
if (pathItems[path].length === 1) {
return false;
}
const isFolder = item.type === 'folder';
if (isFolder) {
// Remove trailing slash
path = path.slice(0, -1);
}
for (let suffix = 1; ; suffix += 1) {
let pathWithPrefix = `${path}.${suffix}`;
if (isFolder) {
pathWithPrefix += '/';
}
if (!pathItems[pathWithPrefix]) {
store.commit(`${item.type}/patchItem`, {
id: item.id,
name: `${item.name}.${suffix}`,
});
return true;
}
}
},
};

View File

@ -2,6 +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';
const dbVersion = 1;
const dbStoreName = 'objects';
@ -186,6 +187,7 @@ const localDbSvc = {
dbStore.delete(item.id);
}
});
fileSvc.ensureUniquePaths();
this.lastTx = lastTx;
cb(storeItemMap);
}
@ -249,20 +251,20 @@ const localDbSvc = {
* Read and apply one DB change.
*/
readDbItem(dbItem, storeItemMap) {
const existingStoreItem = storeItemMap[dbItem.id];
const storeItem = storeItemMap[dbItem.id];
if (!dbItem.hash) {
// DB item is a delete marker
delete this.hashMap[dbItem.type][dbItem.id];
if (existingStoreItem) {
if (storeItem) {
// Remove item from the store
store.commit(`${existingStoreItem.type}/deleteItem`, existingStoreItem.id);
delete storeItemMap[existingStoreItem.id];
store.commit(`${storeItem.type}/deleteItem`, storeItem.id);
delete storeItemMap[storeItem.id];
}
} else if (this.hashMap[dbItem.type][dbItem.id] !== dbItem.hash) {
// DB item is different from the corresponding store item
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
// Update content only if it exists in the store
if (existingStoreItem || !contentTypes[dbItem.type] || exportWorkspace) {
if (storeItem || !contentTypes[dbItem.type] || exportWorkspace) {
// Put item in the store
dbItem.tx = undefined;
store.commit(`${dbItem.type}/setItem`, dbItem);
@ -403,13 +405,14 @@ const localDbSvc = {
// Clean files
store.getters['file/items']
.filter(file => file.parentId === 'trash') // If file is in the trash
.forEach(file => store.dispatch('deleteFile', file.id));
.forEach(file => fileSvc.deleteFile(file.id));
}
// Enable sponsorship
if (utils.queryParams.paymentSuccess) {
location.hash = ''; // PaymentSuccess param is always on its own
store.dispatch('modal/paymentSuccess');
store.dispatch('modal/paymentSuccess')
.catch(() => { /* Cancel */ });
const sponsorToken = store.getters['workspace/sponsorToken'];
// Force check sponsorship after a few seconds
const currentDate = Date.now();
@ -438,10 +441,10 @@ const localDbSvc = {
store.commit('file/setCurrentId', recentFile.id);
} else {
// If still no ID, create a new file
store.dispatch('createFile', {
fileSvc.createFile({
name: 'Welcome file',
text: welcomeFile,
})
}, true)
// Set it as the current file
.then(newFile => store.commit('file/setCurrentId', newFile.id));
}

View File

@ -83,7 +83,9 @@ export default class Provider {
parentId: null,
});
}
return true;
}
}
return false;
}
}

View File

@ -2,6 +2,7 @@ import store from '../../store';
import dropboxHelper from './helpers/dropboxHelper';
import Provider from './common/Provider';
import utils from '../utils';
import fileSvc from '../fileSvc';
const makePathAbsolute = (token, path) => {
if (!token.fullAccess) {
@ -88,12 +89,6 @@ export default new Provider({
};
return this.downloadContent(token, syncLocation)
.then((content) => {
const id = utils.uid();
delete content.history;
store.commit('content/setItem', {
...content,
id: `${id}/content`,
});
let name = path;
const slashPos = name.lastIndexOf('/');
if (slashPos > -1 && slashPos < name.length - 1) {
@ -103,25 +98,30 @@ export default new Provider({
if (dotPos > 0 && slashPos < name.length) {
name = name.slice(0, dotPos);
}
store.commit('file/setItem', {
id,
name: utils.sanitizeName(name),
return fileSvc.createFile({
name,
parentId: store.getters['file/current'].parentId,
});
text: content.text,
properties: content.properties,
discussions: content.discussions,
comments: content.comments,
}, true);
})
.then((item) => {
store.commit('file/setCurrentId', item.id);
store.commit('syncLocation/setItem', {
...syncLocation,
id: utils.uid(),
fileId: id,
fileId: item.id,
});
store.commit('file/setCurrentId', id);
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Dropbox.`);
}, () => {
})
.catch(() => {
store.dispatch('notification/error', `Could not open file ${path}.`);
})
.then(() => openOneFile());
};
return Promise.resolve()
.then(() => openOneFile());
return Promise.resolve(openOneFile());
},
makeLocation(token, path) {
return {

View File

@ -2,6 +2,7 @@ import store from '../../store';
import githubHelper from './helpers/githubHelper';
import Provider from './common/Provider';
import utils from '../utils';
import fileSvc from '../fileSvc';
const savedSha = {};
@ -75,12 +76,6 @@ export default new Provider({
// Download content from GitHub and create the file
return this.downloadContent(token, syncLocation)
.then((content) => {
const id = utils.uid();
delete content.history;
store.commit('content/setItem', {
...content,
id: `${id}/content`,
});
let name = syncLocation.path;
const slashPos = name.lastIndexOf('/');
if (slashPos > -1 && slashPos < name.length - 1) {
@ -90,19 +85,25 @@ export default new Provider({
if (dotPos > 0 && slashPos < name.length) {
name = name.slice(0, dotPos);
}
store.commit('file/setItem', {
id,
name: utils.sanitizeName(name),
return fileSvc.createFile({
name,
parentId: store.getters['file/current'].parentId,
});
text: content.text,
properties: content.properties,
discussions: content.discussions,
comments: content.comments,
}, true);
})
.then((item) => {
store.commit('file/setCurrentId', item.id);
store.commit('syncLocation/setItem', {
...syncLocation,
id: utils.uid(),
fileId: id,
fileId: item.id,
});
store.commit('file/setCurrentId', id);
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from GitHub.`);
}, () => {
})
.catch(() => {
store.dispatch('notification/error', `Could not open file ${syncLocation.path}.`);
});
});

View File

@ -479,12 +479,13 @@ export default new Provider({
} else if (entry.committer && entry.committer.login) {
user = entry.committer;
}
userSvc.addInfo({ id: user.login, name: user.login, imageUrl: user.avatar_url });
const sub = `gh:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (entry.commit.author && entry.commit.author.date)
|| (entry.commit.committer && entry.commit.committer.date);
return {
id: entry.sha,
sub: user.login,
sub,
created: date ? new Date(date).getTime() : 1,
};
})

View File

@ -136,7 +136,7 @@ export default new Provider({
return googleHelper.getAppDataFileRevisions(token, syncData.id)
.then(revisions => revisions.map(revision => ({
id: revision.id,
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
sub: revision.lastModifyingUser && `go:${revision.lastModifyingUser.permissionId}`,
created: new Date(revision.modifiedTime).getTime(),
}))
.sort((revision1, revision2) => revision2.created - revision1.created));

View File

@ -2,6 +2,7 @@ import store from '../../store';
import googleHelper from './helpers/googleHelper';
import Provider from './common/Provider';
import utils from '../utils';
import fileSvc from '../fileSvc';
export default new Provider({
id: 'googleDrive',
@ -93,7 +94,7 @@ export default new Provider({
const token = store.getters['data/googleTokens'][state.userId];
switch (token && state.action) {
case 'create':
return store.dispatch('createFile')
return fileSvc.createFile({}, true)
.then((file) => {
store.commit('file/setCurrentId', file.id);
// Return a new syncLocation
@ -169,32 +170,29 @@ export default new Provider({
sub: token.sub,
};
return this.downloadContent(token, syncLocation)
.then((content) => {
const id = utils.uid();
delete content.history;
store.commit('content/setItem', {
...content,
id: `${id}/content`,
});
store.commit('file/setItem', {
id,
name: utils.sanitizeName(driveFile.name),
parentId: store.getters['file/current'].parentId,
});
.then(content => fileSvc.createFile({
name,
parentId: store.getters['file/current'].parentId,
text: content.text,
properties: content.properties,
discussions: content.discussions,
comments: content.comments,
}, true))
.then((item) => {
store.commit('file/setCurrentId', item.id);
store.commit('syncLocation/setItem', {
...syncLocation,
id: utils.uid(),
fileId: id,
fileId: item.id,
});
store.commit('file/setCurrentId', id);
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Google Drive.`);
}, () => {
})
.catch(() => {
store.dispatch('notification/error', `Could not open file ${driveFile.id}.`);
})
.then(() => openOneFile());
};
return Promise.resolve()
.then(() => openOneFile());
return Promise.resolve(openOneFile());
},
makeLocation(token, fileId, folderId) {
const location = {

View File

@ -2,6 +2,7 @@ import store from '../../store';
import googleHelper from './helpers/googleHelper';
import Provider from './common/Provider';
import utils from '../utils';
import fileSvc from '../fileSvc';
const getSyncData = (fileId) => {
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
@ -195,9 +196,9 @@ export default new Provider({
[syncData.id]: syncData,
});
}
return store.dispatch('createFile', {
return fileSvc.createFile({
parentId: syncData && syncData.itemId,
})
}, true)
.then((file) => {
store.commit('file/setCurrentId', file.id);
// File will be created on next workspace sync

View File

@ -91,9 +91,9 @@ export default {
method: 'POST',
body: { item, time: Date.now() },
};
const loginToken = store.getters['workspace/loginToken'];
if (loginToken) {
options.body.sub = loginToken.sub;
const userId = store.getters['workspace/userId'];
if (userId) {
options.body.sub = userId;
}
if (documentId) {
options.method = 'PUT';

View File

@ -73,6 +73,22 @@ export default {
addAccount(repoFullAccess = false) {
return this.startOauth2(getScopes({ repoFullAccess }));
},
getUser(userId) {
return networkSvc.request({
url: `https://api.github.com/user/${userId}`,
params: {
t: Date.now(), // Prevent from caching
},
})
.then((res) => {
store.commit('userInfo/addItem', {
id: `gh:${res.body.id}`,
name: res.body.login,
imageUrl: res.body.avatar_url || '',
});
return res.body;
});
},
getTree(token, owner, repo, sha) {
return repoRequest(token, owner, repo, {
url: `git/trees/${encodeURIComponent(sha)}?recursive=1`,
@ -86,9 +102,9 @@ export default {
},
getHeadTree(token, owner, repo, branch) {
return repoRequest(token, owner, repo, {
url: `branches/${encodeURIComponent(branch)}`,
url: `commits/${encodeURIComponent(branch)}`,
})
.then(res => this.getTree(token, owner, repo, res.body.commit.commit.tree.sha));
.then(res => this.getTree(token, owner, repo, res.body.commit.tree.sha));
},
getCommits(token, owner, repo, sha, path) {
return repoRequest(token, owner, repo, {

View File

@ -203,7 +203,7 @@ export default {
}, true)
.then((res) => {
store.commit('userInfo/addItem', {
id: res.body.id,
id: `go:${res.body.id}`,
name: res.body.displayName,
imageUrl: (res.body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
});

View File

@ -9,6 +9,7 @@ import './providers/couchdbWorkspaceProvider';
import './providers/githubWorkspaceProvider';
import './providers/googleDriveWorkspaceProvider';
import tempFileSvc from './tempFileSvc';
import fileSvc from './fileSvc';
const minAutoSyncEvery = 60 * 1000; // 60 sec
const inactivityThreshold = 3 * 1000; // 3 sec
@ -746,7 +747,7 @@ function requestSync() {
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
const file = store.state.file.itemMap[fileId];
if (file && file.hash === fileHash) {
store.dispatch('deleteFile', fileId);
fileSvc.deleteFile(fileId);
}
});
})

View File

@ -2,6 +2,7 @@ import cledit from './cledit';
import store from '../store';
import utils from './utils';
import editorSvc from './editorSvc';
import fileSvc from './fileSvc';
const origin = utils.queryParams.origin;
const fileName = utils.queryParams.fileName;
@ -29,12 +30,12 @@ export default {
store.commit('setLight', true);
return store.dispatch('createFile', {
return fileSvc.createFile({
name: fileName || utils.getHostname(origin),
text: contentText || '\n',
properties: contentProperties,
parentId: 'temp',
})
}, true)
.then((file) => {
const fileItemMap = store.state.file.itemMap;
@ -57,7 +58,7 @@ export default {
.splice(10)
.forEach(([id]) => {
delete lastCreated[id];
store.dispatch('deleteFile', id);
fileSvc.deleteFile(id);
});
// Store file creations and open the file

View File

@ -1,8 +1,16 @@
import googleHelper from './providers/helpers/googleHelper';
import githubHelper from './providers/helpers/githubHelper';
import utils from './utils';
import store from '../store';
const promised = {};
const parseUserId = (userId) => {
const prefix = userId[2] === ':' && userId.slice(0, 2);
const type = prefix && utils.userIdPrefixes[prefix];
return type ? [type, userId.slice(3)] : ['google', userId];
};
export default {
addInfo({ id, name, imageUrl }) {
promised[id] = true;
@ -10,8 +18,10 @@ export default {
},
getInfo(userId) {
if (!promised[userId]) {
const [type, sub] = parseUserId(userId);
// Try to find a token with this sub
const token = store.getters['data/googleTokens'][userId];
const token = store.getters[`data/${type}Tokens`][sub];
if (token) {
store.commit('userInfo/addItem', {
id: userId,
@ -19,16 +29,31 @@ export default {
});
}
// Get user info from Google
// Get user info from provider
if (!store.state.offline) {
promised[userId] = true;
googleHelper.getUser(userId)
.catch((err) => {
if (err.status !== 404) {
promised[userId] = false;
}
});
switch (type) {
case 'github': {
return githubHelper.getUser(sub)
.catch((err) => {
if (err.status !== 404) {
promised[userId] = false;
}
});
}
case 'google':
default: {
return googleHelper.getUser(sub)
.catch((err) => {
if (err.status !== 404) {
promised[userId] = false;
}
});
}
}
}
}
return null;
},
};

View File

@ -23,6 +23,7 @@ const parseQueryParams = (params) => {
return result;
};
// For utils.computeProperties()
const deepOverride = (obj, opt) => {
if (obj === undefined) {
@ -96,14 +97,23 @@ export default {
'layoutSettings',
'tokens',
],
userIdPrefixes: {
go: 'google',
gh: 'github',
},
textMaxLength: 250000,
sanitizeText(text) {
const result = `${text || ''}`.slice(0, this.textMaxLength);
// last char must be a `\n`.
return `${result}\n`.replace(/\n\n$/, '\n');
},
defaultName: 'Untitled',
sanitizeName(name) {
return `${name || ''}`.slice(0, 250) || 'Untitled';
return `${name || ''}`
// Replace `/`, control characters and other kind of spaces with a space
.replace(/[/\x00-\x1F\x7f-\xa0\s]+/g, ' ').trim()
// Keep only 250 characters
.slice(0, 250) || this.defaultName;
},
deepCopy,
serializeObject(obj) {
@ -124,9 +134,8 @@ export default {
// If every field fits the criteria
if (Object.entries(criteria).every(([key, value]) => value === item[key])) {
result = item;
return true;
}
return false;
return result;
});
return result;
},
@ -199,6 +208,18 @@ export default {
setInterval(func, interval) {
return setInterval(() => func(), this.randomize(interval));
},
async awaitSequence(values, asyncFunc) {
const results = [];
const valuesLeft = values.slice().reverse();
const runWithNextValue = async () => {
if (!valuesLeft.length) {
return results;
}
results.push(await asyncFunc(valuesLeft.pop()));
return runWithNextValue();
};
return runWithNextValue();
},
parseQueryParams,
addQueryParams(url = '', params = {}, hash = false) {
const keys = Object.keys(params).filter(key => params[key] != null);
@ -239,9 +260,6 @@ export default {
}
return result;
},
concatPaths(...paths) {
return paths.join('/').replace(/\/+/g, '/');
},
getHostname(url) {
urlParser.href = url;
return urlParser.hostname;

View File

@ -130,7 +130,7 @@ export default {
.then(() => syncSvc.requestSync())
.then(() => dispatch('createNewDiscussion', selection)),
}, { root: true })
.catch(() => { }); // Cancel
.catch(() => { /* Cancel */ });
} else if (selection) {
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
const maxLength = 80;

View File

@ -190,85 +190,5 @@ export default {
commit('setDragTargetId', id);
dispatch('openDragTarget');
},
newItem({ getters, commit, dispatch }, isFolder) {
let parentId = getters.selectedNodeFolder.item.id;
if (parentId === 'trash' // Not allowed to create new items in the trash
|| (isFolder && parentId === 'temp') // Not allowed to create new folders in the temp folder
) {
parentId = null;
}
dispatch('openNode', parentId);
commit('setNewItem', {
type: isFolder ? 'folder' : 'file',
parentId,
});
},
deleteItem({ rootState, getters, rootGetters, commit, dispatch }) {
const selectedNode = getters.selectedNode;
if (selectedNode.isNil) {
return Promise.resolve();
}
if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
return dispatch('modal/trashDeletion', null, { root: true });
}
// See if we have a dialog to show
let modalAction;
let moveToTrash = true;
if (selectedNode.isTemp) {
modalAction = 'modal/tempFolderDeletion';
moveToTrash = false;
} else if (selectedNode.item.parentId === 'temp') {
modalAction = 'modal/tempFileDeletion';
moveToTrash = false;
} else if (selectedNode.isFolder) {
modalAction = 'modal/folderDeletion';
}
return (modalAction
? dispatch(modalAction, selectedNode.item, { root: true })
: Promise.resolve())
.then(() => {
const deleteFile = (id) => {
if (moveToTrash) {
commit('file/patchItem', {
id,
parentId: 'trash',
}, { root: true });
} else {
dispatch('deleteFile', id, { root: true });
}
};
if (selectedNode === getters.selectedNode) {
const currentFileId = rootGetters['file/current'].id;
let doClose = selectedNode.item.id === currentFileId;
if (selectedNode.isFolder) {
const recursiveDelete = (folderNode) => {
folderNode.folders.forEach(recursiveDelete);
folderNode.files.forEach((fileNode) => {
doClose = doClose || fileNode.item.id === currentFileId;
deleteFile(fileNode.item.id);
});
commit('folder/deleteItem', folderNode.item.id, { root: true });
};
recursiveDelete(selectedNode);
} else {
deleteFile(selectedNode.item.id);
}
if (doClose) {
// Close the current file by opening the last opened, not deleted one
rootGetters['data/lastOpenedIds'].some((id) => {
const file = rootState.file.itemMap[id];
if (file.parentId === 'trash') {
return false;
}
commit('file/setCurrentId', id, { root: true });
return true;
});
}
}
}, () => {}); // Cancel
},
},
};

View File

@ -62,6 +62,7 @@ const store = new Vuex.Store({
},
itemPaths: (state) => {
const result = {};
const folderMap = state.folder.itemMap;
const getPath = (item) => {
let itemPath = result[item.id];
if (!itemPath) {
@ -69,29 +70,31 @@ const store = new Vuex.Store({
itemPath = `.stackedit-trash/${item.name}`;
} else {
let name = item.name;
if (item.type === 'folder') {
if (folderMap[item.id]) {
name += '/';
}
const parent = state.folder.itemMap[item.parentId];
if (!parent) {
itemPath = name;
const parentFolder = folderMap[item.parentId];
if (parentFolder) {
itemPath = getPath(parentFolder) + name;
} else {
itemPath = getPath(parent) + name;
itemPath = name;
}
}
}
result[item.id] = itemPath;
return itemPath;
};
[...state.folder.items, ...state.file.items].forEach(item => getPath(item));
return result;
},
pathItems: (state, getters) => {
const result = {};
const itemPaths = getters.itemPaths;
const allItemMap = getters.allItemMap;
Object.entries(itemPaths).forEach(([id, path]) => {
result[path] = allItemMap[id];
Object.entries(getters.itemPaths).forEach(([id, path]) => {
const items = result[path] || [];
items.push(allItemMap[id]);
result[path] = items;
});
return result;
},
@ -131,33 +134,6 @@ const store = new Vuex.Store({
}
return Promise.resolve();
},
createFile({ state, getters, commit }, desc = {}) {
const id = utils.uid();
commit('content/setItem', {
id: `${id}/content`,
text: utils.sanitizeText(desc.text || getters['data/computedSettings'].newFileContent),
properties: utils.sanitizeText(
desc.properties || getters['data/computedSettings'].newFileProperties),
discussions: desc.discussions || {},
comments: desc.comments || {},
});
commit('file/setItem', {
id,
name: utils.sanitizeName(desc.name),
parentId: desc.parentId || null,
});
return Promise.resolve(state.file.itemMap[id]);
},
deleteFile({ getters, commit }, fileId) {
(getters['syncLocation/groupedByFileId'][fileId] || [])
.forEach(item => commit('syncLocation/deleteItem', item.id));
(getters['publishLocation/groupedByFileId'][fileId] || [])
.forEach(item => commit('publishLocation/deleteItem', item.id));
commit('file/deleteItem', fileId);
commit('content/deleteItem', `${fileId}/content`);
commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
commit('contentState/deleteItem', `${fileId}/contentState`);
},
},
strict: debug,
plugins: debug ? [createLogger()] : [],

View File

@ -74,13 +74,27 @@ export default {
}),
trashDeletion: ({ dispatch }) => dispatch('open', {
content: '<p>Files in the trash are automatically deleted after 7 days of inactivity.</p>',
resolveText: 'Ok',
rejectText: 'Ok',
}),
fileRestoration: ({ dispatch }) => dispatch('open', {
content: '<p>You are about to revert some changes. Are you sure?</p>',
resolveText: 'Yes, revert',
rejectText: 'No',
}),
unauthorizedName: ({ dispatch }, name) => dispatch('open', {
content: `<p><b>${name}</b> is not an authorized name.</p>`,
rejectText: 'Ok',
}),
stripName: ({ dispatch }, name) => dispatch('open', {
content: `<p><b>${name}</b> contains illegal characters. Do you want to strip them?</p>`,
resolveText: 'Yes, strip',
rejectText: 'No',
}),
pathConflict: ({ dispatch }, name) => dispatch('open', {
content: `<p><b>${name}</b> already exists. Do you want to add a suffix?</p>`,
resolveText: 'Yes, add suffix',
rejectText: 'No',
}),
removeWorkspace: ({ dispatch }) => dispatch('open', {
content: '<p>You are about to remove a workspace locally. Are you sure?</p>',
resolveText: 'Yes, remove',
@ -127,11 +141,11 @@ export default {
}),
sponsorOnly: ({ dispatch }) => dispatch('open', {
content: '<p>This feature is restricted to sponsors as it relies on server resources.</p>',
resolveText: 'Ok, I understand',
rejectText: 'Ok, I understand',
}),
paymentSuccess: ({ dispatch }) => dispatch('open', {
content: '<p>Thank you for your payment! Your sponsorship will be active in a minute.</p>',
resolveText: 'Ok',
rejectText: 'Ok',
}),
},
};

View File

@ -11,7 +11,10 @@ export default (empty, simpleHash = false) => {
itemMap: {},
},
getters: {
items: state => Object.entries(state.itemMap).map(([, item]) => item),
items: (state) => {
console.log(state.itemMap);
return Object.values(state.itemMap);
},
},
mutations: {
setItem(state, value) {

View File

@ -1,3 +1,5 @@
import utils from '../services/utils';
export default {
namespaced: true,
state: {
@ -21,6 +23,10 @@ export default {
const workspaces = rootGetters['data/sanitizedWorkspaces'];
return workspaces[state.currentWorkspaceId] || getters.mainWorkspace;
},
hasUniquePaths: (state, getters) => {
const workspace = getters.currentWorkspace;
return workspace.providerId === 'githubWorkspace';
},
lastSyncActivityKey: (state, getters) => `${getters.currentWorkspace.id}/lastSyncActivity`,
lastFocusKey: (state, getters) => `${getters.currentWorkspace.id}/lastWindowFocus`,
mainWorkspaceToken: (state, getters, rootState, rootGetters) => {
@ -55,10 +61,28 @@ export default {
const googleTokens = rootGetters['data/googleTokens'];
return googleTokens[workspace.sub];
}
case 'githubWorkspace': {
const githubTokens = rootGetters['data/githubTokens'];
return githubTokens[workspace.sub];
}
default:
return getters.mainWorkspaceToken;
}
},
userId: (state, getters, rootState, rootGetters) => {
const loginToken = getters.loginToken;
if (!loginToken) {
return null;
}
let prefix;
Object.entries(utils.userIdPrefixes).some(([key, value]) => {
if (rootGetters[`data/${value}Tokens`][loginToken.sub]) {
prefix = key;
}
return prefix;
});
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
},
sponsorToken: (state, getters) => getters.mainWorkspaceToken,
},
actions: {